source: trunk/abcl/src/org/armedbear/lisp/JarPathname.java

Last change on this file was 15681, checked in by Mark Evenson, 13 months ago

JAR-PATHNAME implements getFile()

This may not be that useful, as the implementation of java.io.File
returned from non-default FileSystems?, of which zip is one, is not
required to implement all behavior.

File size: 20.5 KB
Line 
1/*
2 * JarPathname.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
33package org.armedbear.lisp;
34
35import static org.armedbear.lisp.Lisp.*;
36
37import java.io.InputStream;
38import java.io.IOException;
39import java.io.File;
40import java.net.URL;
41import java.net.URI;
42import java.net.MalformedURLException;
43import java.net.URISyntaxException;
44import java.nio.file.FileSystem;
45import java.nio.file.FileSystemAlreadyExistsException;
46import java.nio.file.FileSystems;
47import java.nio.file.Path;
48import java.util.ArrayList;
49import java.util.Iterator;
50import java.util.List;
51import java.util.HashMap;
52import java.util.Map;
53import java.util.Set;
54import java.util.zip.ZipEntry;
55import java.util.zip.ZipFile;
56
57public class JarPathname
58  extends URLPathname
59{
60  static final public String JAR_URI_SUFFIX = "!/";
61  static final public String JAR_URI_PREFIX = "jar:";
62
63  protected JarPathname() {}
64
65  public static JarPathname create() {
66    return new JarPathname();
67  }
68
69  public static JarPathname create(JarPathname p) {
70    JarPathname result = new JarPathname();
71    result.copyFrom(p);
72    return result;
73  }
74
75  public static JarPathname createFromPathname(Pathname p) {
76    JarPathname result = new JarPathname();
77    URLPathname rootDevice = new URLPathname();
78
79    if (p instanceof URLPathname) {
80      rootDevice.copyFrom(p);
81    } else if (p instanceof Pathname) {
82      rootDevice = URLPathname.create(p);
83    } else {
84      simple_error("Argument is already a JAR-PATHNAME: ~a", p);
85    }
86
87    result.setDevice(new Cons(rootDevice, NIL));
88
89    return result;
90  }
91
92  /** Transform a reference to a nested Jar to an entry */
93  public static JarPathname archiveAsEntry(JarPathname p) {
94    JarPathname result = new JarPathname();
95    result = (JarPathname)result.copyFrom(p);
96
97    LispObject jars = result.getJars();
98    jars = jars.nreverse();
99    Pathname entry = (Pathname)jars.car();
100    jars = jars.cdr().nreverse();
101
102    result
103      .setDevice(jars)
104      .setDirectory(entry.getDirectory())
105      .setName(entry.getName())
106      .setType(entry.getType());
107   
108    return result;
109  }
110   
111
112  /** Transform an entry in a jar to a reference as a jar */
113  public static JarPathname createFromEntry(JarPathname p) {
114    JarPathname result = new JarPathname();
115    result
116      .copyFrom(p)
117      .setDirectory(NIL)
118      .setName(NIL)
119      .setType(NIL)
120      .setVersion(Keyword.NEWEST);
121    Pathname entryPath = p.getEntryPath();
122    LispObject device = result.getDevice();
123    device = device.reverse().push(entryPath).reverse();
124    result.setDevice(device);
125    return result;
126  }
127
128  @DocString(name="as-jar-pathname-archive",
129             args="pathname",
130             returns="jar-pathname",
131             doc="Returns PATHNAME as a reference to a JAR-PATHNAME archive"
132             + "\n"
133             + "If PATHNAME names an ordinary file, the resulting JAR-PATHNAME addresses the"
134             + "file as an archive.  If PATHNAME names an entry in an archive, the resulting"
135             + "JAR-PATHNAME addresses that entry as a zip archive within that archive.")
136  private static final Primitive AS_JAR_PATHNAME_ARCHIVE = new pf_as_jar_pathname_archive();
137  private static class pf_as_jar_pathname_archive extends Primitive {
138    pf_as_jar_pathname_archive() {
139      super("as-jar-pathname-archive", PACKAGE_EXT, true);
140    }
141    @Override
142    public LispObject execute(LispObject arg) {
143      if (arg instanceof AbstractString) {
144        arg = coerceToPathname(arg);
145      }
146      if (arg instanceof JarPathname) {
147        return createFromEntry((JarPathname)arg);
148      } if (arg instanceof Pathname) {
149        return createFromPathname((Pathname)arg);
150      }
151      type_error(arg,
152                 list(Symbol.OR,
153                      Symbol.PATHNAME, Symbol.URL_PATHNAME,
154                      Symbol.JAR_PATHNAME));
155      return (LispObject)UNREACHED;
156    }
157  };
158   
159  static public JarPathname createFromFile(String s) {
160    JarPathname result
161      = JarPathname.create(JAR_URI_PREFIX + "file:" + s + JAR_URI_SUFFIX);
162    result.setVersion(Keyword.NEWEST);
163    return result;
164  }
165
166  static public JarPathname createEntryFromFile(String jar, String entry) {
167    JarPathname result
168      = JarPathname.create(JAR_URI_PREFIX + "file:" + jar + JAR_URI_SUFFIX + entry);
169    result.setVersion(Keyword.NEWEST);
170    return result;
171  }
172
173  static public JarPathname createEntryFromJar(JarPathname jar, String entry) {
174    if (jar.isArchiveEntry()) {
175      simple_error("Failed to create the entry ~a in ~a", entry, jar);
176      return (JarPathname)UNREACHED;
177    }
178    JarPathname result = new JarPathname();
179    result.copyFrom(jar);
180    String path = new String(entry);
181    if (!path.startsWith("/")) {
182      path = "/" + path;
183    }
184    Pathname p = Pathname.create(path);
185    result
186      .setDirectory(p.getDirectory())
187      .setName(p.getName())
188      .setType(p.getType())
189      .setVersion(Keyword.NEWEST);
190   
191    return result;
192  }
193  /**
194   *  Enumerate the components of a jar namestring
195   */
196  static List<String> enumerate(String s) {
197    ArrayList<String> result = new ArrayList<String>();
198
199    int i = s.lastIndexOf(JAR_URI_PREFIX);
200    if (i == -1) {
201      parse_error("Failed to find any occurence of '" + JAR_URI_PREFIX + "' prefixes:" + s);
202      return null; // not reached
203    }
204    i += JAR_URI_PREFIX.length(); // advance index to end of "jar:jar:jar:..."
205    if ((i % JAR_URI_PREFIX.length()) != 0) {
206      parse_error("Failed to parse 'jar:' prefixes:" + s);
207      return null;
208    }
209    int prefixCount = i / JAR_URI_PREFIX.length(); 
210    String withoutPrefixes = s.substring(i);
211
212    String parts[] = withoutPrefixes.split(JAR_URI_SUFFIX);
213
214    // Do we have as many prefixes as suffixes?
215    String notEndingInSuffix = withoutPrefixes + "nonce"; 
216    String suffixParts[] = notEndingInSuffix.split(JAR_URI_SUFFIX);
217    int suffixCount = suffixParts.length - 1;
218    if (suffixCount != prefixCount) {
219      parse_error("Mismatched 'jar:' prefix and '/!' suffixes in jar: " + s);
220      return null;
221    }
222
223    if (parts.length == 1) {
224      if (!s.endsWith(JAR_URI_SUFFIX)) {
225        error(new SimpleError("No trailing jar uri suffix: " + s));
226        return null;
227      }
228      if (!isValidURL(parts[0])) {
229        error(new SimpleError("Not a valid URI: " + parts[0]));
230        return null;
231      }
232
233      result.add(parts[0]);
234      return result;
235    }
236
237    // The root, non-JarPathname location of this reference
238    // For files, possibly either a relative or absolute directory
239    result.add(parts[0]);
240
241    // The references to the pathnames of archives located within the
242    // root jar.
243    // These will be relative directory paths suffixed with JAR_URI_SUFFIX
244    for (int j = 1; j < prefixCount; j++) {
245      String ns = parts[j] + JAR_URI_SUFFIX;
246      result.add(ns);
247    }
248
249    // possibly return the path inside the last jar as an absolute path
250    if (parts.length == (prefixCount + 1)) {
251      result.add("/" + parts[parts.length - 1]);
252    }
253
254    return result;
255  }
256
257  static public JarPathname create(String s) {
258    if (!s.startsWith(JAR_URI_PREFIX)) {
259      parse_error("Cannot create a PATHNAME-JAR from namestring: " + s);
260      return (JarPathname)UNREACHED;
261    }
262   
263    List<String> contents = JarPathname.enumerate(s);
264
265    if (contents == null) {
266      parse_error("Couldn't parse PATHNAME-JAR from namestring: " + s);
267      return (JarPathname)UNREACHED;
268    }
269   
270    JarPathname result = new JarPathname();
271
272    // Normalize the root jar to be a URL
273    URLPathname rootPathname;
274    String rootNamestring = contents.get(0);
275    if (!isValidURL(rootNamestring)) {
276      Pathname root = Pathname.create(rootNamestring);
277      rootPathname = URLPathname.createFromFile(root);
278    } else {
279      rootPathname = URLPathname.create(rootNamestring);
280    }
281
282    LispObject jars = NIL;
283    jars = jars.push(rootPathname);
284   
285    if (contents.size() == 1) {
286      result.setDevice(jars);
287      return result;
288    }
289
290    for (int i = 1; i < contents.size(); i++) {
291      String ns = contents.get(i);
292      if (ns.endsWith(JAR_URI_SUFFIX)) {
293        String nsWithoutSuffix = ns.substring(0, ns.length() - JAR_URI_SUFFIX.length());
294        Pathname pathname = (Pathname)Pathname.create(nsWithoutSuffix);
295        Pathname jar = new Pathname();
296        jar.copyFrom(pathname);
297        jars = jars.push(jar);
298      } else { 
299        Pathname p = (Pathname)Pathname.create(contents.get(i));
300        result.copyFrom(p);
301      }
302    }
303    jars = jars.nreverse();
304    result.setDevice(jars);
305    result.validateComponents();
306    return result;
307  }
308
309  public LispObject validateComponents() {
310    if (!(getDevice() instanceof Cons)) {
311      return type_error("Invalid DEVICE for JAR-PATHNAME", getDevice(), Symbol.CONS);
312    }
313
314    LispObject jars = getDevice();
315
316    LispObject rootJar = getRootJar();
317    if (!(rootJar instanceof URLPathname)) {
318        return type_error("The first element in the DEVICE component of a JAR-PATHNAME is not of expected type",
319                          rootJar,
320                          Symbol.URL_PATHNAME);
321    }
322
323    jars = jars.cdr();
324   
325    while (!jars.car().equals(NIL)) {
326      LispObject jar = jars.car();
327      if (!((jar instanceof Pathname)
328            || (jar instanceof URLPathname))) {
329        return type_error("The value in DEVICE component of a JAR-PATHNAME is not of expected type",
330                          jar,
331                          list(Symbol.OR,
332                               Symbol.PATHNAME, Symbol.URL_PATHNAME));
333      }
334      jars = jars.cdr();
335    }
336
337    return T;
338  }
339
340  public String getNamestring() {
341    StringBuffer sb = new StringBuffer();
342
343    LispObject jars = getJars();
344
345    if (jars.equals(NIL) || jars.equals(Keyword.UNSPECIFIC)) { 
346      // type_error("JAR-PATHNAME has bad DEVICE",
347      //            jars,
348      //            list(Symbol.NOT,
349      //                 list(Symbol.OR,
350      //                      list(Symbol.EQL, NIL),
351      //                      list(Symbol.EQL, Keyword.UNSPECIFIC))));
352      return null;
353    }
354
355    for (int i = 0; i < jars.length() - 1; i++) {
356      sb.append(JAR_URI_PREFIX);
357    }
358
359    LispObject root = getRootJar();
360
361    if (root instanceof URLPathname) {
362      String ns = ((URLPathname)root).getNamestringAsURL();
363      sb.append(JAR_URI_PREFIX)
364        .append(ns)
365        .append(JAR_URI_SUFFIX);
366    } else if (root instanceof Pathname) { // For transitional compatibility?
367      String ns = ((Pathname)root).getNamestring();
368      sb.append(JAR_URI_PREFIX)
369        .append("file:")
370        .append(ns)
371        .append(JAR_URI_SUFFIX);
372    } else {
373      simple_error("Unable to generate namestring for jar with root pathname ~a", root); 
374    }
375
376    LispObject innerJars = jars.cdr();
377    while (innerJars.car() != NIL) {
378      Pathname jar = (Pathname)innerJars.car();
379      Pathname p = new Pathname();
380      p.copyFrom(jar)
381        .setDevice(NIL);
382      String ns = p.getNamestring();
383      sb.append(ns)
384        .append(JAR_URI_SUFFIX);
385      innerJars = innerJars.cdr();
386    }
387
388    if (getDirectory() != NIL
389        || getName() != NIL
390        || getType() != NIL) {
391     
392      Pathname withoutDevice = new Pathname();
393      withoutDevice
394        .copyFrom(this)
395        .setDevice(NIL);
396
397      String withoutDeviceNamestring = withoutDevice.getNamestring(); // need to URI encode?
398      if (withoutDeviceNamestring.startsWith("/")) {
399        sb.append(withoutDeviceNamestring.substring(1));
400      } else {
401        sb.append(withoutDeviceNamestring);
402      }
403    }
404   
405    return sb.toString();
406  }
407
408  LispObject getRootJar() {
409    LispObject jars = getJars();
410    if (!(jars instanceof Cons)) {
411      type_error("JAR-PATHNAME device is not a cons",
412                 jars, Symbol.CONS);
413      return (LispObject)UNREACHED;
414    }
415     
416    return jars.car();
417  }
418
419  String getRootJarAsURLString() {
420    return
421      JarPathname.JAR_URI_PREFIX
422      + ((URLPathname)getRootJar()).getNamestring()
423      + JarPathname.JAR_URI_SUFFIX;
424  }
425
426
427  LispObject getJars() {
428    return getDevice();
429  }
430
431  public static LispObject truename(Pathname pathname,
432                                    boolean errorIfDoesNotExist) {
433    if (!(pathname instanceof JarPathname)) {
434      return URLPathname.truename(pathname, errorIfDoesNotExist);
435    }
436    JarPathname p = new JarPathname();
437    p.copyFrom(pathname);
438
439    // Run truename resolution on the path of local jar archives
440    if (p.isLocalFile()) {
441      Pathname rootJar;
442      if (URLPathname.hasExplicitFile((Pathname)p.getRootJar())) {
443        rootJar = new URLPathname();
444      } else {
445        rootJar = new Pathname();
446      }
447      rootJar.copyFrom((Pathname)p.getRootJar());
448
449      // Ensure that we don't return a JarPathname if the current
450      // default is one when we resolve its TRUENAME.  Under Windows,
451      // the device will get filled in with the DOS drive letter if
452      // applicable.
453      if (rootJar.getDevice().equals(NIL)
454          && !Utilities.isPlatformWindows) {
455        rootJar.setDevice(Keyword.UNSPECIFIC);
456      }
457      LispObject rootJarTruename = Pathname.truename(rootJar, errorIfDoesNotExist);
458      if (rootJarTruename.equals(NIL)) {
459        return Pathname.doTruenameExit(rootJar, errorIfDoesNotExist);
460      }
461      LispObject otherJars = p.getJars().cdr();
462      URLPathname newRootJar;
463      if (rootJarTruename instanceof Pathname) {
464        newRootJar = URLPathname.createFromFile((Pathname)rootJarTruename);
465      } else {
466        newRootJar = (URLPathname) rootJarTruename;
467      }
468
469      p.setDevice(new Cons(newRootJar, otherJars));
470    }
471
472    if (!p.isArchiveEntry()) {
473      ZipCache.Archive archive = ZipCache.getArchive(p);
474      if (archive == null) {
475        return Pathname.doTruenameExit(pathname, errorIfDoesNotExist);
476      }
477      return p;
478    }
479
480    ZipEntry entry = ZipCache.getZipEntry(p);
481    if (entry == null) {
482      return Pathname.doTruenameExit(pathname, errorIfDoesNotExist);
483    }
484    return p;
485  }
486
487  public boolean isLocalFile() {
488    Pathname p = (Pathname) getRootJar();
489    if (p != null) {
490      return p.isLocalFile();
491    }
492    return false;
493  }
494
495  public boolean isArchiveEntry() {
496    return !(getDirectory().equals(NIL)
497             && getName().equals(NIL)
498             && getType().equals(NIL));
499  }
500
501  public JarPathname getArchive() {
502    if (!isArchiveEntry()) {
503      return (JarPathname)simple_error("Pathname already represents an archive.");
504    }
505    JarPathname archive = new JarPathname();
506    archive.copyFrom(this);
507    archive
508      .setDirectory(NIL)
509      .setName(NIL)
510      .setType(NIL);
511    return archive;
512  }
513
514  public LispObject classOf() {
515    return BuiltInClass.JAR_PATHNAME;
516  }
517
518  @Override
519  public LispObject typeOf() {
520    return Symbol.JAR_PATHNAME;
521  }
522
523  public InputStream getInputStream() {
524    // XXX We only return the bytes of an entry in a JAR
525    if (!isArchiveEntry()) {
526      simple_error("Can only get input stream for an entry in a JAR-PATHNAME.", this);
527    }
528    InputStream result = ZipCache.getEntryAsInputStream(this);
529    if (result == null) {
530      error(new FileError("Failed to get InputStream", this));
531    }
532    return result;
533  }
534
535  /** List the contents of a directory within a JAR archive */
536  static public LispObject listDirectory(JarPathname pathname) {
537    String directory = pathname.asEntryPath();
538    // We should only be listing directories
539    if (pathname.getDirectory() == NIL) {
540      return simple_error("Not a directory in a jar ~a", pathname);
541    }
542
543    if (directory.length() == 0) {
544      directory = "/*";
545    } else {
546      if (directory.endsWith("/")) {
547        directory = "/" + directory + "*";
548      } else {
549        directory = "/" + directory + "/*";
550      }
551    }
552
553    Pathname wildcard = (Pathname)Pathname.create(directory);
554
555    LispObject result = NIL;
556   
557    Iterator<Map.Entry<JarPathname,ZipEntry>> iterator = ZipCache.getEntriesIterator(pathname);
558    while (iterator.hasNext()) {
559      Map.Entry<JarPathname,ZipEntry> e = iterator.next();
560      JarPathname entry = e.getKey();
561      if (!Symbol.PATHNAME_MATCH_P.execute(entry, wildcard).equals(NIL)) {
562        result = result.push(entry);
563      }
564    }
565    return result.nreverse();
566  }
567
568  @DocString(name="match-wild-jar-pathname",
569             args="wild-jar-pathname",
570             returns="pathnames",
571  doc="Returns the pathnames matching WILD-JAR-PATHNAME which must be both wild and a JAR-PATHNAME")
572  static final Primitive MATCH_WILD_JAR_PATHNAME = new pf_match_wild_jar_pathname();
573
574  private static class pf_match_wild_jar_pathname extends Primitive {
575    pf_match_wild_jar_pathname() {
576      super(Symbol.MATCH_WILD_JAR_PATHNAME, "wild-jar-pathname");
577    }
578    @Override
579    public LispObject execute(LispObject arg) {
580      Pathname pathname = coerceToPathname(arg);
581      if (pathname instanceof LogicalPathname) {
582        pathname = LogicalPathname.translateLogicalPathname((LogicalPathname) pathname);
583      }
584      if (!pathname.isJar()) {
585        return new FileError("Not a jar pathname.", pathname);
586      }
587      if (!pathname.isWild()) {
588        return new FileError("Not a wild pathname.", pathname);
589      }
590
591      JarPathname jarPathname = new JarPathname();
592      jarPathname
593        .copyFrom(pathname)
594        .setDirectory(NIL)
595        .setName(NIL)
596        .setType(NIL);
597      JarPathname wildcard = (JarPathname)Symbol.TRUENAME.execute(jarPathname);
598      Iterator<Map.Entry<JarPathname,ZipEntry>> iterator
599        = ZipCache.getEntriesIterator(wildcard);
600      wildcard
601        .setDirectory(pathname.getDirectory())
602        .setName(pathname.getName())
603        .setType(pathname.getType());
604           
605      LispObject result = NIL;
606      while (iterator.hasNext()) {
607        Map.Entry<JarPathname,ZipEntry> e = iterator.next();
608        JarPathname entry = e.getKey();
609        LispObject matches
610          = Symbol.PATHNAME_MATCH_P.execute(entry, wildcard);
611         
612        if (!matches.equals(NIL)) {
613          result = new Cons(entry, result);
614        }
615      }
616
617      return result;
618    }
619  }
620
621  public long getLastModified() {
622    if (!isArchiveEntry()) {
623      ZipCache.Archive archive = ZipCache.getArchive(this);
624      if (archive != null) {
625        return archive.lastModified;
626      }
627    } else {
628      ZipEntry entry = ZipCache.getZipEntry(this);
629      if (entry != null) {
630        return entry.getTime();
631      }
632    }
633    return 0;
634  }
635
636  static JarPathname joinEntry(JarPathname root, Pathname entry) {
637    JarPathname result = new JarPathname();
638    result
639      .copyFrom(root)
640      .setDirectory(entry.getDirectory())
641      .setName(entry.getName())
642      .setType(entry.getType()); // ??? VERSION
643    return result;
644  }
645
646  static final Map<String, ?> emptyEnvironment =  new HashMap();
647 
648  public File getFile() {
649    String jarFile = ((Pathname)getRootJar()).getNamestring();
650    URI jarURI = URI.create(JAR_URI_PREFIX + jarFile);
651    // Path jarPath = Path.of(jarURI);
652    // Map<String, String> env = Map.of("create", "true");
653    FileSystem zipfs = null;
654    try {
655      zipfs = FileSystems.newFileSystem(jarURI, emptyEnvironment);
656    } catch (FileSystemAlreadyExistsException e0) {
657      zipfs = FileSystems.getFileSystem(jarURI);
658    } catch (IOException e1) {
659      error(new JavaException(e1));
660    }
661    String entryPath = getEntryPath().getNamestring();
662    String absoluteEntryPath = "/" + entryPath;
663    Path path = zipfs.getPath(absoluteEntryPath);
664    return path.toFile();
665  }
666}
Note: See TracBrowser for help on using the repository browser.