blob: 4830d8fe19e1b2e188c4e9c1f384204e44ec0af6 [file] [log] [blame]
// Copyright 2014 The Bazel Authors. 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 com.google.devtools.build.lib.util.Preconditions;
import java.io.Closeable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
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 implements Closeable {
private final File tempFile; // In case this needs to be written to the file system
private final ZipFile zipFile;
private boolean open;
/**
* 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 {
if (!zipPath.exists()) {
throw new FileNotFoundException(String.format("File '%s' does not exist", zipPath));
}
File file = zipPath.getPathFile();
if (!file.exists()) {
// If the File says that it does not exist but the Path says that it does, we are probably
// dealing with a FileSystem that does not represent the actual file system we are running
// under. Then we copy the Path into a temporary File.
tempFile = File.createTempFile("bazel.test.", ".tmp");
file = tempFile;
byte[] contents = FileSystemUtils.readContent(zipPath);
try (OutputStream os = new FileOutputStream(tempFile)) {
os.write(contents);
}
} else {
tempFile = null;
}
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();
this.open = true;
}
// 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 {
Preconditions.checkState(open);
return zipFile.getInputStream(zipEntryNonNull(path));
}
@Override
protected Collection<Path> getDirectoryEntries(Path path)
throws IOException {
Preconditions.checkState(open);
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) {
Preconditions.checkState(open);
return zipEntry(path) != null;
}
@Override
protected boolean isDirectory(Path path, boolean followSymlinks) {
Preconditions.checkState(open);
ZipEntry entry = zipEntry(path);
return entry != null && entry.isDirectory();
}
@Override
protected boolean isFile(Path path, boolean followSymlinks) {
Preconditions.checkState(open);
ZipEntry entry = zipEntry(path);
return entry != null && !entry.isDirectory();
}
@Override
protected boolean isSpecialFile(Path path, boolean followSymlinks) {
Preconditions.checkState(open);
return false;
}
@Override
protected boolean isReadable(Path path) throws IOException {
Preconditions.checkState(open);
zipEntryNonNull(path);
return true;
}
@Override
protected boolean isWritable(Path path) throws IOException {
Preconditions.checkState(open);
zipEntryNonNull(path);
return false;
}
@Override
protected boolean isExecutable(Path path) throws IOException {
Preconditions.checkState(open);
zipEntryNonNull(path);
return false;
}
@Override
protected PathFragment readSymbolicLink(Path path) throws IOException {
Preconditions.checkState(open);
zipEntryNonNull(path);
throw new NotASymlinkException(path);
}
@Override
protected long getFileSize(Path path, boolean followSymlinks)
throws IOException {
Preconditions.checkState(open);
return zipEntryNonNull(path).getSize();
}
@Override
protected long getLastModifiedTime(Path path, boolean followSymlinks)
throws FileNotFoundException {
Preconditions.checkState(open);
return zipEntryNonNull(path).getTime();
}
@Override
protected boolean isSymbolicLink(Path path) {
Preconditions.checkState(open);
return false;
}
@Override
protected FileStatus statIfFound(Path path, boolean followSymlinks) {
Preconditions.checkState(open);
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);
}
}
@Override
public void close() {
if (open) {
close();
if (tempFile != null) {
tempFile.delete();
}
open = false;
}
}
}