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