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

Last change on this file since 14659 was 14659, checked in by mevenson, 3 years ago

Fix Uniform Naming Convention (aka "UNC" or "network") paths under Windows.

DIRECTORY now works again on UNC paths.

UNC paths may be either specified with either back slash (#
) or
forward slash (#\/) doubled as the first character in a Pathname
namestring.

The patterns in

<server>/<share>/[directories-and-files]

are parsed as

<server> is stored as HOST.

<share> is stored as DEVICE.

[directories-and-files] gets parsed as per the normal rules under
Windows.

Mixing namestrings with both backslash and slash characters can lead
to unpredictable results. It is recommended not to use backslash
characters in namestrings if it can be avoided. The pathname printed
representation is always normalized to using forward slash delimiters.

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