blob: aa9ad1ae29993671a1ec5ef8d3449380b477145e [file] [log] [blame]
Damien Martin-Guillerezf88f4d82015-09-25 13:56:55 +00001// Copyright 2014 The Bazel Authors. All rights reserved.
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +01002//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14package com.google.devtools.build.lib.vfs;
15
lberki3edde6f2017-07-25 15:36:23 +020016import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadCompatible;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010017import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
Lukacs Berkic0e5bc52016-04-06 12:31:07 +000018import com.google.devtools.build.lib.util.Preconditions;
Laszlo Csomorca99bb72016-10-25 13:15:55 +000019import com.google.devtools.build.lib.vfs.Path.PathFactory;
Lukacs Berkic0e5bc52016-04-06 12:31:07 +000020import java.io.Closeable;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010021import java.io.File;
22import java.io.FileNotFoundException;
Lukacs Berkic0e5bc52016-04-06 12:31:07 +000023import java.io.FileOutputStream;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010024import java.io.IOException;
25import java.io.InputStream;
Lukacs Berkic0e5bc52016-04-06 12:31:07 +000026import java.io.OutputStream;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010027import java.util.ArrayList;
28import java.util.Collection;
29import java.util.Collections;
lberki3edde6f2017-07-25 15:36:23 +020030import java.util.logging.Logger;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010031import java.util.zip.ZipEntry;
32import java.util.zip.ZipFile;
33
34/**
35 * A FileSystem that provides a read-only filesystem view on a zip file.
36 * Inherits the constraints imposed by ReadonlyFileSystem.
37 */
lberki3edde6f2017-07-25 15:36:23 +020038@ThreadCompatible // Can only be accessed from one thread at a time (including its Path objects)
Lukacs Berkic0e5bc52016-04-06 12:31:07 +000039public class ZipFileSystem extends ReadonlyFileSystem implements Closeable {
lberki3edde6f2017-07-25 15:36:23 +020040 private static final Logger log = Logger.getLogger(ZipFileSystem.class.getName());
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010041
Lukacs Berkic0e5bc52016-04-06 12:31:07 +000042 private final File tempFile; // In case this needs to be written to the file system
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010043 private final ZipFile zipFile;
Lukacs Berkic0e5bc52016-04-06 12:31:07 +000044 private boolean open;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010045
46 /**
47 * The sole purpose of this field is to hold a strong reference to all leaf
48 * {@link Path}s which have a non-null "entry" field, preventing them from
49 * being garbage-collected. (The leaf paths hold string references to their
50 * parents, so we don't need to include them here.)
51 *
52 * <p>This is necessary because {@link Path}s may be recycled when they
53 * become unreachable, but the ZipFileSystem uses them to hold the {@link
54 * ZipEntry} for that path, if any. Without this additional strong
55 * reference, ZipEntries would seem to "disappear" during garbage collection.
56 */
57 @SuppressWarnings("unused")
58 private final Object paths;
59
60 /**
61 * Constructs a ZipFileSystem from a zip file identified with a given path.
62 */
63 public ZipFileSystem(Path zipPath) throws IOException {
Lukacs Berkic0e5bc52016-04-06 12:31:07 +000064 if (!zipPath.exists()) {
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010065 throw new FileNotFoundException(String.format("File '%s' does not exist", zipPath));
66 }
Lukacs Berkic0e5bc52016-04-06 12:31:07 +000067
68 File file = zipPath.getPathFile();
69 if (!file.exists()) {
70 // If the File says that it does not exist but the Path says that it does, we are probably
71 // dealing with a FileSystem that does not represent the actual file system we are running
72 // under. Then we copy the Path into a temporary File.
73 tempFile = File.createTempFile("bazel.test.", ".tmp");
74 file = tempFile;
75 byte[] contents = FileSystemUtils.readContent(zipPath);
76 try (OutputStream os = new FileOutputStream(tempFile)) {
77 os.write(contents);
78 }
79 } else {
80 tempFile = null;
81 }
82
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010083 if (!file.isFile()) {
84 throw new IOException(String.format("'%s' is not a file", zipPath));
85 }
86 if (!file.canRead()) {
87 throw new IOException(String.format("File '%s' is not readable", zipPath));
88 }
89
90 this.zipFile = new ZipFile(file);
91 this.paths = populatePathTree();
Lukacs Berkic0e5bc52016-04-06 12:31:07 +000092 this.open = true;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +010093 }
94
95 // ZipPath extends Path with a set-once ZipEntry field.
96 // TODO(bazel-team): (2009) Delete class ZipPath, and perform the
97 // Path-to-ZipEntry lookup in {@link #zipEntry} and {@link
98 // #getDirectoryEntries}. Then this field becomes redundant.
99 @ThreadSafe
100 private static class ZipPath extends Path {
Laszlo Csomorca99bb72016-10-25 13:15:55 +0000101
102 private enum Factory implements PathFactory {
103 INSTANCE {
104 @Override
105 public Path createRootPath(FileSystem filesystem) {
106 Preconditions.checkArgument(filesystem instanceof ZipFileSystem);
107 return new ZipPath((ZipFileSystem) filesystem);
108 }
109
110 @Override
111 public Path createChildPath(Path parent, String childName) {
112 Preconditions.checkState(parent instanceof ZipPath);
113 return new ZipPath((ZipFileSystem) parent.getFileSystem(), childName, (ZipPath) parent);
114 }
115
116 @Override
nharmata39e659e2017-04-04 14:42:23 +0000117 public Path getCachedChildPathInternal(Path path, String childName) {
118 return Path.getCachedChildPathInternal(path, childName, /*cacheable=*/ true);
Laszlo Csomorca99bb72016-10-25 13:15:55 +0000119 }
120 };
121 }
122
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100123 /**
124 * Non-null iff this file/directory exists. Set by setZipEntry for files
125 * explicitly mentioned in the zipfile's table of contents, or implicitly
126 * an ancestor of them.
127 */
128 ZipEntry entry = null;
129
130 // Root path.
Laszlo Csomorca99bb72016-10-25 13:15:55 +0000131 private ZipPath(ZipFileSystem fileSystem) {
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100132 super(fileSystem);
133 }
134
135 // Non-root paths.
Laszlo Csomorca99bb72016-10-25 13:15:55 +0000136 private ZipPath(ZipFileSystem fileSystem, String name, ZipPath parent) {
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100137 super(fileSystem, name, parent);
138 }
139
140 void setZipEntry(ZipEntry entry) {
141 if (this.entry != null) {
142 throw new IllegalStateException("setZipEntry(" + entry
143 + ") called twice!");
144 }
145 this.entry = entry;
146
147 // Ensure all parents of this path have a directory ZipEntry:
148 for (ZipPath path = (ZipPath) getParentDirectory();
149 path != null && path.entry == null;
150 path = (ZipPath) path.getParentDirectory()) {
151 // Note, the ZipEntry for the root path is called "//", but that's ok.
152 path.setZipEntry(new ZipEntry(path + "/")); // trailing "/" => isDir
153 }
154 }
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100155 }
156
157 /**
158 * Scans the Zip file and associates a ZipEntry with each filename
159 * (ZipPath) that is mentioned in the table of contents. Returns a
160 * collection of all corresponding Paths.
161 */
162 private Collection<Path> populatePathTree() {
163 Collection<Path> paths = new ArrayList<>();
164 for (ZipEntry entry : Collections.list(zipFile.entries())) {
nharmatab4060b62017-04-04 17:11:39 +0000165 PathFragment frag = PathFragment.create(entry.getName());
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100166 Path path = rootPath.getRelative(frag);
167 paths.add(path);
168 ((ZipPath) path).setZipEntry(entry);
169 }
170 return paths;
171 }
172
173 @Override
174 public String getFileSystemType(Path path) {
175 return "zipfs";
176 }
177
178 @Override
Laszlo Csomorca99bb72016-10-25 13:15:55 +0000179 protected PathFactory getPathFactory() {
180 return ZipPath.Factory.INSTANCE;
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100181 }
182
183 /** Returns the ZipEntry associated with a given path name, if any. */
184 private static ZipEntry zipEntry(Path path) {
185 return ((ZipPath) path).entry;
186 }
187
188 /** Like zipEntry, but throws FileNotFoundException unless path exists. */
189 private static ZipEntry zipEntryNonNull(Path path)
190 throws FileNotFoundException {
191 ZipEntry zipEntry = zipEntry(path);
192 if (zipEntry == null) {
193 throw new FileNotFoundException(path + " (No such file or directory)");
194 }
195 return zipEntry;
196 }
197
198 @Override
199 protected InputStream getInputStream(Path path) throws IOException {
Lukacs Berkic0e5bc52016-04-06 12:31:07 +0000200 Preconditions.checkState(open);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100201 return zipFile.getInputStream(zipEntryNonNull(path));
202 }
203
204 @Override
205 protected Collection<Path> getDirectoryEntries(Path path)
206 throws IOException {
Lukacs Berkic0e5bc52016-04-06 12:31:07 +0000207 Preconditions.checkState(open);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100208 zipEntryNonNull(path);
209 final Collection<Path> result = new ArrayList<>();
laurentlb3d2a68c2017-06-30 00:32:04 +0200210 ((ZipPath) path)
211 .applyToChildren(
212 child -> {
213 if (zipEntry(child) != null) {
214 result.add(child);
215 }
216 return true;
217 });
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100218 return result;
219 }
220
221 @Override
222 protected boolean exists(Path path, boolean followSymlinks) {
Lukacs Berkic0e5bc52016-04-06 12:31:07 +0000223 Preconditions.checkState(open);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100224 return zipEntry(path) != null;
225 }
226
227 @Override
228 protected boolean isDirectory(Path path, boolean followSymlinks) {
Lukacs Berkic0e5bc52016-04-06 12:31:07 +0000229 Preconditions.checkState(open);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100230 ZipEntry entry = zipEntry(path);
231 return entry != null && entry.isDirectory();
232 }
233
234 @Override
235 protected boolean isFile(Path path, boolean followSymlinks) {
Lukacs Berkic0e5bc52016-04-06 12:31:07 +0000236 Preconditions.checkState(open);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100237 ZipEntry entry = zipEntry(path);
238 return entry != null && !entry.isDirectory();
239 }
240
241 @Override
Nathan Harmatad8b6ff22015-10-20 21:54:34 +0000242 protected boolean isSpecialFile(Path path, boolean followSymlinks) {
Lukacs Berkic0e5bc52016-04-06 12:31:07 +0000243 Preconditions.checkState(open);
Nathan Harmatad8b6ff22015-10-20 21:54:34 +0000244 return false;
245 }
246
247 @Override
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100248 protected boolean isReadable(Path path) throws IOException {
Lukacs Berkic0e5bc52016-04-06 12:31:07 +0000249 Preconditions.checkState(open);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100250 zipEntryNonNull(path);
251 return true;
252 }
253
254 @Override
255 protected boolean isWritable(Path path) throws IOException {
Lukacs Berkic0e5bc52016-04-06 12:31:07 +0000256 Preconditions.checkState(open);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100257 zipEntryNonNull(path);
258 return false;
259 }
260
261 @Override
262 protected boolean isExecutable(Path path) throws IOException {
Lukacs Berkic0e5bc52016-04-06 12:31:07 +0000263 Preconditions.checkState(open);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100264 zipEntryNonNull(path);
265 return false;
266 }
267
268 @Override
269 protected PathFragment readSymbolicLink(Path path) throws IOException {
Lukacs Berkic0e5bc52016-04-06 12:31:07 +0000270 Preconditions.checkState(open);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100271 zipEntryNonNull(path);
272 throw new NotASymlinkException(path);
273 }
274
275 @Override
276 protected long getFileSize(Path path, boolean followSymlinks)
277 throws IOException {
Lukacs Berkic0e5bc52016-04-06 12:31:07 +0000278 Preconditions.checkState(open);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100279 return zipEntryNonNull(path).getSize();
280 }
281
282 @Override
283 protected long getLastModifiedTime(Path path, boolean followSymlinks)
284 throws FileNotFoundException {
Lukacs Berkic0e5bc52016-04-06 12:31:07 +0000285 Preconditions.checkState(open);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100286 return zipEntryNonNull(path).getTime();
287 }
288
289 @Override
290 protected boolean isSymbolicLink(Path path) {
Lukacs Berkic0e5bc52016-04-06 12:31:07 +0000291 Preconditions.checkState(open);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100292 return false;
293 }
294
295 @Override
296 protected FileStatus statIfFound(Path path, boolean followSymlinks) {
Lukacs Berkic0e5bc52016-04-06 12:31:07 +0000297 Preconditions.checkState(open);
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100298 try {
299 return stat(path, followSymlinks);
300 } catch (FileNotFoundException e) {
301 return null;
302 } catch (IOException e) {
303 // getLastModifiedTime can only throw FileNotFoundException, which is what stat uses.
304 throw new IllegalStateException (e);
305 }
306 }
307
Lukacs Berkic0e5bc52016-04-06 12:31:07 +0000308 @Override
309 public void close() {
310 if (open) {
lberki3edde6f2017-07-25 15:36:23 +0200311 try {
312 zipFile.close();
313 } catch (IOException e) {
314 // Not a lot can be done about this. Log an error and move on.
315 log.warning(String.format(
316 "Error while closing zip file '%s': %s", zipFile.getName(), e.getMessage()));
317 }
Lukacs Berkic0e5bc52016-04-06 12:31:07 +0000318 if (tempFile != null) {
319 tempFile.delete();
320 }
321 open = false;
322 }
323 }
Han-Wen Nienhuysd08b27f2015-02-25 16:45:20 +0100324}