# HG changeset patch
# Parent 33a110beafaa9612df7009cc46c6734319f408bb
Fix problems with #\Space characters in JAR pathnames.

We now require that inputs to the PATHNAME routines that have the URI
scheme "jar:file" or "file" properly encode themselves as URIs
according to RFC2396.  Mainly this means that #\Space and #\?
characters in such strings should be percent encoded
(i.e. "jar:file:/path%20with%20/space").  The corresponding namestring
routines have been similarly adjusted.  

The #\+ character is no longer an escape for #\Space (this was a bug).

diff --git a/src/org/armedbear/lisp/Pathname.java b/src/org/armedbear/lisp/Pathname.java
--- a/src/org/armedbear/lisp/Pathname.java
+++ b/src/org/armedbear/lisp/Pathname.java
@@ -38,12 +38,14 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.FileInputStream;
+import java.io.UnsupportedEncodingException;
 import java.net.MalformedURLException;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URL;
 import java.net.URLDecoder;
 import java.net.URLConnection;
+import java.net.URLEncoder;
 import java.util.Enumeration;
 import java.util.StringTokenizer;
 import java.util.zip.ZipEntry;
@@ -195,28 +197,10 @@
     }
 
     public Pathname(URL url) {
-        if ("file".equals(url.getProtocol())) {
-            String s = url.getPath();
-            if (s != null) {
-                if (Utilities.isPlatformWindows) {
-                    //  Workaround for Java's idea of URLs
-                    //  new (URL"file:///c:/a/b").getPath() --> "/c:/a/b"
-                    //  whereas we need "c" to be the DEVICE.
-                    if (s.length() > 2 
-                        && s.charAt(0) == '/'
-                        && s.charAt(2) == ':') {
-                        s = s.substring(1);
-                    }
-                }
-                init(s);
-                return;
-            }
-        } else {
-            init(url.toString());
-            return;
-        }
-        error(new LispError("Failed to construct Pathname from URL: "
-                            + "'" + url.toString() + "'"));
+         // URL handling is now buried in init(String), as the URI
+         // escaping mechanism didn't interact well with '+' and other
+         // characters. 
+        init(url.toString());
     }
 
     static final Symbol SCHEME = internKeyword("SCHEME");
@@ -279,19 +263,45 @@
                 jars = jars.push(p.device.car());
             }
             if (jar.startsWith("jar:file:")) {
-                String jarString 
-                    = jar.substring("jar:".length(),
+                String file
+                    = jar.substring("jar:file:".length(),
                                     jar.length() - jarSeparator.length());
-                // Use URL constructor to normalize Windows' use of device
-                URL url = null;
-                try {
-                    url = new URL(jarString);
-                } catch (MalformedURLException e) {
-                    error(new LispError("Failed to parse '" + jarString + "'"
-                            + " as URL:"
-                            + e.getMessage()));
+                Pathname jarPathname;
+                if (file.length() > 0) {
+                    // Instead of "use URL constructor to normalize Windows' use of device"
+                    // attempt to shorten the URL to pass through the normal constructor.
+                    if (Utilities.isPlatformWindows
+                        && file.charAt(0) == '/'
+                        && file.charAt(2) == ':'
+                        && Character.isLetter(file.charAt(1)))
+                        {
+                            file = file.substring(1);
+                        }
+                    URL url = null;
+                    URI uri = null;
+                    try {
+                        url = new URL("file:" + file);
+                        uri = url.toURI();
+                    } catch (MalformedURLException e1) {
+                        error(new FileError("Failed to create URI from "
+                                            + "'" + file + "'"
+                                            + ": " + e1.getMessage()));
+                    } catch (URISyntaxException e2) {
+                        error(new FileError("Failed to create URI from "
+                                            + "'" + file + "'"
+                                            + ": " + e2.getMessage()));
+                    }
+                    String path = uri.getPath();
+                    if (path == null) {
+                        // We allow "jar:file:baz.jar!/" to construct a relative
+                        // path for jar files, so MERGE-PATHNAMES means something.
+                        jarPathname = new Pathname(uri.getSchemeSpecificPart());
+                    } else {
+                        jarPathname = new Pathname(path);
+                    }
+                } else {
+                    jarPathname = new Pathname("");
                 }
-                Pathname jarPathname = new Pathname(url);
                 jars = jars.push(jarPathname);
             } else {
                 URL url = null;
@@ -315,7 +325,15 @@
         final int separatorIndex = s.lastIndexOf(jarSeparator);
         if (separatorIndex > 0 && s.startsWith("jar:")) {
             final String jarURL = s.substring(0, separatorIndex + jarSeparator.length());
-            Pathname d = new Pathname(jarURL);
+            URL url = null;
+            try {
+                url = new URL(jarURL);
+            } catch (MalformedURLException ex) {
+                error(new FileError("Failed to parse URL "
+                                    + "'" + jarURL + "'"
+                                    + ex.getMessage()));
+            }
+            Pathname d = new Pathname(url);
             if (device instanceof Cons) {
                 LispObject[] jars = d.copyToArray();
                 //  XXX Is this ever reached?  If so, need to append lists
@@ -342,7 +360,15 @@
             }
             String scheme = url.getProtocol();
             if (scheme.equals("file")) {
-                Pathname p = new Pathname(url.getFile());
+                URI uri = null;
+                try {
+                    uri = url.toURI();
+                } catch (URISyntaxException ex) {
+                    error(new FileError("Improper URI syntax for "
+                                    + "'" + url.toString() + "'"
+                                    + ": " + ex.toString()));
+                }
+                Pathname p = new Pathname(uri.getPath());
                 this.host = p.host;
                 this.device = p.device;
                 this.directory = p.directory;
@@ -596,6 +622,7 @@
                 return null;
             }
         }
+        boolean uriEncoded = false;
         if (device == NIL) {
         } else if (device == Keyword.UNSPECIFIC) {
         } else if (isJar()) {
@@ -605,8 +632,16 @@
                 prefix.append("jar:");
                 if (!((Pathname)jars[i]).isURL() && i == 0) {
                     sb.append("file:");
+                    uriEncoded = true;
                 }
-                sb.append(((Pathname) jars[i]).getNamestring());
+                Pathname jar = (Pathname) jars[i];
+                String encodedNamestring;
+                if (uriEncoded) {
+                    encodedNamestring = uriEncode(jar.getNamestring());
+                } else { 
+                    encodedNamestring = jar.getNamestring();
+                }
+                sb.append(encodedNamestring);
                 sb.append("!/");
             }
             sb = prefix.append(sb);
@@ -620,6 +655,9 @@
             Debug.assertTrue(false);
         }
         String directoryNamestring = getDirectoryNamestring();
+        if (uriEncoded) {
+            directoryNamestring = uriEncode(directoryNamestring);
+        }
         if (isJar()) {
             if (directoryNamestring.startsWith("/")) {
                 sb.append(directoryNamestring.substring(1));
@@ -635,7 +673,11 @@
                 Debug.assertTrue(namestring == null);
                 return null;
             }
-            sb.append(n);
+            if (uriEncoded) {
+                sb.append(uriEncode(n));
+            } else {
+                sb.append(n);
+            }
         } else if (name == Keyword.WILD) {
             sb.append('*');
         }
@@ -650,7 +692,11 @@
                         return null;
                     }
                 }
-                sb.append(t);
+                if (uriEncoded) {
+                    sb.append(uriEncode(t));
+                } else {
+                    sb.append(t);
+                }
             } else if (type == Keyword.WILD) {
                 sb.append('*');
             } else {
@@ -1981,7 +2027,12 @@
                 LispObject truename = Pathname.truename((Pathname)o, errorIfDoesNotExist);
                 if (truename != null
                     && truename instanceof Pathname) {
-                    jars.car = (Pathname)truename;
+                    Pathname truePathname = (Pathname)truename;
+                    // A jar that is a directory makes no sense, so exit
+                    if (truePathname.getNamestring().endsWith("/")) {
+                        break jarfile;
+                    }
+                    jars.car = truePathname;
                 } else {
                     break jarfile;
                 }
@@ -1994,6 +2045,7 @@
             // 2.  JAR in JAR
             // 3.  JAR with Entry
             // 4.  JAR in JAR with Entry
+
             ZipFile jarFile = ZipCache.get((Pathname)jars.car());
             String entryPath = pathname.asEntryPath();
             if (jarFile != null) {
@@ -2350,5 +2402,34 @@
         Symbol.DEFAULT_PATHNAME_DEFAULTS.setSymbolValue(coerceToPathname(obj));
     }
 
+    static String uriDecode(String s) {
+        try {
+            URI uri = new URI(null, null, null, s, null);
+            return uri.toASCIIString().substring(1);
+        } catch (URISyntaxException e) {}
+        return null;  // Error
+    }
+
+    static String uriEncode(String s) {
+        // The constructor we use here only allows absolute paths, so
+        // we manipulate the input and output correspondingly.
+        String u;
+        if (!s.startsWith("/")) {
+            u = "/" + s;
+        } else {
+            u = new String(s);
+        }
+        try {
+            URI uri = new URI("file", "", u, "");
+            String result = uri.getRawPath();
+            if (!s.startsWith("/")) {
+                return result.substring(1);
+            } 
+            return result;
+        } catch (URISyntaxException e) {
+            Debug.assertTrue(false);
+        }
+        return null; // Error
+    }
 }
 
diff --git a/src/org/armedbear/lisp/Utilities.java b/src/org/armedbear/lisp/Utilities.java
--- a/src/org/armedbear/lisp/Utilities.java
+++ b/src/org/armedbear/lisp/Utilities.java
@@ -254,22 +254,6 @@
         return result;
     }
 
-    static String uriEncode(String s) {
-        try {
-            URI uri = new URI("?" + s);
-            return uri.getQuery();
-        } catch (URISyntaxException e) {}
-        return null;
-    }
-
-    static String uriDecode(String s) {
-        try {
-            URI uri = new URI(null, null, null, s, null);
-            return uri.toASCIIString().substring(1);
-        } catch (URISyntaxException e) {}
-        return null;  // Error
-    }
-    
     static String escapeFormat(String s) {
         return s.replace("~", "~~");
     }
