aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/build/lib/vfs/ZipFileSystem.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/google/devtools/build/lib/vfs/ZipFileSystem.java')
-rw-r--r--src/main/java/com/google/devtools/build/lib/vfs/ZipFileSystem.java253
1 files changed, 253 insertions, 0 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/ZipFileSystem.java b/src/main/java/com/google/devtools/build/lib/vfs/ZipFileSystem.java
new file mode 100644
index 0000000000..558263d178
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/vfs/ZipFileSystem.java
@@ -0,0 +1,253 @@
+// Copyright 2014 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.devtools.build.lib.vfs;
+
+import com.google.common.base.Predicate;
+import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+/**
+ * A FileSystem that provides a read-only filesystem view on a zip file.
+ * Inherits the constraints imposed by ReadonlyFileSystem.
+ */
+@ThreadSafe
+public class ZipFileSystem extends ReadonlyFileSystem {
+
+ private final ZipFile zipFile;
+
+ /**
+ * The sole purpose of this field is to hold a strong reference to all leaf
+ * {@link Path}s which have a non-null "entry" field, preventing them from
+ * being garbage-collected. (The leaf paths hold string references to their
+ * parents, so we don't need to include them here.)
+ *
+ * <p>This is necessary because {@link Path}s may be recycled when they
+ * become unreachable, but the ZipFileSystem uses them to hold the {@link
+ * ZipEntry} for that path, if any. Without this additional strong
+ * reference, ZipEntries would seem to "disappear" during garbage collection.
+ */
+ @SuppressWarnings("unused")
+ private final Object paths;
+
+ /**
+ * Constructs a ZipFileSystem from a zip file identified with a given path.
+ */
+ public ZipFileSystem(Path zipPath) throws IOException {
+ // Throw some more specific exceptions than ZipFile does.
+ // We do this using File instead of Path, in case zipPath points to an
+ // InMemoryFileSystem. This case is not really supported but
+ // can occur in tests.
+ File file = zipPath.getPathFile();
+ if (!file.exists()) {
+ throw new FileNotFoundException(String.format("File '%s' does not exist", zipPath));
+ }
+ if (!file.isFile()) {
+ throw new IOException(String.format("'%s' is not a file", zipPath));
+ }
+ if (!file.canRead()) {
+ throw new IOException(String.format("File '%s' is not readable", zipPath));
+ }
+
+ this.zipFile = new ZipFile(file);
+ this.paths = populatePathTree();
+ }
+
+ // ZipPath extends Path with a set-once ZipEntry field.
+ // TODO(bazel-team): (2009) Delete class ZipPath, and perform the
+ // Path-to-ZipEntry lookup in {@link #zipEntry} and {@link
+ // #getDirectoryEntries}. Then this field becomes redundant.
+ @ThreadSafe
+ private static class ZipPath extends Path {
+ /**
+ * Non-null iff this file/directory exists. Set by setZipEntry for files
+ * explicitly mentioned in the zipfile's table of contents, or implicitly
+ * an ancestor of them.
+ */
+ ZipEntry entry = null;
+
+ // Root path.
+ ZipPath(ZipFileSystem fileSystem) {
+ super(fileSystem);
+ }
+
+ // Non-root paths.
+ ZipPath(ZipFileSystem fileSystem, String name, ZipPath parent) {
+ super(fileSystem, name, parent);
+ }
+
+ void setZipEntry(ZipEntry entry) {
+ if (this.entry != null) {
+ throw new IllegalStateException("setZipEntry(" + entry
+ + ") called twice!");
+ }
+ this.entry = entry;
+
+ // Ensure all parents of this path have a directory ZipEntry:
+ for (ZipPath path = (ZipPath) getParentDirectory();
+ path != null && path.entry == null;
+ path = (ZipPath) path.getParentDirectory()) {
+ // Note, the ZipEntry for the root path is called "//", but that's ok.
+ path.setZipEntry(new ZipEntry(path + "/")); // trailing "/" => isDir
+ }
+ }
+
+ @Override
+ protected ZipPath createChildPath(String childName) {
+ return new ZipPath((ZipFileSystem) getFileSystem(), childName, this);
+ }
+ }
+
+ /**
+ * Scans the Zip file and associates a ZipEntry with each filename
+ * (ZipPath) that is mentioned in the table of contents. Returns a
+ * collection of all corresponding Paths.
+ */
+ private Collection<Path> populatePathTree() {
+ Collection<Path> paths = new ArrayList<>();
+ for (ZipEntry entry : Collections.list(zipFile.entries())) {
+ PathFragment frag = new PathFragment(entry.getName());
+ Path path = rootPath.getRelative(frag);
+ paths.add(path);
+ ((ZipPath) path).setZipEntry(entry);
+ }
+ return paths;
+ }
+
+ @Override
+ public String getFileSystemType(Path path) {
+ return "zipfs";
+ }
+
+ @Override
+ protected Path createRootPath() {
+ return new ZipPath(this);
+ }
+
+ /** Returns the ZipEntry associated with a given path name, if any. */
+ private static ZipEntry zipEntry(Path path) {
+ return ((ZipPath) path).entry;
+ }
+
+ /** Like zipEntry, but throws FileNotFoundException unless path exists. */
+ private static ZipEntry zipEntryNonNull(Path path)
+ throws FileNotFoundException {
+ ZipEntry zipEntry = zipEntry(path);
+ if (zipEntry == null) {
+ throw new FileNotFoundException(path + " (No such file or directory)");
+ }
+ return zipEntry;
+ }
+
+ @Override
+ protected InputStream getInputStream(Path path) throws IOException {
+ return zipFile.getInputStream(zipEntryNonNull(path));
+ }
+
+ @Override
+ protected Collection<Path> getDirectoryEntries(Path path)
+ throws IOException {
+ zipEntryNonNull(path);
+ final Collection<Path> result = new ArrayList<>();
+ ((ZipPath) path).applyToChildren(new Predicate<Path>() {
+ @Override
+ public boolean apply(Path child) {
+ if (zipEntry(child) != null) {
+ result.add(child);
+ }
+ return true;
+ }
+ });
+ return result;
+ }
+
+ @Override
+ protected boolean exists(Path path, boolean followSymlinks) {
+ return zipEntry(path) != null;
+ }
+
+ @Override
+ protected boolean isDirectory(Path path, boolean followSymlinks) {
+ ZipEntry entry = zipEntry(path);
+ return entry != null && entry.isDirectory();
+ }
+
+ @Override
+ protected boolean isFile(Path path, boolean followSymlinks) {
+ ZipEntry entry = zipEntry(path);
+ return entry != null && !entry.isDirectory();
+ }
+
+ @Override
+ protected boolean isReadable(Path path) throws IOException {
+ zipEntryNonNull(path);
+ return true;
+ }
+
+ @Override
+ protected boolean isWritable(Path path) throws IOException {
+ zipEntryNonNull(path);
+ return false;
+ }
+
+ @Override
+ protected boolean isExecutable(Path path) throws IOException {
+ zipEntryNonNull(path);
+ return false;
+ }
+
+ @Override
+ protected PathFragment readSymbolicLink(Path path) throws IOException {
+ zipEntryNonNull(path);
+ throw new NotASymlinkException(path);
+ }
+
+ @Override
+ protected long getFileSize(Path path, boolean followSymlinks)
+ throws IOException {
+ return zipEntryNonNull(path).getSize();
+ }
+
+ @Override
+ protected long getLastModifiedTime(Path path, boolean followSymlinks)
+ throws FileNotFoundException {
+ return zipEntryNonNull(path).getTime();
+ }
+
+ @Override
+ protected boolean isSymbolicLink(Path path) {
+ return false;
+ }
+
+ @Override
+ protected FileStatus statIfFound(Path path, boolean followSymlinks) {
+ try {
+ return stat(path, followSymlinks);
+ } catch (FileNotFoundException e) {
+ return null;
+ } catch (IOException e) {
+ // getLastModifiedTime can only throw FileNotFoundException, which is what stat uses.
+ throw new IllegalStateException (e);
+ }
+ }
+
+}