1 /*
2  * URLPathname.java
3  *
4  * Copyright (C) 2020 @easye
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU General Public License
8  * as published by the Free Software Foundation; either version 2
9  * of the License, or (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, write to the Free Software
18  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19  *
20  * As a special exception, the copyright holders of this library give you
21  * permission to link this library with independent modules to produce an
22  * executable, regardless of the license terms of these independent
23  * modules, and to copy and distribute the resulting executable under
24  * terms of your choice, provided that you also meet, for each linked
25  * independent module, the terms and conditions of the license of that
26  * module.  An independent module is a module which is not derived from
27  * or based on this library.  If you modify this library, you may extend
28  * this exception to your version of the library, but you are not
29  * obligated to do so.  If you do not wish to do so, delete this
30  * exception statement from your version.
31  */
32 
33 package org.armedbear.lisp;
34 
35 import static org.armedbear.lisp.Lisp.*;
36 
37 import java.io.File;
38 import java.io.InputStream;
39 import java.io.IOException;
40 import java.net.URL;
41 import java.net.URLConnection;
42 import java.net.URI;
43 import java.net.MalformedURLException;
44 import java.net.URISyntaxException;
45 import java.text.MessageFormat;
46 
47 public class URLPathname
48   extends Pathname
49 {
50   static public final Symbol SCHEME = internKeyword("SCHEME");
51   static public final Symbol AUTHORITY = internKeyword("AUTHORITY");
52   static public final Symbol QUERY = internKeyword("QUERY");
53   static public final Symbol FRAGMENT = internKeyword("FRAGMENT");
54 
URLPathname()55   protected URLPathname() {}
56 
create()57   public static URLPathname create() {
58     return new URLPathname();
59   }
60 
create(Pathname p)61   public static URLPathname create(Pathname p) {
62     if (p instanceof URLPathname) {
63       URLPathname result = new URLPathname();
64       result.copyFrom(p);
65       return result;
66     }
67     return (URLPathname)createFromFile((Pathname)p);
68   }
69 
create(URL url)70   public static URLPathname create(URL url) {
71     return URLPathname.create(url.toString());
72   }
73 
create(URI uri)74   public static URLPathname create(URI uri) {
75     return URLPathname.create(uri.toString());
76   }
77 
78   static public final LispObject FILE = new SimpleString("file");
createFromFile(Pathname p)79   public static URLPathname createFromFile(Pathname p) {
80     URLPathname result = new URLPathname();
81     result.copyFrom(p);
82     LispObject scheme = NIL;
83     scheme = scheme.push(FILE).push(SCHEME);
84     result.setHost(scheme);
85     return result;
86   }
87 
create(String s)88   public static URLPathname create(String s) {
89     if (!isValidURL(s)) {
90       parse_error("Cannot form a PATHNAME-URL from " + s);
91     }
92     if (s.startsWith(JarPathname.JAR_URI_PREFIX)) {
93       return JarPathname.create(s);
94     }
95 
96     URLPathname result = new URLPathname();
97     URL url = null;
98     try {
99       url = new URL(s);
100     } catch (MalformedURLException e) {
101       parse_error("Malformed URL in namestring '" + s + "': " + e.toString());
102       return (URLPathname) UNREACHED;
103     }
104     String scheme = url.getProtocol();
105     if (scheme.equals("file")) {
106       URI uri = null;
107       try {
108         uri = new URI(s);
109       } catch (URISyntaxException ex) {
110         parse_error("Improper URI syntax for "
111 		    + "'" + url.toString() + "'"
112 		    + ": " + ex.toString());
113 	return (URLPathname)UNREACHED;
114       }
115 
116       String uriPath = uri.getPath();
117       if (null == uriPath) {
118         // Under Windows, deal with pathnames containing
119         // devices expressed as "file:z:/foo/path"
120         uriPath = uri.getSchemeSpecificPart();
121         if (uriPath == null || uriPath.equals("")) {
122           parse_error("The namestring URI has no path: " + uri);
123 	  return (URLPathname)UNREACHED;
124         }
125       }
126       final File file = new File(uriPath);
127       String path = file.getPath();
128       if (uri.toString().endsWith("/") && !path.endsWith("/")) {
129         path += "/";
130       }
131       final Pathname p = (Pathname)Pathname.create(path);
132       LispObject host = NIL.push(FILE).push(SCHEME);
133       result
134         .setHost(host)
135         .setDevice(p.getDevice())
136         .setDirectory(p.getDirectory())
137         .setName(p.getName())
138         .setType(p.getType())
139         .setVersion(p.getVersion());
140       return result;
141     }
142     Debug.assertTrue(scheme != null);
143     URI uri = null;
144     try {
145       uri = url.toURI().normalize();
146     } catch (URISyntaxException e) {
147       parse_error("Couldn't form URI from "
148 		  + "'" + url + "'"
149 		  + " because: " + e);
150       return (URLPathname)UNREACHED;
151     }
152     String authority = uri.getAuthority();
153     if (authority == null) {
154       authority = url.getAuthority();
155     }
156 
157     LispObject host = NIL;
158     host = host.push(SCHEME).push(new SimpleString(scheme));
159     if (authority != null) {
160       host = host.push(AUTHORITY).push(new SimpleString(authority));
161     }
162     String query = uri.getRawQuery();
163     if (query != null) {
164       host = host.push(QUERY).push(new SimpleString(query));
165     }
166     String fragment = uri.getRawFragment();
167     if (fragment != null) {
168       host = host.push(FRAGMENT).push(new SimpleString(fragment));
169     }
170     host = host.nreverse();
171     result.setHost(host);
172 
173     // URI encode necessary characters
174     String path = uri.getRawPath();
175     if (path == null) {
176       path = "";
177     }
178 
179     Pathname p = (Pathname)Pathname.create(path != null ? path : "");
180     result
181       .setDirectory(p.getDirectory())
182       .setName(p.getName())
183       .setType(p.getType());
184 
185     return result;
186   }
187 
toURI()188   public URI toURI() {
189     String uriString = getNamestringAsURL();
190     try {
191       URI uri = new URI(uriString);
192       return uri;
193     } catch (URISyntaxException eo) {
194       return null;
195     }
196   }
197 
toURL()198   public URL toURL() {
199     URI uri = toURI();
200     try {
201       if (uri != null) {
202         return uri.toURL();
203       }
204     } catch (MalformedURLException e) {
205     }
206     return null;
207   }
208 
getFile()209   public File getFile() {
210     if (!hasExplicitFile(this)) {
211       return null; // TODO signal that this is not possible?
212     }
213     URI uri = toURI();
214     if (uri == null) {
215       return null;
216     }
217     File result = new File(uri);
218     return result;
219   }
220 
isFile(Pathname p)221   static public boolean isFile(Pathname p) {
222     LispObject scheme = Symbol.GETF.execute(p.getHost(), SCHEME, NIL);
223     if (scheme.equals(NIL)
224         || hasExplicitFile(p)) {
225       return true;
226     }
227     return false;
228   }
229 
hasExplicitFile(Pathname p)230   static public boolean hasExplicitFile(Pathname p) {
231     if (!p.getHost().listp()) {
232         return false;
233     }
234     LispObject scheme = Symbol.GETF.execute(p.getHost(), SCHEME, NIL);
235     return scheme.equalp(FILE);
236   }
237 
getNamestring()238   public String getNamestring() {
239     StringBuilder sb = new StringBuilder();
240     return getNamestring(sb);
241   }
242 
getNamestring(StringBuilder sb)243   public String getNamestring(StringBuilder sb) {
244     LispObject scheme = Symbol.GETF.execute(getHost(), SCHEME, NIL);
245     LispObject authority = Symbol.GETF.execute(getHost(), AUTHORITY, NIL);
246 
247     // A scheme of NIL is implicitly "file:", for which we don't emit
248     // as part of the usual namestring.  getNamestringAsURI() should
249     // emit the 'file:' string
250     boolean percentEncode = true;
251     if (scheme.equals(NIL)) {
252       percentEncode = false;
253     } else {
254       sb.append(scheme.getStringValue());
255       sb.append(":");
256       if (authority != NIL) {
257         sb.append("//");
258         sb.append(authority.getStringValue());
259       } else if (scheme.equalp(FILE)) {
260         sb.append("//");
261       }
262     }
263     // <https://docs.microsoft.com/en-us/archive/blogs/ie/file-uris-in-windows>
264     if (Utilities.isPlatformWindows
265 	&& getDevice() instanceof SimpleString) {
266       sb.append("/")
267         .append(getDevice().getStringValue())
268 	.append(":");
269     }
270     String directoryNamestring = getDirectoryNamestring();
271     if (percentEncode) {
272       directoryNamestring = uriEncode(directoryNamestring);
273     }
274     sb.append(directoryNamestring);
275 
276     // Use the output of Pathname
277     Pathname p = new Pathname();
278     p.copyFrom(this)
279       .setHost(NIL)
280       .setDevice(NIL)
281       .setDirectory(NIL);
282     String nameTypeVersion = p.getNamestring();
283     if (percentEncode) {
284       nameTypeVersion = uriEncode(nameTypeVersion);
285     }
286     sb.append(nameTypeVersion);
287 
288     LispObject o = Symbol.GETF.execute(getHost(), QUERY, NIL);
289     if (o != NIL) {
290       sb.append("?")
291         .append(uriEncode(o.getStringValue()));
292     }
293     o = Symbol.GETF.execute(getHost(), FRAGMENT, NIL);
294     if (o != NIL) {
295       sb.append("#")
296         .append(uriEncode(o.getStringValue()));
297     }
298 
299     return sb.toString();
300   }
301 
302   // We need our "own" rules for outputting a URL
303   // 1.  For DOS drive letters
304   // 2.  For relative "file" schemas (??)
getNamestringAsURL()305   public String getNamestringAsURL() {
306     LispObject schemeProperty = Symbol.GETF.execute(getHost(), SCHEME, NIL);
307     LispObject authorityProperty = Symbol.GETF.execute(getHost(), AUTHORITY, NIL);
308     LispObject queryProperty = Symbol.GETF.execute(getHost(), QUERY, NIL);
309     LispObject fragmentProperty = Symbol.GETF.execute(getHost(), FRAGMENT, NIL);
310 
311     String scheme;
312     String authority = null;
313     if (!schemeProperty.equals(NIL)) {
314       scheme = schemeProperty.getStringValue();
315       if (!authorityProperty.equals(NIL)) {
316         authority =  authorityProperty.getStringValue();
317       }
318     } else {
319       scheme = "file";
320     }
321 
322     String directory = getDirectoryNamestring();
323     String file = "";
324     LispObject fileNamestring = Symbol.FILE_NAMESTRING.execute(this);
325     if (!fileNamestring.equals(NIL)) {
326       file = fileNamestring.getStringValue();
327     }
328     String path = "";
329 
330     if (!directory.equals("")) {
331       if (Utilities.isPlatformWindows
332 	  && getDevice() instanceof SimpleString) {
333 	path = getDevice().getStringValue() + ":" + directory + file;
334       } else {
335 	path = directory + file;
336       }
337     } else {
338       path = file;
339     }
340 
341     path = uriEncode(path);
342 
343     String query = null;
344     if (!queryProperty.equals(NIL)) {
345       query = queryProperty.getStringValue();
346     }
347 
348     String fragment = null;
349     if (!fragmentProperty.equals(NIL)) {
350       fragment = fragmentProperty.getStringValue();
351     }
352 
353     StringBuffer result = new StringBuffer(scheme);
354     result.append(":");
355     result.append("//");
356     if (authority != null) {
357       result.append(authority);
358     }
359     if (!path.startsWith("/")) {
360       result.append("/");
361     }
362     result.append(path);
363 
364     if (query != null) {
365       result.append("?").append(query);
366     }
367 
368     if (fragment != null) {
369       result.append("#").append(fragment);
370     }
371     return result.toString();
372   }
373 
typeOf()374   public LispObject typeOf() {
375     return Symbol.URL_PATHNAME;
376   }
377 
378   @Override
classOf()379   public LispObject classOf() {
380     return BuiltInClass.URL_PATHNAME;
381   }
382 
truename(Pathname p, boolean errorIfDoesNotExist)383   public static LispObject truename(Pathname p, boolean errorIfDoesNotExist) {
384     URLPathname pathnameURL = (URLPathname)URLPathname.createFromFile(p);
385     return URLPathname.truename(pathnameURL, errorIfDoesNotExist);
386   }
387 
truename(URLPathname p, boolean errorIfDoesNotExist)388   public static LispObject truename(URLPathname p, boolean errorIfDoesNotExist) {
389     if (p.getHost().equals(NIL)
390         || hasExplicitFile(p)) {
391       LispObject fileTruename = Pathname.truename(p, errorIfDoesNotExist);
392       if (fileTruename.equals(NIL)) {
393         return NIL;
394       }
395       if (!(fileTruename instanceof URLPathname)) {
396         URLPathname urlTruename = URLPathname.createFromFile((Pathname)fileTruename);
397         return urlTruename;
398       }
399       return fileTruename;
400     }
401 
402     if (p.getInputStream() != null) {
403       // If there is no type, query or fragment, we check to
404       // see if there is URL available "underneath".
405       if (p.getName() != NIL
406           && p.getType() == NIL
407           && Symbol.GETF.execute(p.getHost(), URLPathname.QUERY, NIL) == NIL
408           && Symbol.GETF.execute(p.getHost(), URLPathname.FRAGMENT, NIL) == NIL) {
409         if (p.getInputStream() != null) {
410           return p;
411         }
412       }
413       return p;
414     }
415     return Pathname.doTruenameExit(p, errorIfDoesNotExist);
416   }
417 
getInputStream()418   public InputStream getInputStream() {
419     InputStream result = null;
420 
421     if (URLPathname.isFile(this)) {
422       Pathname p = new Pathname();
423       p.copyFrom(this)
424         .setHost(NIL);
425       return p.getInputStream();
426     }
427 
428     if (URLPathname.isFile(this)) {
429       Pathname p = new Pathname();
430       p.copyFrom(this)
431         .setHost(NIL);
432       return p.getInputStream();
433     }
434 
435     URL url = this.toURL();
436     try {
437       result = url.openStream();
438     } catch (IOException e) {
439       Debug.warn("Failed to get InputStream from "
440                  + "'" + getNamestring() + "'"
441                  + ": " + e);
442     }
443     return result;
444   }
445 
getURLConnection()446   URLConnection getURLConnection() {
447     Debug.assertTrue(isURL());
448     URL url = this.toURL();
449     URLConnection result = null;
450     try {
451       result = url.openConnection();
452     } catch (IOException e) {
453       error(new FileError("Failed to open URL connection.",
454                           this));
455     }
456     return result;
457   }
458 
getLastModified()459   public long getLastModified() {
460     return getURLConnection().getLastModified();
461   }
462 
463   @DocString(name="uri-decode",
464              args="string",
465              returns="string",
466              doc="Decode STRING percent escape sequences in the manner of URI encodings.")
467   private static final Primitive URI_DECODE = new pf_uri_decode();
468   private static final class pf_uri_decode extends Primitive {
pf_uri_decode()469     pf_uri_decode() {
470       super("uri-decode", PACKAGE_EXT, true);
471     }
472     @Override
execute(LispObject arg)473     public LispObject execute(LispObject arg) {
474       if (!(arg instanceof AbstractString)) {
475         return type_error(arg, Symbol.STRING);
476       }
477       String result = uriDecode(((AbstractString)arg).toString());
478       return new SimpleString(result);
479     }
480   };
481 
uriDecode(String s)482   static String uriDecode(String s) {
483     try {
484       URI uri = new URI("file://foo?" + s);
485       return uri.getQuery();
486     } catch (URISyntaxException e) {}
487     return null;  // Error
488   }
489 
490   @DocString(name="uri-encode",
491              args="string",
492              returns="string",
493              doc="Encode percent escape sequences in the manner of URI encodings.")
494   private static final Primitive URI_ENCODE = new pf_uri_encode();
495   private static final class pf_uri_encode extends Primitive {
pf_uri_encode()496     pf_uri_encode() {
497       super("uri-encode", PACKAGE_EXT, true);
498     }
499     @Override
execute(LispObject arg)500     public LispObject execute(LispObject arg) {
501       if (!(arg instanceof AbstractString)) {
502         return type_error(arg, Symbol.STRING);
503       }
504       String result = uriEncode(((AbstractString)arg).toString());
505       return new SimpleString(result);
506     }
507   };
508 
uriEncode(String s)509   static String uriEncode(String s) {
510     // The constructor we use here only allows absolute paths, so
511     // we manipulate the input and output correspondingly.
512     String u;
513     if (!s.startsWith("/")) {
514       u = "/" + s;
515     } else {
516       u = new String(s);
517     }
518     try {
519       URI uri = new URI("file", "", u, "");
520       String result = uri.getRawPath();
521       if (!s.startsWith("/")) {
522         return result.substring(1);
523       }
524       return result;
525     } catch (URISyntaxException e) {
526       Debug.assertTrue(false);
527     }
528     return null; // Error
529   }
530 }
531