source: branches/1.1.x/src/org/armedbear/lisp/Pathname.java

Last change on this file was 14238, checked in by Mark Evenson, 12 years ago

Fixes #243: MAKE-PATHNAME with a DEVICE string.

We allow DEVICE lists to contain a string value as constructed by
MAKE-PATHNAME, but the result can never actually be resolvable by
TRUENAME.

Instead of trying to figure out the proper use of Java labels, just
use the private static Pathname.doTruenameExit() as the common point
for all exits from the TRUENAME implementation.

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