source: trunk/abcl/src/org/armedbear/lisp/Pathname.java @ 14624

Last change on this file since 14624 was 14624, checked in by mevenson, 4 years ago

Fix (remaining?) bugs for DIRECTORY.

Fixes Quicklisp, aka the "DIRECTORY no longer works with
:WILD-INFERIORS" problem, q.v. <http://abcl.org/trac/ticket/344>.

DIRECTORY under non-Windows now fills nil DEVICE components with
:UNSPECIFIC, otherwise forms like

(equal (truename "~/.emacs")

(first (directory "~/.emacs")) )

fail (c.f. ANSI test DIRECTORY.[67]).

  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
File size: 103.6 KB
Line 
1/*
2 * Pathname.java
3 *
4 * Copyright (C) 2003-2007 Peter Graves
5 * $Id: Pathname.java 14624 2014-02-06 14:28:50Z mevenson $
6 *
7 * This program is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation; either version 2
10 * of the License, or (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program; if not, write to the Free Software
19 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
20 *
21 * As a special exception, the copyright holders of this library give you
22 * permission to link this library with independent modules to produce an
23 * executable, regardless of the license terms of these independent
24 * modules, and to copy and distribute the resulting executable under
25 * terms of your choice, provided that you also meet, for each linked
26 * independent module, the terms and conditions of the license of that
27 * module.  An independent module is a module which is not derived from
28 * or based on this library.  If you modify this library, you may extend
29 * this exception to your version of the library, but you are not
30 * obligated to do so.  If you do not wish to do so, delete this
31 * exception statement from your version.
32 */
33package org.armedbear.lisp;
34
35import static org.armedbear.lisp.Lisp.*;
36
37import java.io.File;
38import java.io.IOException;
39import java.io.InputStream;
40import java.io.FileInputStream;
41import java.net.MalformedURLException;
42import java.net.URI;
43import java.net.URISyntaxException;
44import java.net.URL;
45import java.net.URLConnection;
46import java.text.MessageFormat;
47import java.util.Enumeration;
48import java.util.StringTokenizer;
49import java.util.zip.ZipEntry;
50import java.util.zip.ZipFile;
51import java.util.zip.ZipInputStream;
52
53public class Pathname extends LispObject {
54
55    /** The path component separator used by internally generated
56     * path namestrings.
57     */
58    public final static char separator = '/';
59
60    protected LispObject host = NIL;
61    protected LispObject device = NIL;
62    protected LispObject directory = NIL;
63    protected LispObject name = NIL;
64    // A string, NIL, :WILD or :UNSPECIFIC.
65    protected LispObject type = NIL;
66    // A positive integer, or NIL, :WILD, :UNSPECIFIC, or :NEWEST.
67    protected LispObject version = NIL;
68
69    private volatile String namestring;
70
71    /** The protocol for changing any instance field (i.e. 'host',
72     *  'type', etc.)  is to call this method after changing the field
73     *  to recompute the namestring.  We could do this with
74     *  setter/getters, but that choose not to in order to avoid the
75     *  performance indirection penalty.
76     *
77     *  TODO There is no "perfomance penalty" in contemporary
78     *  compilers which inline such access, so it would be better to
79     *  implement this as setter/getter ME 20110622
80     *
81     *  Although, given the number of bugs that crop up when this
82     *  protocol is not adhered to, maybe we should consider it.
83     */
84    public void invalidateNamestring() {
85        namestring = null;
86    }
87   
88    private static final Primitive _INVALIDATE_NAMESTRING
89        = new pf_invalidate_namestring();
90    @DocString(name="%invalidate-namestring",
91               args="pathname",
92               returns="pathname")
93    private static class pf_invalidate_namestring extends Primitive {
94        pf_invalidate_namestring() {
95            super("%invalidate-namestring", PACKAGE_EXT, false);
96        }
97        @Override
98        public LispObject execute(LispObject first) {
99            ((Pathname)coerceToPathname(first)).invalidateNamestring();
100            return first;
101        }
102    }
103
104    protected Pathname() {}
105
106    /** Copy constructor which shares no structure with the original. */
107    protected Pathname(Pathname p) {
108        if (p.host != NIL) {
109            if (p.host instanceof SimpleString) {
110                host = new SimpleString(((SimpleString)p.host).getStringValue());
111            } else  if (p.host instanceof Symbol) {
112                host = p.host;
113            } else if (p.host instanceof Cons) {
114                host = new Cons((Cons)p.host);
115            } else {
116                Debug.assertTrue(false);
117            }
118        }
119        if (p.device != NIL) {
120            if (p.device instanceof SimpleString) {
121                device = new SimpleString(((SimpleString)p.device).getStringValue());
122            } else if (p.device instanceof Cons) {
123                Cons jars = (Cons)p.device;
124                device = new Cons(NIL, NIL);
125                LispObject first = jars.car();
126                if (first instanceof Pathname) {
127                    ((Cons)device).car = new Pathname((Pathname)first);
128                } else {
129                    Debug.assertTrue(false);
130                }
131                if (!jars.cdr().equals(NIL)) {
132                    if (jars.cdr() instanceof Cons) {
133                        ((Cons)device).cdr = new Cons(new Pathname((Pathname)jars.cdr().car()), NIL);
134                    } else { 
135                        Debug.assertTrue(false);
136                    }
137                }
138            } else if (p.device instanceof Symbol) {
139                device = p.device;
140            } else {
141                Debug.assertTrue(false);
142            }               
143        }
144        if (p.directory != NIL) {
145            if (p.directory instanceof Cons) {
146                directory = NIL;
147                for (LispObject list = p.directory; list != NIL; list = list.cdr()) {
148                    LispObject o = list.car();
149                    if (o instanceof Symbol) {
150                        directory = directory.push(o);
151                    } else if (o instanceof SimpleString) {
152                        directory = directory.push(new SimpleString(((SimpleString)o).getStringValue()));
153                    } else {
154                        Debug.assertTrue(false);
155                    }
156                }
157                directory.nreverse();
158            } else {
159                Debug.assertTrue(false);
160            }
161        }
162        if (p.name != NIL) {
163            if (p.name instanceof SimpleString) {
164                name = new SimpleString(((SimpleString)p.name).getStringValue());
165            } else if (p.name instanceof Symbol) {
166                name = p.name;
167            } else {
168                Debug.assertTrue(false);
169            }
170        } 
171        if (p.type != NIL) {
172            if (p.type instanceof SimpleString) {
173                type = new SimpleString(((SimpleString)p.type).getStringValue());
174            } else if (p.type instanceof Symbol) {
175                type = p.type;
176            } else {
177                Debug.assertTrue(false);
178            }
179        }
180    if (p.version != NIL) {
181        if (p.version instanceof Symbol) {
182        version = p.version;
183        } else if (p.version instanceof LispInteger) {
184        version = p.version;
185        } else {
186        Debug.assertTrue(false);
187        }
188    }
189    }
190
191    public Pathname(String s) {
192        init(s);
193    }
194
195    public static boolean isSupportedProtocol(String protocol) {
196        // There is no programmatic way to know what protocols will
197        // sucessfully construct a URL, so we check for well known ones...
198        if ("jar".equals(protocol) 
199            || "file".equals(protocol))
200            //            || "http".equals(protocol))  XXX remove this as an optimization
201            {
202                return true;
203            }
204        // ... and try the entire constructor with some hopefully
205        // reasonable parameters for everything else.
206        try {
207            new URL(protocol, "example.org", "foo");
208            return true;
209        }  catch (MalformedURLException e) {
210            return false;
211        }
212    }
213
214    public Pathname(URL url) {
215         // URL handling is now buried in init(String), as the URI
216         // escaping mechanism didn't interact well with '+' and other
217         // characters.
218        init(url.toString());
219    }
220   
221    public Pathname(URI uri) {
222        init(uri.toString());
223    }
224
225    static final Symbol SCHEME = internKeyword("SCHEME");
226    static final Symbol AUTHORITY = internKeyword("AUTHORITY");
227    static final Symbol QUERY = internKeyword("QUERY");
228    static final Symbol FRAGMENT = internKeyword("FRAGMENT");
229
230    static final private String jarSeparator = "!/";
231    private final void init(String s) {
232        if (s == null) {
233            return;
234        }
235        if (s.equals(".") || s.equals("./")
236          || (Utilities.isPlatformWindows && s.equals(".\\"))) {
237            directory = new Cons(Keyword.RELATIVE);
238            return;
239        }
240        if (s.equals("..") || s.equals("../")) {
241            directory = list(Keyword.RELATIVE, Keyword.UP);
242            return;
243        }
244        if (Utilities.isPlatformWindows) {
245            if (s.startsWith("\\\\")) { // XXX What if string starts with '//'?
246                //UNC path support
247                // match \\<server>\<share>\[directories-and-files]
248
249                int shareIndex = s.indexOf('\\', 2);
250                int dirIndex = s.indexOf('\\', shareIndex + 1);
251
252                if (shareIndex == -1 || dirIndex == -1) {
253                    error(new LispError("Unsupported UNC path format: \"" + s + '"'));
254                }
255
256                host = new SimpleString(s.substring(2, shareIndex));
257                device = new SimpleString(s.substring(shareIndex + 1, dirIndex));
258
259                Pathname p = new Pathname(s.substring(dirIndex));
260                directory = p.directory;
261                name = p.name;
262                type = p.type;
263                version = p.version;
264                invalidateNamestring();
265                return;
266            }
267        }
268       
269        // A JAR file
270        if (s.startsWith("jar:") && s.endsWith(jarSeparator)) {
271            LispObject jars = NIL;
272            int i = s.lastIndexOf(jarSeparator, s.length() - jarSeparator.length() - 1);
273            String jar = null;
274            if (i == -1) {
275                jar = s;
276            } else {
277                // There can be no more than two jar references and the
278                // inner one must be a file reference within the outer.
279                jar = "jar:file:" + s.substring(i + jarSeparator.length());
280                s = s.substring("jar:".length(), i + jarSeparator.length());
281                Pathname p = new Pathname(s);
282                jars = jars.push(p.device.car());
283            }
284            if (jar.startsWith("jar:file:")) {
285                String file
286                    = jar.substring("jar:file:".length(),
287                                    jar.length() - jarSeparator.length());
288                Pathname jarPathname;
289                if (file.length() > 0) {
290                    URL url = null;
291                    URI uri = null;
292                    try {
293                        url = new URL("file:" + file);
294                        uri = url.toURI();
295                    } catch (MalformedURLException e1) {
296                        error(new SimpleError("Failed to create URI from "
297                                            + "'" + file + "'"
298                                            + ": " + e1.getMessage()));
299                    } catch (URISyntaxException e2) {
300                        error(new SimpleError("Failed to create URI from "
301                                            + "'" + file + "'"
302                                            + ": " + e2.getMessage()));
303                    }
304                    String path = uri.getPath();
305                    if (path == null) {
306                        // We allow "jar:file:baz.jar!/" to construct a relative
307                        // path for jar files, so MERGE-PATHNAMES means something.
308                        jarPathname = new Pathname(uri.getSchemeSpecificPart());
309                    } else {
310                        jarPathname = new Pathname((new File(path)).getPath());
311                    }
312                } else {
313                    jarPathname = new Pathname("");
314                }
315                jars = jars.push(jarPathname);
316            } else {
317                URL url = null;
318                try {
319                    url = new URL(jar.substring("jar:".length(), jar.length() - 2));
320                    Pathname p = new Pathname(url);
321                    jars = jars.push(p);
322                } catch (MalformedURLException e) {
323                    error(new LispError("Failed to parse URL "
324                                        + "'" + url + "'"
325                                        + e.getMessage()));
326                }
327            }
328            jars = jars.nreverse();
329            device = jars;
330            invalidateNamestring();
331            return;
332        }
333
334        // An entry in a JAR file
335        final int separatorIndex = s.lastIndexOf(jarSeparator);
336        if (separatorIndex > 0 && s.startsWith("jar:")) {
337            final String jarURL = s.substring(0, separatorIndex + jarSeparator.length());
338            URL url = null;
339            try {
340                url = new URL(jarURL);
341            } catch (MalformedURLException ex) {
342                error(new LispError("Failed to parse URL "
343                                    + "'" + jarURL + "'"
344                                    + ex.getMessage()));
345            }
346            Pathname d = new Pathname(url);
347            if (device instanceof Cons) {
348                LispObject[] jars = d.copyToArray();
349                //  XXX Is this ever reached?  If so, need to append lists
350                Debug.assertTrue(false);
351            } else {
352                device = d.device;
353            }
354            s = "/" + s.substring(separatorIndex + jarSeparator.length());
355            Pathname p = new Pathname("file:" + s); // Use URI escaping rules
356            directory = p.directory;
357            name = p.name;
358            type = p.type;
359            version = p.version;
360            return;
361        }
362
363        // A URL
364        if (isValidURL(s)) {
365            URL url = null;
366            try {
367                url = new URL(s);
368            } catch (MalformedURLException e) {
369                Debug.assertTrue(false);
370            }
371            String scheme = url.getProtocol();
372            if (scheme.equals("file")) {
373                URI uri = null;
374                try {
375                    uri = new URI(s);
376                } catch (URISyntaxException ex) {
377                    error(new SimpleError("Improper URI syntax for "
378                                    + "'" + url.toString() + "'"
379                                    + ": " + ex.toString()));
380                }
381           
382                String uriPath = uri.getPath();
383                if (null == uriPath) {
384          // Under Windows, deal with pathnames containing
385          // devices expressed as "file:z:/foo/path"
386          uriPath = uri.getSchemeSpecificPart();
387          if (uriPath == null || uriPath.equals("")) {
388                    error(new LispError("The URI has no path: " + uri));
389                  }
390                }
391                final File file = new File(uriPath);
392                String path = file.getPath();
393                if (uri.toString().endsWith("/") && !path.endsWith("/")) {
394                  path += "/";
395                }
396                final Pathname p = new Pathname(path);
397                this.host = p.host;
398                this.device = p.device;
399                this.directory = p.directory;
400                this.name = p.name;
401                this.type = p.type;
402                this.version = p.version;
403                return;
404            }
405            Debug.assertTrue(scheme != null);
406            URI uri = null;
407            try { 
408                uri = url.toURI().normalize();
409            } catch (URISyntaxException e) {
410                error(new LispError("Couldn't form URI from "
411                                    + "'" + url + "'"
412                                    + " because: " + e));
413            }
414            String authority = uri.getAuthority();
415        if (authority == null) {
416          authority = url.getAuthority();
417          if (authority == null) {
418            Debug.trace(MessageFormat.format("{0} has a null authority.", url));
419          }
420        }
421
422        host = NIL;
423        host = host.push(SCHEME);
424        host = host.push(new SimpleString(scheme));
425
426        if (authority != null) {
427          host = host.push(AUTHORITY);
428          host = host.push(new SimpleString(authority));
429        }
430
431        device = NIL;
432           
433        // URI encode necessary characters
434        String path = uri.getRawPath();
435        if (path == null) {
436          path = "";
437        } 
438        String query = uri.getRawQuery();
439        if (query != null) {
440          host = host.push(QUERY);
441                host = host.push(new SimpleString(query));
442            }
443            String fragment = uri.getRawFragment();
444            if (fragment != null) {
445                host = host.push(FRAGMENT);
446                host = host.push(new SimpleString(fragment));
447            }
448            Pathname p = new Pathname(path != null ? path : ""); 
449
450            directory = p.directory;
451            name = p.name;
452            type = p.type;
453           
454            host = host.nreverse();
455            invalidateNamestring();
456            return;
457        }
458
459        if (Utilities.isPlatformWindows) {
460            if (s.contains("\\")) {
461                s = s.replace("\\", "/");
462            } 
463        }
464
465        // Expand user home directories
466        if (Utilities.isPlatformUnix) {
467            if (s.equals("~")) {
468                s = System.getProperty("user.home").concat("/");
469            } else if (s.startsWith("~/")) {
470                s = System.getProperty("user.home").concat(s.substring(1));
471            }
472        }
473        namestring = s;
474        if (Utilities.isPlatformWindows) {
475            if (s.length() >= 2 && s.charAt(1) == ':') {
476                device = new SimpleString(s.charAt(0));
477                s = s.substring(2);
478            }
479        }
480        String d = null;
481        // Find last file separator char.
482        for (int i = s.length(); i-- > 0;) {
483            if (s.charAt(i) == '/') {
484                d = s.substring(0, i + 1);
485                s = s.substring(i + 1);
486                break;
487            }
488        }
489        if (d != null) {
490            if (s.equals("..")) {
491                d = d.concat(s);
492                s = "";
493            }
494            directory = parseDirectory(d);
495        }
496        if (s.startsWith(".") 
497            // No TYPE can be parsed
498            && (s.indexOf(".", 1) == -1 
499                || s.substring(s.length() -1).equals("."))) {
500            name = new SimpleString(s);
501            return;
502        }
503        int index = s.lastIndexOf('.');
504        String n = null;
505        String t = null;
506        if (index > 0) {
507            n = s.substring(0, index);
508            t = s.substring(index + 1);
509        } else if (s.length() > 0) {
510            n = s;
511        }
512        if (n != null) {
513            if (n.equals("*")) {
514                name = Keyword.WILD;
515            } else {
516                name = new SimpleString(n);
517            }
518        }
519        if (t != null) {
520            if (t.equals("*")) {
521                type = Keyword.WILD;
522            } else {
523                type = new SimpleString(t);
524            }
525        }
526    }
527
528    private static final LispObject parseDirectory(String d) {
529        if (d.equals("/") || (Utilities.isPlatformWindows && d.equals("\\"))) {
530            return new Cons(Keyword.ABSOLUTE);
531        }
532        LispObject result;
533        if (d.startsWith("/") || (Utilities.isPlatformWindows && d.startsWith("\\"))) {
534            result = new Cons(Keyword.ABSOLUTE);
535        } else {
536            result = new Cons(Keyword.RELATIVE);
537        }
538        StringTokenizer st = new StringTokenizer(d, "/\\");
539        while (st.hasMoreTokens()) {
540            String token = st.nextToken();
541            LispObject obj;
542            if (token.equals("*")) {
543                obj = Keyword.WILD;
544            } else if (token.equals("**")) {
545                obj = Keyword.WILD_INFERIORS;
546            } else if (token.equals("..")) {
547                if (result.car() instanceof AbstractString) {
548                    result = result.cdr();
549                    continue;
550                }
551                obj = Keyword.UP;
552            } else {
553                obj = new SimpleString(token);
554            }
555            result = new Cons(obj, result);
556        }
557        return result.nreverse();
558    }
559
560    @Override
561    public LispObject getParts() {
562        LispObject parts = NIL;
563        parts = parts.push(new Cons("HOST", host));
564        parts = parts.push(new Cons("DEVICE", device));
565        parts = parts.push(new Cons("DIRECTORY", directory));
566        parts = parts.push(new Cons("NAME", name));
567        parts = parts.push(new Cons("TYPE", type));
568        parts = parts.push(new Cons("VERSION", version));
569        return parts.nreverse();
570    }
571
572    @Override
573    public LispObject typeOf() {
574        if (isURL()) {
575            return Symbol.URL_PATHNAME;
576        } 
577        if (isJar()) {
578            return Symbol.JAR_PATHNAME;
579        }
580        return Symbol.PATHNAME;
581    }
582
583    @Override
584    public LispObject classOf() {
585        if (isURL()) {
586            return BuiltInClass.URL_PATHNAME;
587        } 
588        if (isJar()) {
589            return BuiltInClass.JAR_PATHNAME;
590        }
591        return BuiltInClass.PATHNAME;
592    }
593
594    @Override
595    public LispObject typep(LispObject type) {
596        if (type == Symbol.PATHNAME) {
597            return T;
598        }
599        if (type == Symbol.JAR_PATHNAME && isJar()) {
600            return T;
601        }
602        if (type == Symbol.URL_PATHNAME && isURL()) {
603            return T;
604        }
605        if (type == BuiltInClass.PATHNAME) {
606            return T;
607        }
608        if (type == BuiltInClass.JAR_PATHNAME && isJar()) {
609            return T;
610        }
611        if (type == BuiltInClass.URL_PATHNAME && isURL()) {
612            return T;
613        }
614        return super.typep(type);
615    }
616
617    public final LispObject getDevice() {
618        return device;
619    }
620
621    public String getNamestring() {
622        if (namestring != null) {
623            return namestring;
624        }
625        if (name == NIL && type != NIL) {
626            Debug.assertTrue(namestring == null);
627            return null;
628        }
629        if (directory instanceof AbstractString) {
630            Debug.assertTrue(false);
631        }
632        StringBuilder sb = new StringBuilder();
633        // "If a pathname is converted to a namestring, the symbols NIL and
634        // :UNSPECIFIC cause the field to be treated as if it were empty. That
635        // is, both NIL and :UNSPECIFIC cause the component not to appear in
636        // the namestring." 19.2.2.2.3.1
637        if (host != NIL) {
638            Debug.assertTrue(host instanceof AbstractString
639                             || isURL());
640            if (isURL()) {
641                LispObject scheme = Symbol.GETF.execute(host, SCHEME, NIL);
642                LispObject authority = Symbol.GETF.execute(host, AUTHORITY, NIL);
643                Debug.assertTrue(scheme != NIL);
644                sb.append(scheme.getStringValue());
645                sb.append(":");
646                if (authority != NIL) {
647                    sb.append("//");
648                    sb.append(authority.getStringValue());
649                }
650            } else if (this instanceof LogicalPathname) {
651                sb.append(host.getStringValue());
652                sb.append(':');
653            } else { 
654                // UNC paths now use unprintable representation
655                return null;
656            }
657        }
658        boolean uriEncoded = false;
659        if (device == NIL) {
660        } else if (device == Keyword.UNSPECIFIC) {
661        } else if (isJar()) {
662            LispObject[] jars = ((Cons) device).copyToArray();
663            StringBuilder prefix = new StringBuilder();
664            for (int i = 0; i < jars.length; i++) {
665                prefix.append("jar:");
666        LispObject component = jars[i];
667        if (!(component instanceof Pathname)) {
668           return null; // If DEVICE is a CONS, it should only contain Pathname
669        }
670                if (! ((Pathname)component).isURL() && i == 0) {
671                    sb.append("file:");
672                    uriEncoded = true;
673                }
674                Pathname jar = (Pathname) component;
675                String encodedNamestring;
676                if (uriEncoded) {
677                    encodedNamestring = uriEncode(jar.getNamestring());
678                } else { 
679                    encodedNamestring = jar.getNamestring();
680                }
681                sb.append(encodedNamestring);
682                sb.append("!/");
683            }
684            sb = prefix.append(sb);
685        } else if (device instanceof AbstractString) {
686            sb.append(device.getStringValue());
687            if (this instanceof LogicalPathname
688              || host == NIL) {
689                sb.append(':'); // non-UNC paths
690            }
691        } else {
692            Debug.assertTrue(false);
693        }
694        String directoryNamestring = getDirectoryNamestring();
695        if (uriEncoded) {
696            directoryNamestring = uriEncode(directoryNamestring);
697        }
698        if (isJar()) {
699            if (directoryNamestring.startsWith("/")) {
700                sb.append(directoryNamestring.substring(1));
701            } else {
702                sb.append(directoryNamestring);
703            }
704        } else {
705            sb.append(directoryNamestring);
706        }
707        if (name instanceof AbstractString) {
708            String n = name.getStringValue();
709            if (n.indexOf('/') >= 0) {
710                Debug.assertTrue(namestring == null);
711                return null;
712            }
713            if (uriEncoded) {
714                sb.append(uriEncode(n));
715            } else {
716                sb.append(n);
717            }
718        } else if (name == Keyword.WILD) {
719            sb.append('*');
720        }
721        if (type != NIL && type != Keyword.UNSPECIFIC) {
722            sb.append('.');
723            if (type instanceof AbstractString) {
724                String t = type.getStringValue();
725                // Allow Windows shortcuts to include TYPE
726                if (!(t.endsWith(".lnk") && Utilities.isPlatformWindows)) {
727                    if (t.indexOf('.') >= 0) {
728                        Debug.assertTrue(namestring == null);
729                        return null;
730                    }
731                }
732                if (uriEncoded) {
733                    sb.append(uriEncode(t));
734                } else {
735                    sb.append(t);
736                }
737            } else if (type == Keyword.WILD) {
738                sb.append('*');
739            } else {
740                Debug.assertTrue(false);
741            }
742        }
743       
744        if (isURL()) {
745            LispObject o = Symbol.GETF.execute(host, QUERY, NIL);
746            if (o != NIL) {
747                sb.append("?");
748                sb.append(o.getStringValue());
749            }
750            o = Symbol.GETF.execute(host, FRAGMENT, NIL);
751            if (o != NIL) {
752                sb.append("#");
753                sb.append(o.getStringValue());
754            }
755        }
756           
757        if (this instanceof LogicalPathname) {
758            if (version.integerp()) {
759                sb.append('.');
760                int base = Fixnum.getValue(Symbol.PRINT_BASE.symbolValue());
761                if (version instanceof Fixnum) {
762                    sb.append(Integer.toString(((Fixnum) version).value, base).toUpperCase());
763                } else if (version instanceof Bignum) {
764                    sb.append(((Bignum) version).value.toString(base).toUpperCase());
765                }
766            } else if (version == Keyword.WILD) {
767                sb.append(".*");
768            } else if (version == Keyword.NEWEST) {
769                sb.append(".NEWEST");
770            }
771        }
772        namestring = sb.toString();
773        // XXX Decide if this is necessary
774        // if (isURL()) {
775        //     namestring = Utilities.uriEncode(namestring);
776        // }
777        return namestring;
778    }
779
780    protected String getDirectoryNamestring() {
781        validateDirectory(true);
782        StringBuilder sb = new StringBuilder();
783        // "If a pathname is converted to a namestring, the symbols NIL and
784        // :UNSPECIFIC cause the field to be treated as if it were empty. That
785        // is, both NIL and :UNSPECIFIC cause the component not to appear in
786        // the namestring." 19.2.2.2.3.1
787        if (directory != NIL) {
788            final char separatorChar = '/';
789            LispObject temp = directory;
790            LispObject part = temp.car();
791            temp = temp.cdr();
792            if (part == Keyword.ABSOLUTE) {
793                sb.append(separatorChar);
794            } else if (part == Keyword.RELATIVE) {
795                if (temp == NIL) {
796                    // #p"./"
797                    sb.append('.');
798                    sb.append(separatorChar);
799                }
800                // else: Nothing to do.
801            } else {
802                error(new FileError("Unsupported directory component "
803                  + part.printObject() + ".",
804                  this));
805            }
806            while (temp != NIL) {
807                part = temp.car();
808                if (part instanceof AbstractString) {
809                    sb.append(part.getStringValue());
810                } else if (part == Keyword.WILD) {
811                    sb.append('*');
812                } else if (part == Keyword.WILD_INFERIORS) {
813                    sb.append("**");
814                } else if (part == Keyword.UP) {
815                    sb.append("..");
816                } else {
817                    error(new FileError("Unsupported directory component " + part.princToString() + ".",
818                      this));
819                }
820                sb.append(separatorChar);
821                temp = temp.cdr();
822            }
823        }
824        return sb.toString();
825    }
826
827    /** @return The representation of this pathname suitable for
828     *  referencing an entry in a Zip/JAR file
829     */
830    protected String asEntryPath() {
831        Pathname p = new Pathname();
832        p.directory = directory;
833        p.name = name;
834        p.type = type;
835        p.invalidateNamestring();
836        String path = p.getNamestring();
837        StringBuilder result = new StringBuilder();
838        result.append(path);
839
840        // Entries in jar files are always relative, but Pathname
841        // directories are :ABSOLUTE.
842        if (result.length() > 1
843          && result.substring(0, 1).equals("/")) {
844            return result.substring(1);
845        }
846        return result.toString();
847    }
848
849    @Override
850    public boolean equal(LispObject obj) {
851        if (this == obj) {
852            return true;
853        }
854        if (obj instanceof Pathname) {
855            Pathname p = (Pathname) obj;
856            if (Utilities.isPlatformWindows) {
857                if (!host.equalp(p.host)) {
858                    return false;
859                }
860                if (!device.equalp(p.device)) {
861                    return false;
862                }
863                if (!directory.equalp(p.directory)) {
864                    return false;
865                }
866                if (!name.equalp(p.name)) {
867                    return false;
868                }
869                if (!type.equalp(p.type)) {
870                    return false;
871                }
872                // Ignore version component.
873                //if (!version.equalp(p.version))
874                //    return false;
875            } else {
876                // Unix.
877                if (!host.equal(p.host)) {
878                    return false;
879                }
880                if (!device.equal(p.device)) {
881                    return false;
882                }
883                if (!directory.equal(p.directory)) {
884                    return false;
885                }
886                if (!name.equal(p.name)) {
887                    return false;
888                }
889                if (!type.equal(p.type)) {
890                    return false;
891                }
892                // Ignore version component.
893                //if (!version.equal(p.version))
894                //    return false;
895            }
896            return true;
897        }
898        return false;
899    }
900
901    @Override
902    public boolean equalp(LispObject obj) {
903        return equal(obj);
904    }
905
906    @Override
907    public int sxhash() {
908        return ((host.sxhash()
909          ^ device.sxhash()
910          ^ directory.sxhash()
911          ^ name.sxhash()
912          ^ type.sxhash()) & 0x7fffffff);
913    }
914
915    @Override
916    public String printObject() {
917        final LispThread thread = LispThread.currentThread();
918        final boolean printReadably = (Symbol.PRINT_READABLY.symbolValue(thread) != NIL);
919        final boolean printEscape = (Symbol.PRINT_ESCAPE.symbolValue(thread) != NIL);
920        boolean useNamestring;
921        String s = null;
922        s = getNamestring();
923        if (s != null) {
924            useNamestring = true;
925            if (printReadably) {
926                // We have a namestring. Check for pathname components that
927                // can't be read from the namestring.
928                if ((host != NIL && !isURL())
929                    || version != NIL) 
930                {
931                    useNamestring = false;
932                } else if (name instanceof AbstractString) {
933                    String n = name.getStringValue();
934                    if (n.equals(".") || n.equals("..")) {
935                        useNamestring = false;
936                    } else if (n.indexOf(File.separatorChar) >= 0) {
937                        useNamestring = false;
938                    }
939                }
940            }
941        } else { 
942            useNamestring = false;
943        }
944        StringBuilder sb = new StringBuilder();
945
946        if (useNamestring) {
947            if (printReadably || printEscape) {
948                sb.append("#P\"");
949            }
950            final int limit = s.length();
951            for (int i = 0; i < limit; i++) {
952                char c = s.charAt(i);
953                if (printReadably || printEscape) {
954                    if (c == '\"' || c == '\\') {
955                        sb.append('\\');
956                    }
957                }
958                sb.append(c);
959            }
960            if (printReadably || printEscape) {
961                sb.append('"');
962            }
963            return sb.toString();
964        } 
965
966        sb.append("PATHNAME (with no namestring) ");
967        if (host != NIL) {
968            sb.append(":HOST ");
969            sb.append(host.printObject());
970            sb.append(" ");
971        }
972        if (device != NIL) {
973            sb.append(":DEVICE ");
974            sb.append(device.printObject());
975            sb.append(" ");
976        }
977        if (directory != NIL) {
978            sb.append(":DIRECTORY ");
979            sb.append(directory.printObject());
980            sb.append(" ");
981        }
982        if (name != NIL) {
983            sb.append(":NAME ");
984            sb.append(name.printObject());
985            sb.append(" ");
986        }
987        if (type != NIL) {
988            sb.append(":TYPE ");
989            sb.append(type.printObject());
990            sb.append(" ");
991        }
992        if (version != NIL) {
993            sb.append(":VERSION ");
994            sb.append(version.printObject());
995            sb.append(" ");
996        }
997        if (sb.charAt(sb.length() - 1) == ' ') { 
998            sb.setLength(sb.length() - 1);
999        }
1000
1001        return unreadableString(sb.toString());
1002    }
1003    // A logical host is represented as the string that names it.
1004    // (defvar *logical-pathname-translations* (make-hash-table :test 'equal))
1005    public static HashTable LOGICAL_PATHNAME_TRANSLATIONS =
1006      HashTable.newEqualHashTable(64, NIL, NIL);
1007    private static final Symbol _LOGICAL_PATHNAME_TRANSLATIONS_ =
1008      exportSpecial("*LOGICAL-PATHNAME-TRANSLATIONS*", PACKAGE_SYS,
1009      LOGICAL_PATHNAME_TRANSLATIONS);
1010
1011    public static Pathname parseNamestring(String s) {
1012        return new Pathname(s);
1013    }
1014
1015    public static boolean isValidURL(String s) {
1016        // On Windows, the scheme "[A-Z]:.*" is ambiguous; reject as urls
1017        // This special case reduced exceptions while compiling Maxima by 90%+
1018        if (Utilities.isPlatformWindows && s.length() >= 2 && s.charAt(1) == ':') {
1019            char c = s.charAt(0);
1020            if (('A' <= s.charAt(0) && s.charAt(0) <= 'Z')
1021                    || ('a' <= s.charAt(0) && s.charAt(0) <= 'z'))
1022                return false;
1023        }
1024
1025        if (s.indexOf(':') == -1) // no schema separator; can't be valid
1026            return false;
1027       
1028        try {
1029            URL url = new URL(s);
1030        } catch (MalformedURLException e) {
1031            // Generating an exception is a heavy operation,
1032            // we want to try hard not to get into this branch, without
1033            // implementing the URL class ourselves
1034            return false;
1035        }
1036        return true;
1037    }
1038
1039//    public static URL toURL(Pathname p) {
1040 //       try {
1041//            return p.toURL();
1042//        } catch (MalformedURLException e) {
1043//            Debug.assertTrue(false);
1044//            return null; // not reached
1045//        }
1046//    }
1047
1048    URLConnection getURLConnection() {
1049        Debug.assertTrue(isURL());
1050        URL url = this.toURL();
1051        URLConnection result = null;
1052        try {
1053            result = url.openConnection();
1054        } catch (IOException e) {
1055            error(new FileError("Failed to open URL connection.",
1056                                this));
1057        }
1058        return result;
1059    }
1060
1061    public static Pathname parseNamestring(AbstractString namestring) {
1062        // Check for a logical pathname host.
1063        String s = namestring.getStringValue();
1064        if (!isValidURL(s)) {
1065            String h = getHostString(s);
1066            if (h != null && LOGICAL_PATHNAME_TRANSLATIONS.get(new SimpleString(h)) != null) {
1067                // A defined logical pathname host.
1068                return new LogicalPathname(h, s.substring(s.indexOf(':') + 1));
1069            }
1070        }
1071        return new Pathname(s);
1072    }
1073
1074    // XXX was @return Pathname
1075    public static LogicalPathname parseNamestring(AbstractString namestring,
1076                                                  AbstractString host) 
1077    {
1078        String s = namestring.getStringValue();
1079
1080        // Look for a logical pathname host in the namestring.       
1081        String h = getHostString(s);
1082        if (h != null) {
1083            if (!h.equals(host.getStringValue())) {
1084                error(new LispError("Host in " + s
1085                  + " does not match requested host "
1086                  + host.getStringValue()));
1087                // Not reached.
1088                return null;
1089            }
1090            // Remove host prefix from namestring.
1091            s = s.substring(s.indexOf(':') + 1);
1092        }
1093        if (LOGICAL_PATHNAME_TRANSLATIONS.get(host) != null) {
1094            // A defined logical pathname host.
1095            return new LogicalPathname(host.getStringValue(), s);
1096        }
1097        error(new LispError(host.princToString() + " is not defined as a logical pathname host."));
1098        // Not reached.
1099        return null;
1100    }
1101
1102    // "one or more uppercase letters, digits, and hyphens"
1103    protected static String getHostString(String s) {
1104        int colon = s.indexOf(':');
1105        if (colon >= 0) {
1106            return s.substring(0, colon).toUpperCase();
1107        } else {
1108            return null;
1109        }
1110    }
1111
1112    static final void checkCaseArgument(LispObject arg) {
1113        if (arg != Keyword.COMMON && arg != Keyword.LOCAL) {
1114            type_error(arg, list(Symbol.MEMBER, Keyword.COMMON,
1115              Keyword.LOCAL));
1116        }
1117    }
1118
1119    private static final Primitive _PATHNAME_HOST = new pf_pathname_host();
1120    @DocString(name="%pathname-host")
1121    private static class pf_pathname_host extends Primitive {
1122        pf_pathname_host() {
1123            super("%pathname-host", PACKAGE_SYS, false);
1124        }
1125        @Override
1126        public LispObject execute(LispObject first, LispObject second) {
1127            checkCaseArgument(second); // FIXME Why is this ignored?
1128            return coerceToPathname(first).host;
1129        }
1130    }
1131    private static final Primitive _PATHNAME_DEVICE = new pf_pathname_device(); 
1132    @DocString(name="%pathname-device")
1133    private static class pf_pathname_device extends Primitive {
1134        pf_pathname_device() {
1135            super("%pathname-device", PACKAGE_SYS, false);
1136        }
1137        @Override
1138        public LispObject execute(LispObject first, LispObject second) {
1139            checkCaseArgument(second); // FIXME Why is this ignored?
1140            return coerceToPathname(first).device;
1141        }
1142    }
1143    private static final Primitive _PATHNAME_DIRECTORY = new pf_pathname_directory();
1144    @DocString(name="%pathname-directory")
1145    private static class pf_pathname_directory extends Primitive {
1146        pf_pathname_directory() {
1147            super("%pathname-directory", PACKAGE_SYS, false);
1148        }
1149        @Override
1150        public LispObject execute(LispObject first, LispObject second) {
1151            checkCaseArgument(second); // FIXME Why is this ignored?
1152            return coerceToPathname(first).directory;
1153        }
1154    }
1155    private static final Primitive _PATHNAME_NAME = new pf_pathname_name();
1156    @DocString(name="%pathname-name")
1157    private static class  pf_pathname_name extends Primitive {
1158        pf_pathname_name() {
1159            super ("%pathname-name", PACKAGE_SYS, false);
1160        }
1161        @Override
1162        public LispObject execute(LispObject first, LispObject second) {
1163            checkCaseArgument(second); // FIXME Why is this ignored?
1164            return coerceToPathname(first).name;
1165        }
1166    }
1167    private static final Primitive _PATHNAME_TYPE = new pf_pathname_type();
1168    @DocString(name="%pathname-type")
1169    private static class pf_pathname_type extends Primitive {
1170        pf_pathname_type() {
1171            super("%pathname-type", PACKAGE_SYS, false);
1172        }
1173        @Override
1174        public LispObject execute(LispObject first, LispObject second) {
1175            checkCaseArgument(second); // FIXME Why is this ignored?
1176            return coerceToPathname(first).type;
1177        }
1178    }
1179   
1180    private static final Primitive PATHNAME_VERSION = new pf_pathname_version();
1181    @DocString(name="pathname-version",
1182               args="pathname",
1183               returns="version",
1184               doc="Return the version component of PATHNAME.")
1185    private static class pf_pathname_version extends Primitive {
1186        pf_pathname_version() {
1187            super("pathname-version", "pathname");
1188        }
1189        @Override
1190        public LispObject execute(LispObject arg) {
1191            return coerceToPathname(arg).version;
1192        }
1193    }
1194    private static final Primitive NAMESTRING = new pf_namestring();
1195    @DocString(name="namestring",
1196               args="pathname",
1197               returns="namestring",
1198    doc="Returns the NAMESTRING of PATHNAME if it has one.\n"
1199      + "\n"
1200      + "If PATHNAME is of type url-pathname or jar-pathname the NAMESTRING is encoded\n"
1201      + "according to the uri percent escape rules.\n"
1202      + "\n"
1203      + "Signals an error if PATHNAME lacks a printable NAMESTRING representation.\n")
1204    private static class pf_namestring extends Primitive {
1205        pf_namestring() {
1206            super("namestring", "pathname");
1207        }
1208        @Override
1209        public LispObject execute(LispObject arg) {
1210            Pathname pathname = coerceToPathname(arg);
1211            String namestring = pathname.getNamestring();
1212            if (namestring == null) {
1213                error(new SimpleError("Pathname has no namestring: "
1214                                      + pathname.princToString()));
1215            }
1216            return new SimpleString(namestring);
1217        }
1218    }
1219   
1220    private static final Primitive DIRECTORY_NAMESTRING = new pf_directory_namestring();
1221    // TODO clarify uri encoding rules in implementation, then document
1222    @DocString(name="directory-namestring",
1223               args="pathname",
1224               returns="namestring",
1225    doc="Returns the NAMESTRING of directory porition of PATHNAME if it has one.")
1226    private static class pf_directory_namestring extends Primitive {
1227        pf_directory_namestring() {
1228            super("directory-namestring", "pathname");
1229        }
1230        @Override
1231        public LispObject execute(LispObject arg) {
1232            return new SimpleString(coerceToPathname(arg).getDirectoryNamestring());
1233        }
1234    }
1235    private static final Primitive PATHNAME = new pf_pathname();
1236    @DocString(name="pathname",
1237               args="pathspec",
1238               returns="pathname",
1239               doc="Returns the PATHNAME denoted by PATHSPEC.")
1240    private static class pf_pathname extends Primitive {
1241        pf_pathname() {
1242            super("pathname", "pathspec");
1243        }
1244        @Override
1245        public LispObject execute(LispObject arg) {
1246            return coerceToPathname(arg);
1247        }
1248    }
1249    private static final Primitive _PARSE_NAMESTRING = new pf_parse_namestring();
1250    @DocString(name="%parse-namestring",
1251               args="namestring host default-pathname",
1252               returns="pathname, position")
1253    private static class pf_parse_namestring extends Primitive {
1254        pf_parse_namestring() {
1255            super("%parse-namestring", PACKAGE_SYS, false,
1256                  "namestring host default-pathname");
1257        }
1258        @Override
1259        public LispObject execute(LispObject first, LispObject second, LispObject third) {
1260            final LispThread thread = LispThread.currentThread();
1261            final AbstractString namestring = checkString(first);
1262            // The HOST parameter must be a string or NIL.
1263            if (second == NIL) {
1264                // "If HOST is NIL, DEFAULT-PATHNAME is a logical pathname, and
1265                // THING is a syntactically valid logical pathname namestring
1266                // without an explicit host, then it is parsed as a logical
1267                // pathname namestring on the host that is the host component
1268                // of DEFAULT-PATHNAME."
1269                third = coerceToPathname(third);
1270                if (third instanceof LogicalPathname) {
1271                    second = ((LogicalPathname) third).host;
1272                } else {
1273                    return thread.setValues(parseNamestring(namestring),
1274                                            namestring.LENGTH());
1275                }
1276            }
1277            Debug.assertTrue(second != NIL);
1278            final AbstractString host = checkString(second);
1279            return thread.setValues(parseNamestring(namestring, host),
1280                                    namestring.LENGTH());
1281        }
1282    }
1283    private static final Primitive MAKE_PATHNAME = new pf_make_pathname();
1284    @DocString(name="make-pathname",
1285               args="&key host device directory name type version defaults case",
1286               returns="pathname",
1287    doc="Constructs and returns a pathname from the supplied keyword arguments.")
1288    private static class pf_make_pathname extends Primitive {
1289        pf_make_pathname() {
1290            super("make-pathname",
1291                  "&key host device directory name type version defaults case");
1292        }
1293        @Override
1294        public LispObject execute(LispObject[] args) {
1295            return _makePathname(args);
1296        }
1297    }
1298
1299    // Used by the #p reader.
1300    public static final Pathname makePathname(LispObject args) {
1301        return _makePathname(args.copyToArray());
1302    }
1303
1304    public static final Pathname makePathname(File file) {
1305        String namestring = null;
1306        try {
1307            namestring = file.getCanonicalPath();
1308        } catch (IOException e) {
1309            Debug.trace("Failed to make a Pathname from "
1310              + "." + file + "'");
1311            return null;
1312        }
1313        return new Pathname(namestring);
1314    }
1315
1316    static final Pathname _makePathname(LispObject[] args) {
1317        if (args.length % 2 != 0) {
1318            program_error("Odd number of keyword arguments.");
1319        }
1320        LispObject host = NIL;
1321        LispObject device = NIL;
1322        LispObject directory = NIL;
1323        LispObject name = NIL;
1324        LispObject type = NIL;
1325        LispObject version = NIL;
1326        Pathname defaults = null;
1327        boolean deviceSupplied = false;
1328        boolean nameSupplied = false;
1329        boolean typeSupplied = false;
1330        boolean directorySupplied = false;
1331        boolean versionSupplied = false;
1332        for (int i = 0; i < args.length; i += 2) {
1333            LispObject key = args[i];
1334            LispObject value = args[i + 1];
1335            if (key == Keyword.HOST) {
1336                host = value;
1337            } else if (key == Keyword.DEVICE) {
1338                device = value;
1339                deviceSupplied = true;
1340                if (!(value instanceof AbstractString
1341                      || value.equals(Keyword.UNSPECIFIC)
1342                      || value.equals(NIL)
1343                      || value instanceof Cons))
1344                  error(new TypeError("DEVICE is not a string, :UNSPECIFIC, NIL, or a list.", value, NIL));
1345            } else if (key == Keyword.DIRECTORY) {
1346                directorySupplied = true;
1347                if (value instanceof AbstractString) {
1348                    directory = list(Keyword.ABSOLUTE, value);
1349                } else if (value == Keyword.WILD) {
1350                    directory = list(Keyword.ABSOLUTE, Keyword.WILD);
1351                } else {
1352                  // a valid pathname directory is a string, a list of strings, nil, :wild, :unspecific
1353                  // ??? would be nice to (deftype pathname-arg ()
1354                  // '(or (member :wild :unspecific) string (and cons ,(mapcar ...
1355                  // Is this possible?
1356                  if ((value instanceof Cons
1357                       // XXX check that the elements of a list are themselves valid
1358                       || value == Keyword.UNSPECIFIC
1359                       || value.equals(NIL))) {
1360                      directory = value;
1361                  } else {
1362                      error(new TypeError("DIRECTORY argument not a string, list of strings, nil, :WILD, or :UNSPECIFIC.", value, NIL));
1363                  }
1364                }
1365            } else if (key == Keyword.NAME) {
1366                name = value;
1367                nameSupplied = true;
1368            } else if (key == Keyword.TYPE) {
1369                type = value;
1370                typeSupplied = true;
1371            } else if (key == Keyword.VERSION) {
1372                version = value;
1373                versionSupplied = true;
1374            } else if (key == Keyword.DEFAULTS) {
1375                defaults = coerceToPathname(value);
1376            } else if (key == Keyword.CASE) {
1377                // Ignored.
1378            }
1379        }
1380        if (defaults != null) {
1381            if (host == NIL) {
1382                host = defaults.host;
1383            }
1384            if (!directorySupplied) {
1385                directory = defaults.directory;
1386            }
1387            if (!deviceSupplied) {
1388                device = defaults.device;
1389            }
1390            if (!nameSupplied) {
1391                name = defaults.name;
1392            }
1393            if (!typeSupplied) {
1394                type = defaults.type;
1395            }
1396            if (!versionSupplied) {
1397                version = defaults.version;
1398            }
1399        }
1400        final Pathname p;
1401        final boolean logical;
1402        LispObject logicalHost = NIL;
1403        if (host != NIL) {
1404            if (host instanceof AbstractString) {
1405                logicalHost = LogicalPathname.canonicalizeStringComponent((AbstractString) host);
1406            }
1407            if (LOGICAL_PATHNAME_TRANSLATIONS.get(logicalHost) == null) {
1408                // Not a defined logical pathname host -- A UNC path
1409                //warning(new LispError(host.printObject() + " is not defined as a logical pathname host."));
1410                p = new Pathname();
1411                logical = false;
1412                p.host = host;
1413            } else { 
1414                p = new LogicalPathname();
1415                logical = true;
1416                p.host = logicalHost;
1417            }
1418            p.device = Keyword.UNSPECIFIC;
1419        } else {
1420            p = new Pathname();
1421            logical = false;
1422        }
1423        if (device != NIL) {
1424            if (logical) {
1425                // "The device component of a logical pathname is always :UNSPECIFIC."
1426                if (device != Keyword.UNSPECIFIC) {
1427                    error(new LispError("The device component of a logical pathname must be :UNSPECIFIC."));
1428                }
1429            } else {
1430                p.device = device;
1431            }
1432        }
1433        if (directory != NIL) {
1434            if (logical) {
1435                if (directory.listp()) {
1436                    LispObject d = NIL;
1437                    while (directory != NIL) {
1438                        LispObject component = directory.car();
1439                        if (component instanceof AbstractString) {
1440                            d = d.push(LogicalPathname.canonicalizeStringComponent((AbstractString) component));
1441                        } else {
1442                            d = d.push(component);
1443                        }
1444                        directory = directory.cdr();
1445                    }
1446                    p.directory = d.nreverse();
1447                } else if (directory == Keyword.WILD || directory == Keyword.WILD_INFERIORS) {
1448                    p.directory = directory;
1449                } else {
1450                    error(new LispError("Invalid directory component for logical pathname: " + directory.princToString()));
1451                }
1452            } else {
1453                p.directory = directory;
1454            }
1455        }
1456        if (name != NIL) {
1457            if (logical && name instanceof AbstractString) {
1458                p.name = LogicalPathname.canonicalizeStringComponent((AbstractString) name);
1459            } else if (name instanceof AbstractString) {
1460                p.name = validateStringComponent((AbstractString) name);
1461            } else {
1462                p.name = name;
1463            }
1464        }
1465        if (type != NIL) {
1466            if (logical && type instanceof AbstractString) {
1467                p.type = LogicalPathname.canonicalizeStringComponent((AbstractString) type);
1468            } else {
1469                p.type = type;
1470            }
1471        }
1472       
1473        p.version = version;
1474        return p;
1475    }
1476
1477    private static final AbstractString validateStringComponent(AbstractString s) {
1478        final int limit = s.length();
1479        for (int i = 0; i < limit; i++) {
1480            char c = s.charAt(i);
1481            // XXX '\\' should be illegal in all Pathnames at this point?
1482            if (c == '/' || c == '\\' && Utilities.isPlatformWindows) {
1483                error(new LispError("Invalid character #\\" + c
1484                  + " in pathname component \"" + s
1485                  + '"'));
1486                // Not reached.
1487                return null;
1488            }
1489        }
1490        return s;
1491    }
1492
1493    private final boolean validateDirectory(boolean signalError) {
1494        LispObject temp = directory;
1495        while (temp != NIL) {
1496            LispObject first = temp.car();
1497            temp = temp.cdr();
1498            if (first == Keyword.ABSOLUTE || first == Keyword.WILD_INFERIORS) {
1499                LispObject second = temp.car();
1500                if (second == Keyword.UP || second == Keyword.BACK) {
1501                    if (signalError) {
1502                        StringBuilder sb = new StringBuilder();
1503                        sb.append(first.printObject());
1504                        sb.append(" may not be followed immediately by ");
1505                        sb.append(second.printObject());
1506                        sb.append('.');
1507                        error(new FileError(sb.toString(), this));
1508                    }
1509                    return false;
1510                }
1511            }
1512        }
1513        return true;
1514    }
1515    private static final Primitive PATHNAMEP = new pf_pathnamep();
1516    @DocString(name="pathnamep",
1517               args="object",
1518               returns="generalized-boolean",
1519    doc="Returns true if OBJECT is of type pathname; otherwise, returns false.")
1520    private static class pf_pathnamep extends Primitive  {
1521        pf_pathnamep() {
1522            super("pathnamep", "object");
1523        }
1524        @Override
1525        public LispObject execute(LispObject arg) {
1526            return arg instanceof Pathname ? T : NIL;
1527        }
1528    }
1529    private static final Primitive LOGICAL_PATHNAME_P = new pf_logical_pathname_p();
1530    @DocString(name="logical-pathname-p",
1531               args="object",
1532               returns="generalized-boolean",
1533
1534    doc="Returns true if OBJECT is of type logical-pathname; otherwise, returns false.")
1535    private static class pf_logical_pathname_p extends Primitive {
1536        pf_logical_pathname_p() {
1537            super("logical-pathname-p", PACKAGE_SYS, true, "object");
1538        }
1539        @Override
1540        public LispObject execute(LispObject arg) {
1541            return arg instanceof LogicalPathname ? T : NIL;
1542        }
1543    }
1544
1545    private static final Primitive USER_HOMEDIR_PATHNAME = new pf_user_homedir_pathname();
1546    @DocString(name="user-homedir-pathname",
1547               args="&optional host",
1548               returns="pathname",
1549    doc="Determines the pathname that corresponds to the user's home directory.\n"
1550      + "The value returned is obtained from the JVM system propoerty 'user.home'.\n"
1551      + "If HOST is specified, returns NIL.")
1552    private static class pf_user_homedir_pathname extends Primitive {
1553        pf_user_homedir_pathname() {
1554            super("user-homedir-pathname", "&optional host");
1555        }
1556        @Override
1557        public LispObject execute(LispObject[] args) {
1558            switch (args.length) {
1559            case 0: {
1560                String s = System.getProperty("user.home");
1561                if (!s.endsWith(File.separator)) {
1562                    s = s.concat(File.separator);
1563                }
1564                return new Pathname(s);
1565            }
1566            case 1:
1567                return NIL; 
1568            default:
1569                return error(new WrongNumberOfArgumentsException(this, 0, 1));
1570            }
1571        }
1572    }
1573
1574    private static final Primitive LIST_DIRECTORY = new pf_list_directory();
1575    @DocString(name="list-directory",
1576               args="directory &optional (resolve-symlinks nil)",
1577               returns="pathnames",
1578               doc="Lists the contents of DIRECTORY, optionally resolving symbolic links.")
1579    private static class pf_list_directory extends Primitive {
1580        pf_list_directory() {
1581            super("list-directory", PACKAGE_SYS, true, "directory &optional (resolve-symlinks t)");
1582        }
1583        @Override
1584        public LispObject execute(LispObject arg) {
1585            return execute(arg, T);
1586        }
1587        @Override
1588        public LispObject execute(LispObject arg, LispObject resolveSymlinks) {
1589            Pathname pathname = coerceToPathname(arg);
1590            if (pathname instanceof LogicalPathname) {
1591                pathname = LogicalPathname.translateLogicalPathname((LogicalPathname) pathname);
1592            }
1593
1594            LispObject result = NIL;
1595            if (pathname.isJar()) {
1596                String directory = pathname.asEntryPath();
1597                Debug.assertTrue(directory != null);  // We should only be listing directories
1598
1599                if (pathname.device.cdr() instanceof Cons) {
1600                    return error(new FileError("Unimplemented directory listing of JAR within JAR.", pathname));
1601                }
1602
1603                if (directory.length() == 0) {
1604                    directory = "/*";
1605                } else {
1606                    if (directory.endsWith("/")) {
1607                        directory = "/" + directory + "*";
1608                    } else {
1609                        directory = "/" + directory + "/*";
1610                    }
1611                }
1612                SimpleString wildcard = new SimpleString(directory);
1613                SimpleString wildcardDirectory = new SimpleString(directory + "/");
1614
1615                ZipFile jar = ZipCache.get((Pathname)pathname.device.car());
1616                LispObject matches;
1617                for (Enumeration<? extends ZipEntry> entries = jar.entries(); 
1618                     entries.hasMoreElements();) {
1619                    ZipEntry entry = entries.nextElement();
1620                    String entryName = "/" + entry.getName();
1621
1622                    if (entryName.endsWith("/")) {
1623                        matches = Symbol.PATHNAME_MATCH_P
1624                            .execute(new SimpleString(entryName), wildcardDirectory);
1625                    } else {
1626                        matches = Symbol.PATHNAME_MATCH_P.
1627                            execute(new SimpleString(entryName), wildcard);
1628                    }
1629                    if (!matches.equals(NIL)) {
1630                        String namestring = new String(pathname.getNamestring());
1631                        namestring = namestring.substring(0, namestring.lastIndexOf("!/") + 2)
1632                                 + entry.getName();
1633                        Pathname p = new Pathname(namestring);
1634                        result = new Cons(p, result);
1635                    }
1636                }
1637                return result;
1638            }
1639
1640            if (pathname.isURL()) {
1641                return error(new LispError("Unimplemented.")); // XXX
1642            }
1643
1644            String s = pathname.getNamestring();
1645            if (s != null) {
1646                File f = new File(s);
1647                if (f.isDirectory()) {
1648                    try {
1649                        File[] files = f.listFiles();
1650                        for (int i = files.length; i-- > 0;) {
1651                            File file = files[i];
1652                            Pathname p;
1653                            String path;
1654                            if (resolveSymlinks == NIL) {
1655                              path = file.getAbsolutePath();
1656                            } else {
1657                              path = file.getCanonicalPath();
1658                            }
1659                            URI pathURI = (new File(path)).toURI();
1660                            p = new Pathname(pathURI);
1661                            result = new Cons(p, result);
1662                        }
1663                    } catch (IOException e) {
1664                        return error(new FileError("Unable to list directory " 
1665                                                   + pathname.princToString() + ".",
1666                                                   pathname));
1667                    } catch (SecurityException e) {
1668                        Debug.trace(e);
1669                    } catch (NullPointerException e) {
1670                        Debug.trace(e);
1671                    }
1672                }
1673            }
1674            return result;
1675        }
1676    }
1677
1678    @DocString(name="match-wild-jar-pathname",
1679               args="wild-jar-pathname",
1680               returns="pathnames",
1681    doc="Returns the pathnames matching WILD-JAR-PATHNAME which is both wild and a jar-pathname.")
1682    static final Primitive MATCH_WILD_JAR_PATHNAME = new pf_match_wild_jar_pathname();
1683    private static class pf_match_wild_jar_pathname extends Primitive {
1684        pf_match_wild_jar_pathname() {
1685            super("match-wild-jar-pathname", PACKAGE_SYS, false, "wild-jar-pathname");
1686        }
1687        @Override
1688        public LispObject execute(LispObject arg) {
1689            Pathname pathname = coerceToPathname(arg);
1690            if (pathname instanceof LogicalPathname) {
1691                pathname = LogicalPathname.translateLogicalPathname((LogicalPathname) pathname);
1692            }
1693            if (!pathname.isJar()) {
1694                return new FileError("Not a jar pathname.", pathname);
1695            }
1696            if (!pathname.isWild()) {
1697                return new FileError("Not a wild pathname.", pathname);
1698            }
1699            Pathname jarPathname = new Pathname(pathname);
1700            jarPathname.directory = NIL;
1701            jarPathname.name = NIL;
1702            jarPathname.type = NIL;
1703            jarPathname.invalidateNamestring();
1704            LispObject jarTruename = truename(jarPathname, false); 
1705           
1706            // We can't match anything in a non-existent jar
1707            if (jarTruename == NIL) {
1708                return NIL;
1709            }
1710           
1711            LispObject result = NIL;
1712            String wild = "/" + pathname.asEntryPath();
1713            final SimpleString wildcard = new SimpleString(wild);
1714
1715            if (pathname.device.cdr() instanceof Cons) {
1716                ZipFile outerJar = ZipCache.get((Pathname)pathname.device.car());
1717                String entryPath = ((Pathname)pathname.device.cdr().car()).getNamestring(); //???
1718                if (entryPath.startsWith("/")) {
1719                    entryPath = entryPath.substring(1);
1720                }
1721                ZipEntry entry = outerJar.getEntry(entryPath);
1722                InputStream inputStream = null;
1723                try {
1724                    inputStream = outerJar.getInputStream(entry);
1725                } catch (IOException e) {
1726                    return new FileError("Failed to read zip input stream inside zip.",
1727                                         pathname);
1728                }
1729                ZipInputStream zipInputStream
1730                    = new ZipInputStream(inputStream);
1731
1732                try {
1733                    while ((entry = zipInputStream.getNextEntry()) != null) {
1734                        String entryName = "/" + entry.getName();
1735                        LispObject matches = Symbol.PATHNAME_MATCH_P
1736                            .execute(new SimpleString(entryName), wildcard);
1737                   
1738                        if (!matches.equals(NIL)) {
1739                            String namestring = new String(pathname.getNamestring());
1740                            namestring = namestring.substring(0, namestring.lastIndexOf("!/") + 2)
1741                                + entry.getName();
1742                            Pathname p = new Pathname(namestring);
1743                            result = new Cons(p, result);
1744                        }
1745                    }
1746                } catch (IOException e) {
1747                    return new FileError("Failed to seek through zip inputstream inside zip.",
1748                                         pathname);
1749                }
1750            } else {
1751                ZipFile jar = ZipCache.get((Pathname)pathname.device.car());
1752                for (Enumeration<? extends ZipEntry> entries = jar.entries(); 
1753                     entries.hasMoreElements();) 
1754                    {
1755                        ZipEntry entry = entries.nextElement();
1756                        String entryName = "/" + entry.getName();
1757                        LispObject matches = Symbol.PATHNAME_MATCH_P
1758                            .execute(new SimpleString(entryName), wildcard);
1759
1760                        if (!matches.equals(NIL)) {
1761                            String namestring = new String(pathname.getNamestring());
1762                            namestring = namestring.substring(0, namestring.lastIndexOf("!/") + 2)
1763                                + entry.getName();
1764                            Pathname p = new Pathname(namestring);
1765                            result = new Cons(p, result);
1766                        }
1767                    }
1768            }
1769            return result;
1770        }
1771    }
1772
1773    public boolean isAbsolute()  {
1774        if (!directory.equals(NIL) || !(directory == null)) {
1775            if (directory instanceof Cons) {
1776                if (((Cons)directory).car().equals(Keyword.ABSOLUTE)) {
1777                    return true;
1778                }
1779            }
1780        }
1781        return false;
1782    }
1783
1784    @DocString(name="pathname-jar-p",
1785               args="pathname",
1786               returns="generalized-boolean",
1787    doc="Predicate functionfor whether PATHNAME references a jar.")
1788    private static final Primitive PATHNAME_JAR_P = new pf_pathname_jar_p();
1789    private static class pf_pathname_jar_p extends Primitive {
1790        pf_pathname_jar_p() {
1791            super("pathname-jar-p", PACKAGE_EXT, true);
1792        }
1793        @Override
1794        public LispObject execute(LispObject arg) {
1795            Pathname p = coerceToPathname(arg);
1796            return p.isJar() ? T : NIL;
1797        }
1798    }
1799
1800    public boolean isJar() {
1801        return (device instanceof Cons);
1802    }
1803
1804    @DocString(name="pathname-url-p",
1805               args="pathname",
1806               returns="generalized-boolean",
1807    doc="Predicate function for whether PATHNAME references a jaurl.")
1808    private static final Primitive PATHNAME_URL_P = new pf_pathname_url_p();
1809    private static class pf_pathname_url_p extends Primitive {
1810        pf_pathname_url_p() {
1811            super("pathname-url-p", PACKAGE_EXT, true, "pathname",
1812                  "Predicate for whether PATHNAME references a URL.");
1813        }
1814        @Override
1815        public LispObject execute(LispObject arg) {
1816            Pathname p = coerceToPathname(arg);
1817            return p.isURL() ? T : NIL;
1818        }
1819    }
1820
1821    public boolean isURL() {
1822        return (host instanceof Cons);
1823    }
1824
1825    public boolean isWild() {
1826        if (host == Keyword.WILD || host == Keyword.WILD_INFERIORS) {
1827            return true;
1828        }
1829        if (device == Keyword.WILD || device == Keyword.WILD_INFERIORS) {
1830            return true;
1831        }
1832        if (directory instanceof Cons) {
1833            if (memq(Keyword.WILD, directory)) {
1834                return true;
1835            }
1836            if (memq(Keyword.WILD_INFERIORS, directory)) {
1837                return true;
1838            }
1839            Cons d = (Cons) directory;
1840            while (true) {
1841                if (d.car() instanceof AbstractString) {
1842                    String s = d.car().printObject();
1843                    if (s.contains("*")) {
1844                        return true;
1845                    }
1846                }
1847                if (d.cdr() == NIL || ! (d.cdr() instanceof Cons)) {
1848                    break;
1849                }
1850                d = (Cons)d.cdr();
1851            }
1852        }
1853        if (name == Keyword.WILD || name == Keyword.WILD_INFERIORS) {
1854            return true;
1855        }
1856        if (name instanceof AbstractString) {
1857            if (name.printObject().contains("*")) {
1858                return true;
1859            }
1860        }
1861        if (type == Keyword.WILD || type == Keyword.WILD_INFERIORS) {
1862            return true;
1863        }
1864        if (type instanceof AbstractString) {
1865            if (type.printObject().contains("*")) {
1866                return true;
1867            }
1868        }
1869        if (version == Keyword.WILD || version == Keyword.WILD_INFERIORS) {
1870            return true;
1871        }
1872        return false;
1873    }
1874
1875    private static final Primitive _WILD_PATHNAME_P = new pf_wild_pathname_p();
1876    @DocString(name="%wild-pathname-p",
1877               args="pathname keyword",
1878               returns="generalized-boolean",
1879    doc="Predicate for determing whether PATHNAME contains wild components.\n"
1880      + "KEYWORD, if non-nil, should be one of :directory, :host, :device,\n"
1881      + ":name, :type, or :version indicating that only the specified component\n"
1882      + "should be checked for wildness.")
1883    static final class pf_wild_pathname_p extends Primitive {
1884        pf_wild_pathname_p() {
1885            super("%wild-pathname-p", PACKAGE_SYS, true);
1886        }
1887        @Override
1888        public LispObject execute(LispObject first, LispObject second) {
1889            Pathname pathname = coerceToPathname(first);
1890            if (second == NIL) {
1891                return pathname.isWild() ? T : NIL;
1892            }
1893            if (second == Keyword.DIRECTORY) {
1894                if (pathname.directory instanceof Cons) {
1895                    if (memq(Keyword.WILD, pathname.directory)) {
1896                        return T;
1897                    }
1898                    if (memq(Keyword.WILD_INFERIORS, pathname.directory)) {
1899                        return T;
1900                    }
1901                }
1902                return NIL;
1903            }
1904            LispObject value;
1905            if (second == Keyword.HOST) {
1906                value = pathname.host;
1907            } else if (second == Keyword.DEVICE) {
1908                value = pathname.device;
1909            } else if (second == Keyword.NAME) {
1910                value = pathname.name;
1911            } else if (second == Keyword.TYPE) {
1912                value = pathname.type;
1913            } else if (second == Keyword.VERSION) {
1914                value = pathname.version;
1915            } else {
1916                return program_error("Unrecognized keyword "
1917                                     + second.princToString() + ".");
1918            }
1919            if (value == Keyword.WILD || value == Keyword.WILD_INFERIORS) {
1920                return T;
1921            } else {
1922                return NIL;
1923            }
1924        }
1925    }
1926
1927    static final Primitive MERGE_PATHNAMES = new pf_merge_pathnames();
1928    @DocString(name="merge-pathnames",
1929               args="pathname &optional default-pathname default-version",
1930               returns="pathname",
1931    doc="Constructs a pathname from PATHNAME by filling in any unsupplied components\n"
1932     +  "with the corresponding values from DEFAULT-PATHNAME and DEFAULT-VERSION.")
1933    static final class pf_merge_pathnames extends Primitive {
1934        pf_merge_pathnames() {
1935            super("merge-pathnames", "pathname &optional default-pathname default-version");
1936        }
1937        @Override
1938        public LispObject execute(LispObject arg) {
1939            Pathname pathname = coerceToPathname(arg);
1940            Pathname defaultPathname =
1941                coerceToPathname(Symbol.DEFAULT_PATHNAME_DEFAULTS.symbolValue());
1942            LispObject defaultVersion = Keyword.NEWEST;
1943            return mergePathnames(pathname, defaultPathname, defaultVersion);
1944        }
1945        @Override
1946        public LispObject execute(LispObject first, LispObject second) {
1947            Pathname pathname = coerceToPathname(first);
1948            Pathname defaultPathname =
1949                coerceToPathname(second);
1950            LispObject defaultVersion = Keyword.NEWEST;
1951            return mergePathnames(pathname, defaultPathname, defaultVersion);
1952        }
1953        @Override
1954        public LispObject execute(LispObject first, LispObject second,
1955                                  LispObject third) {
1956            Pathname pathname = coerceToPathname(first);
1957            Pathname defaultPathname =
1958                coerceToPathname(second);
1959            LispObject defaultVersion = third;
1960            return mergePathnames(pathname, defaultPathname, defaultVersion);
1961        }
1962    }
1963
1964    public static final Pathname mergePathnames(Pathname pathname, Pathname defaultPathname) {
1965        return mergePathnames(pathname, defaultPathname, Keyword.NEWEST);
1966    }
1967   
1968    public static final Pathname mergePathnames(final Pathname pathname,
1969                                                final Pathname defaultPathname,
1970                                                final LispObject defaultVersion)
1971    {
1972        Pathname result;
1973        Pathname p = new Pathname(pathname);
1974        Pathname d;
1975
1976        if (pathname instanceof LogicalPathname) {
1977            result = new LogicalPathname();
1978            d = new Pathname(defaultPathname);
1979        } else {
1980            result = new Pathname();
1981            if (defaultPathname instanceof LogicalPathname) {
1982                d = LogicalPathname.translateLogicalPathname((LogicalPathname) defaultPathname);
1983            } else {
1984                d = new Pathname(defaultPathname);
1985            }
1986        }
1987        if (pathname.host != NIL) {
1988            result.host = p.host;
1989        } else {
1990            result.host = d.host;
1991        }
1992
1993        if (pathname.device != NIL) { // XXX if device represent JARs we want to merge
1994            result.device = p.device;
1995        } else {
1996            if (!p.isURL()) {
1997                // If the defaults contain a JAR-PATHNAME, and the
1998                // pathname to be be merged does not have a specified
1999                // DEVICE, a specified HOST, and doesn't contain a
2000                // relative DIRECTORY, then on non-MSDOG, set its
2001                // device to :UNSPECIFIC.
2002                if (pathname.host == NIL
2003                    && pathname.device == NIL
2004                    && d.isJar()
2005                    && !Utilities.isPlatformWindows) {
2006                    if (pathname.directory != NIL
2007                        && pathname.directory.car() == Keyword.ABSOLUTE) {
2008                        result.device = Keyword.UNSPECIFIC;
2009                    } else {
2010                        result.device = d.device;
2011                    }
2012                } else {
2013                    result.device = d.device;
2014                }
2015            }
2016        }
2017
2018        if (pathname.isJar()) {
2019            Cons jars = (Cons)result.device;
2020            LispObject jar = jars.car;
2021            if (jar instanceof Pathname) {
2022                Pathname defaults = new Pathname(d);
2023                if (defaults.isJar()) {
2024                    defaults.device = NIL;
2025                }
2026                Pathname o = mergePathnames((Pathname)jar, defaults);
2027                if (o.directory instanceof Cons
2028                    && ((Cons)o.directory).length() == 1) { // i.e. (:ABSOLUTE) or (:RELATIVE)
2029                    o.directory = NIL;
2030                }
2031                ((Cons)result.device).car = o;
2032            }
2033            result.directory = p.directory;
2034        } else {
2035            result.directory = mergeDirectories(p.directory, d.directory);
2036        }
2037
2038        if (pathname.name != NIL) {
2039            result.name = p.name;
2040        } else {
2041            result.name = d.name;
2042        }
2043        if (pathname.type != NIL) {
2044            result.type = p.type;
2045        } else {
2046            result.type = d.type;
2047        }
2048        //  CLtLv2 MERGE-PATHNAMES
2049   
2050    // "[T]he missing components in the given pathname are filled
2051    // in from the defaults pathname, except that if no version is
2052    // specified the default version is used."
2053
2054    // "The merging rules for the version are more complicated and
2055    // depend on whether the pathname specifies a name. If the
2056    // pathname doesn't specify a name, then the version, if not
2057    // provided, will come from the defaults, just like the other
2058    // components. However, if the pathname does specify a name,
2059    // then the version is not affected by the defaults. The
2060    // reason is that the version ``belongs to'' some other file
2061    // name and is unlikely to have anything to do with the new
2062    // one. Finally, if this process leaves the
2063    // version missing, the default version is used."
2064
2065        if (p.version != NIL) {
2066            result.version = p.version;
2067        } else if (p.name == NIL) {
2068            if (defaultPathname.version == NIL) {
2069                result.version = defaultVersion;
2070            } else {
2071                result.version = defaultPathname.version;
2072            }
2073        } else if (defaultVersion == NIL) {
2074            result.version = p.version;
2075        } 
2076        if (result.version == NIL) {
2077            result.version = defaultVersion;
2078        }
2079
2080        if (pathname instanceof LogicalPathname) {
2081            // When we're returning a logical
2082            result.device = Keyword.UNSPECIFIC;
2083            if (result.directory.listp()) {
2084                LispObject original = result.directory;
2085                LispObject canonical = NIL;
2086                while (original != NIL) {
2087                    LispObject component = original.car();
2088                    if (component instanceof AbstractString) {
2089                        component = LogicalPathname.canonicalizeStringComponent((AbstractString) component);
2090                    }
2091                    canonical = canonical.push(component);
2092                    original = original.cdr();
2093                }
2094                result.directory = canonical.nreverse();
2095            }
2096            if (result.name instanceof AbstractString) {
2097                result.name = LogicalPathname.canonicalizeStringComponent((AbstractString) result.name);
2098            }
2099            if (result.type instanceof AbstractString) {
2100                result.type = LogicalPathname.canonicalizeStringComponent((AbstractString) result.type);
2101            }
2102        }
2103        result.invalidateNamestring();
2104        return result;
2105    }
2106
2107    private static final LispObject mergeDirectories(LispObject dir,
2108                                                     LispObject defaultDir) {
2109        if (dir == NIL) {
2110            return defaultDir;
2111        }
2112        if (dir.car() == Keyword.RELATIVE && defaultDir != NIL) {
2113            LispObject result = NIL;
2114            while (defaultDir != NIL) {
2115                result = new Cons(defaultDir.car(), result);
2116                defaultDir = defaultDir.cdr();
2117            }
2118            dir = dir.cdr(); // Skip :RELATIVE.
2119            while (dir != NIL) {
2120                result = new Cons(dir.car(), result);
2121                dir = dir.cdr();
2122            }
2123            LispObject[] array = result.copyToArray();
2124            for (int i = 0; i < array.length - 1; i++) {
2125                if (array[i] == Keyword.BACK) {
2126                    if (array[i + 1] instanceof AbstractString || array[i + 1] == Keyword.WILD) {
2127                        array[i] = null;
2128                        array[i + 1] = null;
2129                    }
2130                }
2131            }
2132            result = NIL;
2133            for (int i = 0; i < array.length; i++) {
2134                if (array[i] != null) {
2135                    result = new Cons(array[i], result);
2136                }
2137            }
2138            return result;
2139        }
2140        return dir;
2141    }
2142
2143
2144    public static final LispObject truename(Pathname pathname) {
2145        return truename(pathname, false);
2146    }
2147
2148    public static final LispObject truename(LispObject arg) {
2149        return truename(arg, false);
2150    }
2151
2152    public static final LispObject truename(LispObject arg, boolean errorIfDoesNotExist) {
2153        final Pathname pathname = coerceToPathname(arg);
2154        return truename(pathname, errorIfDoesNotExist);
2155    }
2156
2157    /** @return The canonical TRUENAME as a Pathname if the pathname
2158     * exists, otherwise returns NIL or possibly a subtype of
2159     * LispError if there are logical problems with the input.
2160     */
2161    public static final LispObject truename(Pathname pathname,
2162                                            boolean errorIfDoesNotExist) 
2163    {
2164        if (pathname == null || pathname.equals(NIL)) { 
2165           return doTruenameExit(pathname, errorIfDoesNotExist); 
2166        }
2167        if (pathname instanceof LogicalPathname) {
2168            pathname = LogicalPathname.translateLogicalPathname((LogicalPathname) pathname);
2169        }
2170        if (pathname.isWild()) {
2171            return error(new FileError("Bad place for a wild pathname.",
2172                                       pathname));
2173        }
2174        if (!(pathname.isJar() || pathname.isURL())) {
2175            Pathname result
2176                = mergePathnames(pathname,
2177                                 coerceToPathname(Symbol.DEFAULT_PATHNAME_DEFAULTS.symbolValue()),
2178                                 NIL);
2179            final File file = result.getFile();
2180            if (file.exists()) {
2181                if (file.isDirectory()) {
2182                    result = Pathname.getDirectoryPathname(file);
2183                } else {
2184                    try {
2185                        result = new Pathname(file.getCanonicalPath());
2186                    } catch (IOException e) {
2187                        return error(new FileError(e.getMessage(), pathname));
2188                    }
2189                }
2190                if (Utilities.isPlatformUnix) {
2191                  result.device = Keyword.UNSPECIFIC;
2192                }
2193                return result;
2194            }
2195        } else if (pathname.isURL()) {
2196            if (pathname.getInputStream() != null) {
2197              // If there is no type, query or fragment, we check to
2198              // see if there is URL available "underneath".
2199              if (pathname.name != NIL
2200                  && pathname.type == NIL
2201                  && Symbol.GETF.execute(pathname.host, QUERY, NIL) == NIL
2202                  && Symbol.GETF.execute(pathname.host, FRAGMENT, NIL) == NIL) {
2203                Pathname p = new Pathname(pathname.getNamestring() + "/");
2204                if (p.getInputStream() != null) {
2205                  return p;
2206                }
2207              }
2208              return pathname;
2209            }
2210        } else
2211        jarfile: {
2212            // Possibly canonicalize jar file directory
2213            Cons jars = (Cons) pathname.device;
2214            LispObject o = jars.car();
2215        if (!(o instanceof Pathname)) {
2216           return doTruenameExit(pathname, errorIfDoesNotExist);
2217        }
2218            if (o instanceof Pathname
2219                && !(((Pathname)o).isURL())
2220                // XXX Silently fail to call truename() if the default
2221                // pathname defaults exist within a jar, as that will
2222                // (probably) not succeed.  The better solution would
2223                // probably be to parametize the value of
2224                // *DEFAULT-PATHNAME-DEFAULTS* on invocations of
2225                // truename().
2226                && !coerceToPathname(Symbol.DEFAULT_PATHNAME_DEFAULTS.symbolValue()).isJar()) 
2227                {
2228                LispObject truename = Pathname.truename((Pathname)o, errorIfDoesNotExist);
2229                if (truename != null && truename != NIL
2230                    && truename instanceof Pathname) {
2231                    Pathname truePathname = (Pathname)truename;
2232                    // A jar that is a directory makes no sense, so exit
2233                    if (truePathname.getNamestring().endsWith("/")) {
2234                        break jarfile;
2235                    }
2236                    jars.car = truePathname;
2237                } else {
2238                    break jarfile;
2239                }
2240            }
2241
2242            // Check for existence of a JAR file and/or JarEntry
2243            //
2244            // Cases:
2245            // 1.  JAR
2246            // 2.  JAR in JAR
2247            // 3.  JAR with Entry
2248            // 4.  JAR in JAR with Entry
2249
2250            ZipFile jarFile = ZipCache.get((Pathname)jars.car());
2251            String entryPath = pathname.asEntryPath();
2252            if (jarFile != null) {
2253                if (jars.cdr() instanceof Cons) {
2254                  Pathname inner = (Pathname) jars.cdr().car();
2255                  InputStream inputStream = Utilities.getInputStream(jarFile, inner);
2256                  if (inputStream != null) {
2257                      if (entryPath.length() == 0) {
2258                          return pathname; // Case 2
2259                      } else {
2260                          ZipInputStream zipInputStream
2261                              = new ZipInputStream(inputStream);
2262                          ZipEntry entry = Utilities.getEntry(zipInputStream,
2263                                                              entryPath,
2264                                                              false);
2265                          if (entry != null) {
2266                              // XXX this could possibly be a directory?
2267                              return pathname; // Case 4
2268                         }
2269                      }
2270                  }
2271                } else {
2272                    if (entryPath.length() == 0) {
2273                        return pathname; // Case 1
2274                    } else {
2275                        ZipEntry entry = jarFile.getEntry(entryPath);
2276                        if (entry != null) {
2277                            // ensure this isn't a directory
2278                            if (entry.isDirectory()) {
2279                                break jarfile;
2280                            }
2281                            try {
2282                                InputStream input = jarFile.getInputStream(entry);
2283                                if (input != null) {
2284                                    return pathname; // Case 3
2285                                }
2286                            } catch (IOException e) {
2287                                break jarfile;
2288                            }
2289                        }
2290                    }
2291                }
2292            }
2293        }
2294        error:
2295      return doTruenameExit(pathname, errorIfDoesNotExist);
2296    }
2297   
2298    static private LispObject doTruenameExit(Pathname pathname, boolean errorIfDoesNotExist) {
2299        if (errorIfDoesNotExist) {
2300            StringBuilder sb = new StringBuilder("The file ");
2301            sb.append(pathname.princToString());
2302            sb.append(" does not exist.");
2303            return error(new FileError(sb.toString(), pathname));
2304        }
2305        return NIL;
2306    }
2307
2308
2309    protected static URL makeURL(Pathname pathname) {
2310        URL result = null;
2311        try {
2312            if (pathname.isURL()) {
2313                result = new URL(pathname.getNamestring());
2314            } else {
2315                // XXX Properly encode Windows drive letters and UNC paths
2316                // XXX ensure that we have cannonical path?
2317                result = new URL("file://" + pathname.getNamestring());
2318            }
2319        } catch (MalformedURLException e) {
2320            Debug.trace("Could not form URL from " + pathname);
2321        }
2322        return result;
2323    }
2324
2325  public static final Primitive GET_INPUT_STREAM = new pf_get_input_stream();
2326  @DocString(name="get-input-stream",
2327             args="pathname",
2328             doc="Returns a java.io.InputStream for resource denoted by PATHNAME.")
2329  private static final class pf_get_input_stream extends Primitive {
2330    pf_get_input_stream() {
2331      super("ensure-input-stream", PACKAGE_SYS, true);
2332    }
2333    @Override
2334    public LispObject execute(LispObject pathname) {
2335      Pathname p = (Pathname) coerceToPathname(pathname);
2336      return new JavaObject(p.getInputStream());
2337    }
2338  };
2339
2340
2341    public InputStream getInputStream() {
2342        InputStream result = null;
2343        if (isJar()) {
2344            String entryPath = asEntryPath();
2345            // XXX We only return the bytes of an entry in a JAR
2346            Debug.assertTrue(entryPath != null);
2347            ZipFile jarFile = ZipCache.get((Pathname)device.car());
2348            Debug.assertTrue(jarFile != null);
2349            // Is this a JAR within a JAR?
2350            if (device.cdr() instanceof Cons) {
2351                Pathname inner = (Pathname) device.cdr().car();
2352                InputStream input = Utilities.getInputStream(jarFile, inner);
2353                ZipInputStream zipInputStream = new ZipInputStream(input);
2354                result =  Utilities.getEntryAsInputStream(zipInputStream, entryPath);
2355            } else {
2356                ZipEntry entry = jarFile.getEntry(entryPath);
2357                if (entry == null) {
2358                    Debug.trace("Failed to get InputStream for "   
2359                                + "'" + getNamestring() + "'");
2360                    // XXX should this be fatal?
2361                    Debug.assertTrue(false);
2362                }
2363                try {
2364                    result = jarFile.getInputStream(entry);
2365                } catch (IOException e) {
2366                    Debug.warn("Failed to get InputStream from "
2367                                + "'" + getNamestring() + "'"
2368                                + ": " + e);
2369                }
2370            }
2371        } else if (isURL()) {
2372            URL url = this.toURL();
2373            try { 
2374                result = url.openStream();
2375            } catch (IOException e) {
2376                Debug.warn("Failed to get InputStream from "
2377                            + "'" + getNamestring() + "'"
2378                            + ": " + e);
2379            }
2380        } else {
2381            File file = getFile();
2382            try { 
2383                result = new FileInputStream(file);
2384            } catch (IOException e) {
2385                Debug.warn("Failed to get InputStream from "
2386                            + "'" + getNamestring() + "'"
2387                            + ": " + e);
2388            }
2389        }
2390        return result;
2391    }
2392
2393    /** @return Time in milliseconds since the UNIX epoch at which the
2394     * resource was last modified, or 0 if the time is unknown.
2395     */
2396    public long getLastModified() {
2397        if (!(isJar() || isURL())) {
2398            File f = getFile();
2399            return f.lastModified();
2400        }
2401
2402        if (isJar()) {
2403            // JAR cases
2404            // 0.  JAR from URL
2405            // 1.  JAR
2406            // 2.  JAR in JAR
2407            // 3.  Entry in JAR
2408            // 4.  Entry in JAR in JAR
2409            String entryPath = asEntryPath();
2410            Cons d = (Cons)device;
2411            if (d.cdr().equals(NIL)) {
2412                if (entryPath.length() == 0) {
2413                    LispObject o = d.car();
2414                        // 0. JAR from URL
2415                        // 1. JAR
2416                    return ((Pathname)o).getLastModified();
2417                } else {
2418                    // 3. Entry in JAR
2419                    final ZipEntry entry
2420                        = ZipCache.get((Pathname)device.car()).getEntry(entryPath);
2421                    if (entry == null) {
2422                        return 0;
2423                    }
2424                    final long time = entry.getTime();
2425                    if (time == -1) {
2426                        return 0;
2427                    }
2428                    return time;
2429                }
2430            } else {
2431                ZipFile outerJar = ZipCache.get((Pathname)d.car());
2432                if (entryPath.length() == 0) {
2433                    // 4.  JAR in JAR
2434                    String jarPath = ((Pathname)d.cdr()).asEntryPath();
2435                    final ZipEntry entry = outerJar.getEntry(jarPath);
2436                    final long time = entry.getTime();
2437                    if (time == -1) {
2438                        return 0;
2439                    }
2440                    return time;
2441                } else {
2442                    // 5. Entry in JAR in JAR
2443                    String innerJarPath = ((Pathname)d.cdr()).asEntryPath();
2444                    ZipEntry entry = outerJar.getEntry(entryPath);
2445                    ZipInputStream innerJarInputStream
2446                        = Utilities.getZipInputStream(outerJar, innerJarPath);
2447                    ZipEntry innerEntry = Utilities.getEntry(innerJarInputStream,
2448                                                             entryPath);
2449                    long time = innerEntry.getTime();
2450                    if (time == -1) {
2451                        return 0;
2452                    }
2453                    return time;
2454                }
2455            }
2456        }
2457        if (isURL()) {
2458            return getURLConnection().getLastModified();
2459        }
2460        return 0;
2461    }
2462
2463    private static final Primitive MKDIR = new pf_mkdir();
2464    @DocString(name="mkdir",
2465               args="pathname",
2466               returns="generalized-boolean",
2467    doc="Attempts to create directory at PATHNAME returning the success or failure.")
2468    private static class pf_mkdir extends Primitive {
2469        pf_mkdir() {
2470            super("mkdir", PACKAGE_SYS, false, "pathname");
2471        }
2472
2473        @Override
2474        public LispObject execute(LispObject arg) {
2475            final Pathname pathname = coerceToPathname(arg);
2476            if (pathname.isWild()) {
2477                error(new FileError("Bad place for a wild pathname.", pathname));
2478            }
2479            Pathname defaultedPathname =
2480                mergePathnames(pathname,
2481                               coerceToPathname(Symbol.DEFAULT_PATHNAME_DEFAULTS.symbolValue()),
2482                               NIL);
2483            if (defaultedPathname.isURL() || defaultedPathname.isJar()) {
2484                return new FileError("Cannot mkdir with a " 
2485                                     + (defaultedPathname.isURL() ? "URL" : "jar")
2486                                     + " Pathname.",
2487                                     defaultedPathname);
2488            }
2489                   
2490            File file = defaultedPathname.getFile();
2491            return file.mkdir() ? T : NIL;
2492        }
2493    }
2494
2495    private static final Primitive RENAME_FILE = new pf_rename_file();
2496    @DocString(name="rename-file",
2497               args="filespec new-name",
2498               returns="defaulted-new-name, old-truename, new-truename",
2499               doc = "Modifies the file system in such a way that the file indicated by FILESPEC is renamed to DEFAULTED-NEW-NAME.\n"
2500               + "\n"
2501               + "Returns three values if successful. The primary value, DEFAULTED-NEW-NAME, is \n"
2502               + "the resulting name which is composed of NEW-NAME with any missing components filled in by \n"
2503               + "performing a merge-pathnames operation using filespec as the defaults. The secondary \n" 
2504               + "value, OLD-TRUENAME, is the truename of the file before it was renamed. The tertiary \n"
2505               + "value, NEW-TRUENAME, is the truename of the file after it was renamed.\n")
2506    private static class pf_rename_file extends Primitive {
2507        pf_rename_file() {
2508            super("rename-file", "filespec new-name");
2509        }
2510        @Override
2511        public LispObject execute(LispObject first, LispObject second) {
2512            Pathname oldPathname = coerceToPathname(first);
2513            Pathname oldTruename = (Pathname) truename(oldPathname, true);
2514            Pathname newName = coerceToPathname(second);
2515            if (newName.isWild()) {
2516                error(new FileError("Bad place for a wild pathname.", newName));
2517            }
2518            if (oldTruename.isJar()) {
2519                error(new FileError("Bad place for a jar pathname.", oldTruename));
2520            }
2521            if (newName.isJar()) {
2522                error(new FileError("Bad place for a jar pathname.", newName));
2523            }
2524            if (oldTruename.isURL()) {
2525                error(new FileError("Bad place for a URL pathname.", oldTruename));
2526            }
2527            if (newName.isURL()) {
2528                error(new FileError("Bad place for a jar pathname.", newName));
2529            }
2530               
2531            Pathname defaultedNewName = mergePathnames(newName, oldTruename, NIL);
2532
2533            File source = oldTruename.getFile();
2534            File destination = null;
2535            if (defaultedNewName instanceof LogicalPathname) {
2536                destination = LogicalPathname.translateLogicalPathname((LogicalPathname)defaultedNewName)
2537                    .getFile();
2538            } else {
2539                destination = defaultedNewName.getFile();
2540            }
2541            // By default, MSDOG doesn't allow one to remove files that are open.
2542            if (Utilities.isPlatformWindows) {
2543              if (destination.isFile()) {
2544                ZipCache.remove(destination);
2545                destination.delete();
2546              }
2547            }
2548            if (source.renameTo(destination)) { // Success!
2549              Pathname newTruename = (Pathname)truename(defaultedNewName, true);
2550              return LispThread.currentThread().setValues(defaultedNewName, 
2551                                                          oldTruename,
2552                                                          newTruename);
2553            }
2554            return error(new FileError("Unable to rename "
2555                                       + oldTruename.princToString()
2556                                       + " to " + newName.princToString()
2557                                       + ".",
2558                                       oldTruename));
2559        }
2560    }
2561   
2562    // TODO clarify uri encoding cases in implementation and document
2563    private static final Primitive FILE_NAMESTRING = new pf_file_namestring();
2564    @DocString(name="file-namestring",
2565               args="pathname",
2566               returns="namestring",
2567    doc="Returns just the name, type, and version components of PATHNAME.")
2568    private static class pf_file_namestring extends Primitive {
2569        pf_file_namestring() {
2570            super("file-namestring", "pathname");
2571        }
2572        @Override
2573        public LispObject execute(LispObject arg) {
2574            Pathname p = coerceToPathname(arg);
2575            StringBuilder sb = new StringBuilder();
2576            if (p.name instanceof AbstractString) {
2577                sb.append(p.name.getStringValue());
2578            } else if (p.name == Keyword.WILD) {
2579                sb.append('*');
2580            } else {
2581                return NIL;
2582            }
2583            if (p.type instanceof AbstractString) {
2584                sb.append('.');
2585                sb.append(p.type.getStringValue());
2586            } else if (p.type == Keyword.WILD) {
2587                sb.append(".*");
2588            }
2589            return new SimpleString(sb);
2590        }
2591    }
2592
2593    private static final Primitive HOST_NAMESTRING = new pf_host_namestring();
2594    @DocString(name="host-namestring",
2595               args="pathname",
2596               returns="namestring",
2597    doc="Returns the host name of PATHNAME.")
2598    private static class pf_host_namestring extends Primitive {
2599        pf_host_namestring() {
2600            super("host-namestring", "pathname");
2601        }
2602        @Override
2603        public LispObject execute(LispObject arg) {
2604            return coerceToPathname(arg).host; // XXX URL-PATHNAME
2605        }
2606    }
2607   
2608    public URL toURL() {
2609        try {
2610            if (isURL()) {
2611                return new URL(getNamestring());
2612            } else {
2613                return toFile().toURI().toURL();
2614            }
2615        } catch (MalformedURLException e) {
2616            error(new LispError(getNamestring() + " is not a valid URL"));
2617            return null; // not reached
2618        }
2619    }
2620
2621    public File toFile() {
2622        if(!isURL()) {
2623            return new File(getNamestring());
2624        } else {
2625            throw new RuntimeException(this + " does not represent a file");
2626        }
2627    }
2628
2629    static {
2630        LispObject obj = Symbol.DEFAULT_PATHNAME_DEFAULTS.getSymbolValue();
2631        Symbol.DEFAULT_PATHNAME_DEFAULTS.setSymbolValue(coerceToPathname(obj));
2632    }
2633
2634   
2635    @DocString(name="uri-decode",
2636               args="string",
2637               returns="string",
2638    doc="Decode STRING percent escape sequences in the manner of URI encodings.")
2639    private static final Primitive URI_DECODE = new pf_uri_decode();
2640    private static final class pf_uri_decode extends Primitive {
2641        pf_uri_decode() {
2642            super("uri-decode", PACKAGE_EXT, true);
2643        }
2644        @Override
2645        public LispObject execute(LispObject arg) {
2646            if (!(arg instanceof AbstractString)) {
2647                return type_error(arg, Symbol.STRING);
2648            }
2649            String result = uriDecode(((AbstractString)arg).toString());
2650            return new SimpleString(result);
2651        }
2652    };
2653
2654    static String uriDecode(String s) {
2655        try {
2656            URI uri = new URI("file://foo?" + s);
2657            return uri.getQuery();
2658        } catch (URISyntaxException e) {}
2659        return null;  // Error
2660    }
2661   
2662    @DocString(name="uri-encode",
2663               args="string",
2664               returns="string",
2665    doc="Encode percent escape sequences in the manner of URI encodings.")
2666    private static final Primitive URI_ENCODE = new pf_uri_encode();
2667    private static final class pf_uri_encode extends Primitive {
2668        pf_uri_encode() {
2669            super("uri-encode", PACKAGE_EXT, true);
2670        }
2671        @Override
2672        public LispObject execute(LispObject arg) {
2673            if (!(arg instanceof AbstractString)) {
2674                return type_error(arg, Symbol.STRING);
2675            }
2676            String result = uriEncode(((AbstractString)arg).toString());
2677            return new SimpleString(result);
2678        }
2679    };
2680
2681    static String uriEncode(String s) {
2682        // The constructor we use here only allows absolute paths, so
2683        // we manipulate the input and output correspondingly.
2684        String u;
2685        if (!s.startsWith("/")) {
2686            u = "/" + s;
2687        } else {
2688            u = new String(s);
2689        }
2690        try {
2691            URI uri = new URI("file", "", u, "");
2692            String result = uri.getRawPath();
2693            if (!s.startsWith("/")) {
2694                return result.substring(1);
2695            } 
2696            return result;
2697        } catch (URISyntaxException e) {
2698            Debug.assertTrue(false);
2699        }
2700        return null; // Error
2701    }
2702
2703
2704    File getFile() {
2705        String namestring = getNamestring(); // XXX UNC pathnames currently have no namestring
2706        if (namestring != null) {
2707            return new File(namestring);
2708        }
2709        error(new FileError("Pathname has no namestring: " + princToString(),
2710                        this));
2711        // Not reached.
2712        return null;
2713    }
2714    public static Pathname getDirectoryPathname(File file) {
2715        try {
2716            String namestring = file.getCanonicalPath();
2717            if (namestring != null && namestring.length() > 0) {
2718                if (namestring.charAt(namestring.length() - 1) != File.separatorChar) {
2719                    namestring = namestring.concat(File.separator);
2720                }
2721            }
2722            return new Pathname(namestring);
2723        } catch (IOException e) {
2724            error(new LispError(e.getMessage()));
2725            // Not reached.
2726            return null;
2727        }
2728    }
2729
2730}
2731
Note: See TracBrowser for help on using the repository browser.