1 /*
   2  * Copyright (c) 2014, 2020, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 package jdk.internal.jimage;
  26 
  27 import java.io.IOException;
  28 import java.io.InputStream;
  29 import java.io.UncheckedIOException;
  30 import java.nio.ByteBuffer;
  31 import java.nio.ByteOrder;
  32 import java.nio.IntBuffer;
  33 import java.nio.file.Files;
  34 import java.nio.file.attribute.BasicFileAttributes;
  35 import java.nio.file.attribute.FileTime;
  36 import java.nio.file.Path;
  37 import java.util.ArrayList;
  38 import java.util.Collections;
  39 import java.util.HashMap;
  40 import java.util.HashSet;
  41 import java.util.List;
  42 import java.util.Map;
  43 import java.util.Objects;
  44 import java.util.Set;
  45 import java.util.function.Consumer;
  46 
  47 /**
  48  * @implNote This class needs to maintain JDK 8 source compatibility.
  49  *
  50  * It is used internally in the JDK to implement jimage/jrtfs access,
  51  * but also compiled and delivered as part of the jrtfs.jar to support access
  52  * to the jimage file provided by the shipped JDK by tools running on JDK 8.
  53  */
  54 public final class ImageReader implements AutoCloseable {
  55     private final SharedImageReader reader;
  56 
  57     private volatile boolean closed;
  58 
  59     private ImageReader(SharedImageReader reader) {
  60         this.reader = reader;
  61     }
  62 
  63     public static ImageReader open(Path imagePath, ByteOrder byteOrder) throws IOException {
  64         Objects.requireNonNull(imagePath);
  65         Objects.requireNonNull(byteOrder);
  66 
  67         return SharedImageReader.open(imagePath, byteOrder);
  68     }
  69 
  70     public static ImageReader open(Path imagePath) throws IOException {
  71         return open(imagePath, ByteOrder.nativeOrder());
  72     }
  73 
  74     @Override
  75     public void close() throws IOException {
  76         if (closed) {
  77             throw new IOException("image file already closed");
  78         }
  79         reader.close(this);
  80         closed = true;
  81     }
  82 
  83     private void ensureOpen() throws IOException {
  84         if (closed) {
  85             throw new IOException("image file closed");
  86         }
  87     }
  88 
  89     private void requireOpen() {
  90         if (closed) {
  91             throw new IllegalStateException("image file closed");
  92         }
  93     }
  94 
  95     // directory management interface
  96     public Directory getRootDirectory() throws IOException {
  97         ensureOpen();
  98         return reader.getRootDirectory();
  99     }
 100 
 101 
 102     public Node findNode(String name) throws IOException {
 103         ensureOpen();
 104         return reader.findNode(name);
 105     }
 106 
 107     public byte[] getResource(Node node) throws IOException {
 108         ensureOpen();
 109         return reader.getResource(node);
 110     }
 111 
 112     public byte[] getResource(Resource rs) throws IOException {
 113         ensureOpen();
 114         return reader.getResource(rs);
 115     }
 116 
 117     public ImageHeader getHeader() {
 118         requireOpen();
 119         return reader.getHeader();
 120     }
 121 
 122     public static void releaseByteBuffer(ByteBuffer buffer) {
 123         BasicImageReader.releaseByteBuffer(buffer);
 124     }
 125 
 126     public String getName() {
 127         requireOpen();
 128         return reader.getName();
 129     }
 130 
 131     public ByteOrder getByteOrder() {
 132         requireOpen();
 133         return reader.getByteOrder();
 134     }
 135 
 136     public Path getImagePath() {
 137         requireOpen();
 138         return reader.getImagePath();
 139     }
 140 
 141     public ImageStringsReader getStrings() {
 142         requireOpen();
 143         return reader.getStrings();
 144     }
 145 
 146     public ImageLocation findLocation(String mn, String rn) {
 147         requireOpen();
 148         return reader.findLocation(mn, rn);
 149     }
 150 
 151     public ImageLocation findLocation(String name) {
 152         requireOpen();
 153         return reader.findLocation(name);
 154     }
 155 
 156     public String[] getEntryNames() {
 157         requireOpen();
 158         return reader.getEntryNames();
 159     }
 160 
 161     public String[] getModuleNames() {
 162         requireOpen();
 163         int off = "/modules/".length();
 164         return reader.findNode("/modules")
 165                      .getChildren()
 166                      .stream()
 167                      .map(Node::getNameString)
 168                      .map(s -> s.substring(off, s.length()))
 169                      .toArray(String[]::new);
 170     }
 171 
 172     public long[] getAttributes(int offset) {
 173         requireOpen();
 174         return reader.getAttributes(offset);
 175     }
 176 
 177     public String getString(int offset) {
 178         requireOpen();
 179         return reader.getString(offset);
 180     }
 181 
 182     public byte[] getResource(String name) {
 183         requireOpen();
 184         return reader.getResource(name);
 185     }
 186 
 187     public byte[] getResource(ImageLocation loc) {
 188         requireOpen();
 189         return reader.getResource(loc);
 190     }
 191 
 192     public ByteBuffer getResourceBuffer(ImageLocation loc) {
 193         requireOpen();
 194         return reader.getResourceBuffer(loc);
 195     }
 196 
 197     public InputStream getResourceStream(ImageLocation loc) {
 198         requireOpen();
 199         return reader.getResourceStream(loc);
 200     }
 201 
 202     private final static class SharedImageReader extends BasicImageReader {
 203         static final int SIZE_OF_OFFSET = Integer.BYTES;
 204 
 205         static final Map<Path, SharedImageReader> OPEN_FILES = new HashMap<>();
 206 
 207         // List of openers for this shared image.
 208         final Set<ImageReader> openers;
 209 
 210         // attributes of the .jimage file. jimage file does not contain
 211         // attributes for the individual resources (yet). We use attributes
 212         // of the jimage file itself (creation, modification, access times).
 213         // Iniitalized lazily, see {@link #imageFileAttributes()}.
 214         BasicFileAttributes imageFileAttributes;
 215 
 216         // directory management implementation
 217         final HashMap<String, Node> nodes;
 218         volatile Directory rootDir;
 219 
 220         Directory packagesDir;
 221         Directory modulesDir;
 222 
 223         private SharedImageReader(Path imagePath, ByteOrder byteOrder) throws IOException {
 224             super(imagePath, byteOrder);
 225             this.openers = new HashSet<>();
 226             this.nodes = new HashMap<>();
 227         }
 228 
 229         public static ImageReader open(Path imagePath, ByteOrder byteOrder) throws IOException {
 230             Objects.requireNonNull(imagePath);
 231             Objects.requireNonNull(byteOrder);
 232 
 233             synchronized (OPEN_FILES) {
 234                 SharedImageReader reader = OPEN_FILES.get(imagePath);
 235 
 236                 if (reader == null) {
 237                     // Will fail with an IOException if wrong byteOrder.
 238                     reader =  new SharedImageReader(imagePath, byteOrder);
 239                     OPEN_FILES.put(imagePath, reader);
 240                 } else if (reader.getByteOrder() != byteOrder) {
 241                     throw new IOException("\"" + reader.getName() + "\" is not an image file");
 242                 }
 243 
 244                 ImageReader image = new ImageReader(reader);
 245                 reader.openers.add(image);
 246 
 247                 return image;
 248             }
 249         }
 250 
 251         public void close(ImageReader image) throws IOException {
 252             Objects.requireNonNull(image);
 253 
 254             synchronized (OPEN_FILES) {
 255                 if (!openers.remove(image)) {
 256                     throw new IOException("image file already closed");
 257                 }
 258 
 259                 if (openers.isEmpty()) {
 260                     close();
 261                     nodes.clear();
 262                     rootDir = null;
 263 
 264                     if (!OPEN_FILES.remove(this.getImagePath(), this)) {
 265                         throw new IOException("image file not found in open list");
 266                     }
 267                 }
 268             }
 269         }
 270 
 271         void addOpener(ImageReader reader) {
 272             synchronized (OPEN_FILES) {
 273                 openers.add(reader);
 274             }
 275         }
 276 
 277         boolean removeOpener(ImageReader reader) {
 278             synchronized (OPEN_FILES) {
 279                 return openers.remove(reader);
 280             }
 281         }
 282 
 283         // directory management interface
 284         Directory getRootDirectory() {
 285             return buildRootDirectory();
 286         }
 287 
 288         /**
 289          * Lazily build a node from a name.
 290          */
 291         synchronized Node buildNode(String name) {
 292             Node n;
 293             boolean isPackages = name.startsWith("/packages");
 294             boolean isModules = !isPackages && name.startsWith("/modules");
 295 
 296             if (!(isModules || isPackages)) {
 297                 return null;
 298             }
 299 
 300             ImageLocation loc = findLocation(name);
 301 
 302             if (loc != null) { // A sub tree node
 303                 if (isPackages) {
 304                     n = handlePackages(name, loc);
 305                 } else { // modules sub tree
 306                     n = handleModulesSubTree(name, loc);
 307                 }
 308             } else { // Asking for a resource? /modules/java.base/java/lang/Object.class
 309                 if (isModules) {
 310                     n = handleResource(name);
 311                 } else {
 312                     // Possibly ask for /packages/java.lang/java.base
 313                     // although /packages/java.base not created
 314                     n = handleModuleLink(name);
 315                 }
 316             }
 317             return n;
 318         }
 319 
 320         synchronized Directory buildRootDirectory() {
 321             Directory root = rootDir; // volatile read
 322             if (root != null) {
 323                 return root;
 324             }
 325 
 326             root = newDirectory(null, "/");
 327             root.setIsRootDir();
 328 
 329             // /packages dir
 330             packagesDir = newDirectory(root, "/packages");
 331             packagesDir.setIsPackagesDir();
 332 
 333             // /modules dir
 334             modulesDir = newDirectory(root, "/modules");
 335             modulesDir.setIsModulesDir();
 336 
 337             root.setCompleted(true);
 338             return rootDir = root;
 339         }
 340 
 341         /**
 342          * To visit sub tree resources.
 343          */
 344         interface LocationVisitor {
 345             void visit(ImageLocation loc);
 346         }
 347 
 348         void visitLocation(ImageLocation loc, LocationVisitor visitor) {
 349             byte[] offsets = getResource(loc);
 350             ByteBuffer buffer = ByteBuffer.wrap(offsets);
 351             buffer.order(getByteOrder());
 352             IntBuffer intBuffer = buffer.asIntBuffer();
 353             for (int i = 0; i < offsets.length / SIZE_OF_OFFSET; i++) {
 354                 int offset = intBuffer.get(i);
 355                 ImageLocation pkgLoc = getLocation(offset);
 356                 visitor.visit(pkgLoc);
 357             }
 358         }
 359 
 360         void visitPackageLocation(ImageLocation loc) {
 361             // Retrieve package name
 362             String pkgName = getBaseExt(loc);
 363             // Content is array of offsets in Strings table
 364             byte[] stringsOffsets = getResource(loc);
 365             ByteBuffer buffer = ByteBuffer.wrap(stringsOffsets);
 366             buffer.order(getByteOrder());
 367             IntBuffer intBuffer = buffer.asIntBuffer();
 368             // For each module, create a link node.
 369             for (int i = 0; i < stringsOffsets.length / SIZE_OF_OFFSET; i++) {
 370                 // skip empty state, useless.
 371                 intBuffer.get(i);
 372                 i++;
 373                 int offset = intBuffer.get(i);
 374                 String moduleName = getString(offset);
 375                 Node targetNode = findNode("/modules/" + moduleName);
 376                 if (targetNode != null) {
 377                     String pkgDirName = packagesDir.getName() + "/" + pkgName;
 378                     Directory pkgDir = (Directory) nodes.get(pkgDirName);
 379                     newLinkNode(pkgDir, pkgDir.getName() + "/" + moduleName, targetNode);
 380                 }
 381             }
 382         }
 383 
 384         Node handlePackages(String name, ImageLocation loc) {
 385             long size = loc.getUncompressedSize();
 386             Node n = null;
 387             // Only possiblities are /packages, /packages/package/module
 388             if (name.equals("/packages")) {
 389                 visitLocation(loc, (childloc) -> {
 390                     findNode(childloc.getFullName());
 391                 });
 392                 packagesDir.setCompleted(true);
 393                 n = packagesDir;
 394             } else {
 395                 if (size != 0) { // children are offsets to module in StringsTable
 396                     String pkgName = getBaseExt(loc);
 397                     Directory pkgDir = newDirectory(packagesDir, packagesDir.getName() + "/" + pkgName);
 398                     visitPackageLocation(loc);
 399                     pkgDir.setCompleted(true);
 400                     n = pkgDir;
 401                 } else { // Link to module
 402                     String pkgName = loc.getParent();
 403                     String modName = getBaseExt(loc);
 404                     Node targetNode = findNode("/modules/" + modName);
 405                     if (targetNode != null) {
 406                         String pkgDirName = packagesDir.getName() + "/" + pkgName;
 407                         Directory pkgDir = (Directory) nodes.get(pkgDirName);
 408                         Node linkNode = newLinkNode(pkgDir, pkgDir.getName() + "/" + modName, targetNode);
 409                         n = linkNode;
 410                     }
 411                 }
 412             }
 413             return n;
 414         }
 415 
 416         // Asking for /packages/package/module although
 417         // /packages/<pkg>/ not yet created, need to create it
 418         // prior to return the link to module node.
 419         Node handleModuleLink(String name) {
 420             // eg: unresolved /packages/package/module
 421             // Build /packages/package node
 422             Node ret = null;
 423             String radical = "/packages/";
 424             String path = name;
 425             if (path.startsWith(radical)) {
 426                 int start = radical.length();
 427                 int pkgEnd = path.indexOf('/', start);
 428                 if (pkgEnd != -1) {
 429                     String pkg = path.substring(start, pkgEnd);
 430                     String pkgPath = radical + pkg;
 431                     Node n = findNode(pkgPath);
 432                     // If not found means that this is a symbolic link such as:
 433                     // /packages/java.util/java.base/java/util/Vector.class
 434                     // and will be done by a retry of the filesystem
 435                     for (Node child : n.getChildren()) {
 436                         if (child.name.equals(name)) {
 437                             ret = child;
 438                             break;
 439                         }
 440                     }
 441                 }
 442             }
 443             return ret;
 444         }
 445 
 446         Node handleModulesSubTree(String name, ImageLocation loc) {
 447             Node n;
 448             assert (name.equals(loc.getFullName()));
 449             Directory dir = makeDirectories(name);
 450             visitLocation(loc, (childloc) -> {
 451                 String path = childloc.getFullName();
 452                 if (path.startsWith("/modules")) { // a package
 453                     makeDirectories(path);
 454                 } else { // a resource
 455                     makeDirectories(childloc.buildName(true, true, false));
 456                     newResource(dir, childloc);
 457                 }
 458             });
 459             dir.setCompleted(true);
 460             n = dir;
 461             return n;
 462         }
 463 
 464         Node handleResource(String name) {
 465             Node n = null;
 466             String locationPath = name.substring("/modules".length());
 467             ImageLocation resourceLoc = findLocation(locationPath);
 468             if (resourceLoc != null) {
 469                 Directory dir = makeDirectories(resourceLoc.buildName(true, true, false));
 470                 Resource res = newResource(dir, resourceLoc);
 471                 n = res;
 472             }
 473             return n;
 474         }
 475 
 476         String getBaseExt(ImageLocation loc) {
 477             String base = loc.getBase();
 478             String ext = loc.getExtension();
 479             if (ext != null && !ext.isEmpty()) {
 480                 base = base + "." + ext;
 481             }
 482             return base;
 483         }
 484 
 485         synchronized Node findNode(String name) {
 486             buildRootDirectory();
 487             Node n = nodes.get(name);
 488             if (n == null || !n.isCompleted()) {
 489                 n = buildNode(name);
 490             }
 491             return n;
 492         }
 493 
 494         /**
 495          * Returns the file attributes of the image file.
 496          */
 497         BasicFileAttributes imageFileAttributes() {
 498             BasicFileAttributes attrs = imageFileAttributes;
 499             if (attrs == null) {
 500                 try {
 501                     Path file = getImagePath();
 502                     attrs = Files.readAttributes(file, BasicFileAttributes.class);
 503                 } catch (IOException ioe) {
 504                     throw new UncheckedIOException(ioe);
 505                 }
 506                 imageFileAttributes = attrs;
 507             }
 508             return attrs;
 509         }
 510 
 511         Directory newDirectory(Directory parent, String name) {
 512             Directory dir = Directory.create(parent, name, imageFileAttributes());
 513             nodes.put(dir.getName(), dir);
 514             return dir;
 515         }
 516 
 517         Resource newResource(Directory parent, ImageLocation loc) {
 518             Resource res = Resource.create(parent, loc, imageFileAttributes());
 519             nodes.put(res.getName(), res);
 520             return res;
 521         }
 522 
 523         LinkNode newLinkNode(Directory dir, String name, Node link) {
 524             LinkNode linkNode = LinkNode.create(dir, name, link);
 525             nodes.put(linkNode.getName(), linkNode);
 526             return linkNode;
 527         }
 528 
 529         Directory makeDirectories(String parent) {
 530             Directory last = rootDir;
 531             for (int offset = parent.indexOf('/', 1);
 532                     offset != -1;
 533                     offset = parent.indexOf('/', offset + 1)) {
 534                 String dir = parent.substring(0, offset);
 535                 last = makeDirectory(dir, last);
 536             }
 537             return makeDirectory(parent, last);
 538 
 539         }
 540 
 541         Directory makeDirectory(String dir, Directory last) {
 542             Directory nextDir = (Directory) nodes.get(dir);
 543             if (nextDir == null) {
 544                 nextDir = newDirectory(last, dir);
 545             }
 546             return nextDir;
 547         }
 548 
 549         byte[] getResource(Node node) throws IOException {
 550             if (node.isResource()) {
 551                 return super.getResource(node.getLocation());
 552             }
 553             throw new IOException("Not a resource: " + node);
 554         }
 555 
 556         byte[] getResource(Resource rs) throws IOException {
 557             return super.getResource(rs.getLocation());
 558         }
 559     }
 560 
 561     // jimage file does not store directory structure. We build nodes
 562     // using the "path" strings found in the jimage file.
 563     // Node can be a directory or a resource
 564     public abstract static class Node {
 565         private static final int ROOT_DIR = 0b0000_0000_0000_0001;
 566         private static final int PACKAGES_DIR = 0b0000_0000_0000_0010;
 567         private static final int MODULES_DIR = 0b0000_0000_0000_0100;
 568 
 569         private int flags;
 570         private final String name;
 571         private final BasicFileAttributes fileAttrs;
 572         private boolean completed;
 573 
 574         protected Node(String name, BasicFileAttributes fileAttrs) {
 575             this.name = Objects.requireNonNull(name);
 576             this.fileAttrs = Objects.requireNonNull(fileAttrs);
 577         }
 578 
 579         /**
 580          * A node is completed when all its direct children have been built.
 581          *
 582          * @return
 583          */
 584         public boolean isCompleted() {
 585             return completed;
 586         }
 587 
 588         public void setCompleted(boolean completed) {
 589             this.completed = completed;
 590         }
 591 
 592         public final void setIsRootDir() {
 593             flags |= ROOT_DIR;
 594         }
 595 
 596         public final boolean isRootDir() {
 597             return (flags & ROOT_DIR) != 0;
 598         }
 599 
 600         public final void setIsPackagesDir() {
 601             flags |= PACKAGES_DIR;
 602         }
 603 
 604         public final boolean isPackagesDir() {
 605             return (flags & PACKAGES_DIR) != 0;
 606         }
 607 
 608         public final void setIsModulesDir() {
 609             flags |= MODULES_DIR;
 610         }
 611 
 612         public final boolean isModulesDir() {
 613             return (flags & MODULES_DIR) != 0;
 614         }
 615 
 616         public final String getName() {
 617             return name;
 618         }
 619 
 620         public final BasicFileAttributes getFileAttributes() {
 621             return fileAttrs;
 622         }
 623 
 624         // resolve this Node (if this is a soft link, get underlying Node)
 625         public final Node resolveLink() {
 626             return resolveLink(false);
 627         }
 628 
 629         public Node resolveLink(boolean recursive) {
 630             return this;
 631         }
 632 
 633         // is this a soft link Node?
 634         public boolean isLink() {
 635             return false;
 636         }
 637 
 638         public boolean isDirectory() {
 639             return false;
 640         }
 641 
 642         public List<Node> getChildren() {
 643             throw new IllegalArgumentException("not a directory: " + getNameString());
 644         }
 645 
 646         public boolean isResource() {
 647             return false;
 648         }
 649 
 650         public ImageLocation getLocation() {
 651             throw new IllegalArgumentException("not a resource: " + getNameString());
 652         }
 653 
 654         public long size() {
 655             return 0L;
 656         }
 657 
 658         public long compressedSize() {
 659             return 0L;
 660         }
 661 
 662         public String extension() {
 663             return null;
 664         }
 665 
 666         public long contentOffset() {
 667             return 0L;
 668         }
 669 
 670         public final FileTime creationTime() {
 671             return fileAttrs.creationTime();
 672         }
 673 
 674         public final FileTime lastAccessTime() {
 675             return fileAttrs.lastAccessTime();
 676         }
 677 
 678         public final FileTime lastModifiedTime() {
 679             return fileAttrs.lastModifiedTime();
 680         }
 681 
 682         public final String getNameString() {
 683             return name;
 684         }
 685 
 686         @Override
 687         public final String toString() {
 688             return getNameString();
 689         }
 690 
 691         @Override
 692         public final int hashCode() {
 693             return name.hashCode();
 694         }
 695 
 696         @Override
 697         public final boolean equals(Object other) {
 698             if (this == other) {
 699                 return true;
 700             }
 701 
 702             if (other instanceof Node) {
 703                 return name.equals(((Node) other).name);
 704             }
 705 
 706             return false;
 707         }
 708     }
 709 
 710     // directory node - directory has full path name without '/' at end.
 711     static final class Directory extends Node {
 712         private final List<Node> children;
 713 
 714         private Directory(String name, BasicFileAttributes fileAttrs) {
 715             super(name, fileAttrs);
 716             children = new ArrayList<>();
 717         }
 718 
 719         static Directory create(Directory parent, String name, BasicFileAttributes fileAttrs) {
 720             Directory d = new Directory(name, fileAttrs);
 721             if (parent != null) {
 722                 parent.addChild(d);
 723             }
 724             return d;
 725         }
 726 
 727         @Override
 728         public boolean isDirectory() {
 729             return true;
 730         }
 731 
 732         @Override
 733         public List<Node> getChildren() {
 734             return Collections.unmodifiableList(children);
 735         }
 736 
 737         void addChild(Node node) {
 738             children.add(node);
 739         }
 740 
 741         public void walk(Consumer<? super Node> consumer) {
 742             consumer.accept(this);
 743             for ( Node child : children ) {
 744                 if (child.isDirectory()) {
 745                     ((Directory)child).walk(consumer);
 746                 } else {
 747                     consumer.accept(child);
 748                 }
 749             }
 750         }
 751     }
 752 
 753     // "resource" is .class or any other resource (compressed/uncompressed) in a jimage.
 754     // full path of the resource is the "name" of the resource.
 755     static class Resource extends Node {
 756         private final ImageLocation loc;
 757 
 758         private Resource(ImageLocation loc, BasicFileAttributes fileAttrs) {
 759             super(loc.getFullName(true), fileAttrs);
 760             this.loc = loc;
 761         }
 762 
 763         static Resource create(Directory parent, ImageLocation loc, BasicFileAttributes fileAttrs) {
 764             Resource rs = new Resource(loc, fileAttrs);
 765             parent.addChild(rs);
 766             return rs;
 767         }
 768 
 769         @Override
 770         public boolean isCompleted() {
 771             return true;
 772         }
 773 
 774         @Override
 775         public boolean isResource() {
 776             return true;
 777         }
 778 
 779         @Override
 780         public ImageLocation getLocation() {
 781             return loc;
 782         }
 783 
 784         @Override
 785         public long size() {
 786             return loc.getUncompressedSize();
 787         }
 788 
 789         @Override
 790         public long compressedSize() {
 791             return loc.getCompressedSize();
 792         }
 793 
 794         @Override
 795         public String extension() {
 796             return loc.getExtension();
 797         }
 798 
 799         @Override
 800         public long contentOffset() {
 801             return loc.getContentOffset();
 802         }
 803     }
 804 
 805     // represents a soft link to another Node
 806     static class LinkNode extends Node {
 807         private final Node link;
 808 
 809         private LinkNode(String name, Node link) {
 810             super(name, link.getFileAttributes());
 811             this.link = link;
 812         }
 813 
 814         static LinkNode create(Directory parent, String name, Node link) {
 815             LinkNode ln = new LinkNode(name, link);
 816             parent.addChild(ln);
 817             return ln;
 818         }
 819 
 820         @Override
 821         public boolean isCompleted() {
 822             return true;
 823         }
 824 
 825         @Override
 826         public Node resolveLink(boolean recursive) {
 827             return (recursive && link instanceof LinkNode) ? ((LinkNode)link).resolveLink(true) : link;
 828         }
 829 
 830         @Override
 831         public boolean isLink() {
 832             return true;
 833         }
 834     }
 835 }