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

Last change on this file was 13718, checked in by Mark Evenson, 14 years ago

backport r13704: Fix problems loading ABCL-CONTRIB.

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