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 | |
---|
55 | protected URLPathname() {} |
---|
56 | |
---|
57 | public static URLPathname create() { |
---|
58 | return new URLPathname(); |
---|
59 | } |
---|
60 | |
---|
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 | |
---|
70 | public static URLPathname create(URL url) { |
---|
71 | return URLPathname.create(url.toString()); |
---|
72 | } |
---|
73 | |
---|
74 | public static URLPathname create(URI uri) { |
---|
75 | return URLPathname.create(uri.toString()); |
---|
76 | } |
---|
77 | |
---|
78 | static public final LispObject FILE = new SimpleString("file"); |
---|
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 | |
---|
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 | |
---|
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 | |
---|
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 | |
---|
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 | |
---|
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 | |
---|
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 | |
---|
238 | public String getNamestring() { |
---|
239 | StringBuilder sb = new StringBuilder(); |
---|
240 | return getNamestring(sb); |
---|
241 | } |
---|
242 | |
---|
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 (??) |
---|
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 | |
---|
374 | public LispObject typeOf() { |
---|
375 | return Symbol.URL_PATHNAME; |
---|
376 | } |
---|
377 | |
---|
378 | @Override |
---|
379 | public LispObject classOf() { |
---|
380 | return BuiltInClass.URL_PATHNAME; |
---|
381 | } |
---|
382 | |
---|
383 | public static LispObject truename(Pathname p, boolean errorIfDoesNotExist) { |
---|
384 | URLPathname pathnameURL = (URLPathname)URLPathname.createFromFile(p); |
---|
385 | return URLPathname.truename(pathnameURL, errorIfDoesNotExist); |
---|
386 | } |
---|
387 | |
---|
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 | |
---|
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 | |
---|
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 | |
---|
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 { |
---|
469 | pf_uri_decode() { |
---|
470 | super("uri-decode", PACKAGE_EXT, true); |
---|
471 | } |
---|
472 | @Override |
---|
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 | |
---|
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 { |
---|
496 | pf_uri_encode() { |
---|
497 | super("uri-encode", PACKAGE_EXT, true); |
---|
498 | } |
---|
499 | @Override |
---|
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 | |
---|
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 | } |
---|