From e35235f71f08f7216833964e325ea57c96da965d Mon Sep 17 00:00:00 2001 From: Paulo Casanova Date: Thu, 2 Nov 2017 21:02:29 -0700 Subject: Add read-only mode to ZFile. When opening the zip file in read-only mode the file is never modified and any attempt to modify throws IllegalStateExeption. Test: included Bug: 62249349 Change-Id: I5e589502dfd4d8d112bc563d7ed28291143ec5ea --- .../java/com/android/apkzlib/zip/StoredEntry.java | 2 + src/main/java/com/android/apkzlib/zip/ZFile.java | 82 ++++++- .../com/android/apkzlib/zip/ZFileReadOnlyTest.java | 240 +++++++++++++++++++++ 3 files changed, 323 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/android/apkzlib/zip/ZFileReadOnlyTest.java diff --git a/src/main/java/com/android/apkzlib/zip/StoredEntry.java b/src/main/java/com/android/apkzlib/zip/StoredEntry.java index 94aa01a..854bf3a 100644 --- a/src/main/java/com/android/apkzlib/zip/StoredEntry.java +++ b/src/main/java/com/android/apkzlib/zip/StoredEntry.java @@ -380,6 +380,7 @@ public class StoredEntry { * To eventually write updates to disk, {@link ZFile#update()} must be called. * * @throws IOException failed to delete the entry + * @throws IllegalStateException if the zip file was open in read-only mode */ public void delete() throws IOException { delete(true); @@ -392,6 +393,7 @@ public class StoredEntry { * @param notify should listeners be notified of the deletion? This will only be * {@code false} if the entry is being removed as part of a replacement * @throws IOException failed to delete the entry + * @throws IllegalStateException if the zip file was open in read-only mode */ void delete(boolean notify) throws IOException { Preconditions.checkState(!deleted, "deleted"); diff --git a/src/main/java/com/android/apkzlib/zip/ZFile.java b/src/main/java/com/android/apkzlib/zip/ZFile.java index 8a88cd0..1bdb410 100644 --- a/src/main/java/com/android/apkzlib/zip/ZFile.java +++ b/src/main/java/com/android/apkzlib/zip/ZFile.java @@ -414,6 +414,11 @@ public class ZFile implements Closeable { @Nullable private byte[] eocdComment; + /** + * Is the file in read-only mode? In read-only mode no changes are allowed. + */ + private boolean readOnly; + /** * Creates a new zip file. If the zip file does not exist, then no file is created at this @@ -439,12 +444,30 @@ public class ZFile implements Closeable { * @throws IOException some file exists but could not be read */ public ZFile(@Nonnull File file, @Nonnull ZFileOptions options) throws IOException { + this(file, options, false); + } + + /** + * Creates a new zip file. If the zip file does not exist, then no file is created at this + * point and {@code ZFile} will contain an empty structure. However, an (empty) zip file will + * be created if either {@link #update()} or {@link #close()} are used. If a zip file exists, + * it will be parsed and read. + * + * @param file the zip file + * @param options configuration options + * @param readOnly should the file be open in read-only mode? If {@code true} then the file must + * exist and no methods can be invoked that could potentially change the file + * @throws IOException some file exists but could not be read + */ + public ZFile(@Nonnull File file, @Nonnull ZFileOptions options, boolean readOnly) + throws IOException { this.file = file; map = new FileUseMap( 0, options.getCoverEmptySpaceUsingExtraField() ? MINIMUM_EXTRA_FIELD_SIZE : 0); + this.readOnly = readOnly; dirty = false; closedControl = null; alignmentRule = options.getAlignmentRule(); @@ -466,6 +489,8 @@ public class ZFile implements Closeable { if (file.exists()) { openReadOnly(); + } else if (readOnly) { + throw new IOException("File does not exist but read-only mode requested"); } else { dirty = true; } @@ -873,8 +898,11 @@ public class ZFile implements Closeable { * @param notify should listeners be notified of the deletion? This will only be * {@code false} if the entry is being removed as part of a replacement * @throws IOException failed to delete the entry + * @throws IllegalStateException if open in read-only mode */ void delete(@Nonnull final StoredEntry entry, boolean notify) throws IOException { + checkNotInReadOnlyMode(); + String path = entry.getCentralDirectoryHeader().getName(); FileUseMapEntry mapEntry = entries.get(path); Preconditions.checkNotNull(mapEntry, "mapEntry == null"); @@ -890,6 +918,17 @@ public class ZFile implements Closeable { } } + /** + * Checks that the file is not in read-only mode. + * + * @throws IllegalStateException if the file is in read-only mode + */ + private void checkNotInReadOnlyMode() { + if (readOnly) { + throw new IllegalStateException("Illegal operation in read only model"); + } + } + /** * Updates the file writing new entries and removing deleted entries. This will force * reopening the file as read/write if the file wasn't open in read/write mode. @@ -898,6 +937,8 @@ public class ZFile implements Closeable { * the compressor but only reported here */ public void update() throws IOException { + checkNotInReadOnlyMode(); + /* * Process all background stuff before calling in the extensions. */ @@ -1193,7 +1234,9 @@ public class ZFile implements Closeable { // We need to make sure to release raf, otherwise we end up locking the file on // Windows. Use try-with-resources to handle exception suppressing. try (Closeable ignored = this::innerClose) { - update(); + if (!readOnly) { + update(); + } } notify(ext -> { @@ -1489,6 +1532,9 @@ public class ZFile implements Closeable { * has been modified outside the control of this object */ private void reopenRw() throws IOException { + // We an never open a file RW in read-only mode. We should never get this far, though. + Verify.verify(!readOnly); + if (state == ZipFileState.OPEN_RW) { return; } @@ -1537,8 +1583,10 @@ public class ZFile implements Closeable { * and the name should not end in slash * @param stream the source for the file's data * @throws IOException failed to read the source data + * @throws IllegalStateException if the file is in read-only mode */ public void add(@Nonnull String name, @Nonnull InputStream stream) throws IOException { + checkNotInReadOnlyMode(); add(name, stream, true); } @@ -1656,9 +1704,11 @@ public class ZFile implements Closeable { * @param mayCompress can the file be compressed? This flag will be ignored if the alignment * rules force the file to be aligned, in which case the file will not be compressed. * @throws IOException failed to read the source data + * @throws IllegalStateException if the file is in read-only mode */ public void add(@Nonnull String name, @Nonnull InputStream stream, boolean mayCompress) throws IOException { + checkNotInReadOnlyMode(); /* * Clean pending background work, if needed. @@ -1868,9 +1918,12 @@ public class ZFile implements Closeable { * @param ignoreFilter predicate that, if {@code true}, identifies files in src that * should be ignored by merging; merging will behave as if these files were not there * @throws IOException failed to read from src or write on the output + * @throws IllegalStateException if the file is in read-only mode */ public void mergeFrom(@Nonnull ZFile src, @Nonnull Predicate ignoreFilter) throws IOException { + checkNotInReadOnlyMode(); + for (StoredEntry fromEntry : src.entries()) { if (ignoreFilter.test(fromEntry.getCentralDirectoryHeader().getName())) { continue; @@ -1960,8 +2013,11 @@ public class ZFile implements Closeable { /** * Forcibly marks this zip file as touched, forcing it to be updated when {@link #update()} * or {@link #close()} are invoked. + * + * @throws IllegalStateException if the file is in read-only mode */ public void touch() { + checkNotInReadOnlyMode(); dirty = true; } @@ -1988,8 +2044,11 @@ public class ZFile implements Closeable { * of {@code ZFile} may refer to {@link StoredEntry}s that are no longer valid * @throws IOException failed to realign the zip; some entries in the zip may have been lost * due to the I/O error + * @throws IllegalStateException if the file is in read-only mode */ public boolean realign() throws IOException { + checkNotInReadOnlyMode(); + boolean anyChanges = false; for (StoredEntry entry : entries()) { anyChanges |= entry.realign(); @@ -2103,8 +2162,10 @@ public class ZFile implements Closeable { * Adds an extension to this zip file. * * @param extension the listener to add + * @throws IllegalStateException if the file is in read-only mode */ public void addZFileExtension(@Nonnull ZFileExtension extension) { + checkNotInReadOnlyMode(); extensions.add(extension); } @@ -2112,8 +2173,10 @@ public class ZFile implements Closeable { * Removes an extension from this zip file. * * @param extension the listener to remove + * @throws IllegalStateException if the file is in read-only mode */ public void removeZFileExtension(@Nonnull ZFileExtension extension) { + checkNotInReadOnlyMode(); extensions.remove(extension); } @@ -2157,9 +2220,12 @@ public class ZFile implements Closeable { * @param start start offset in {@code data} where data to write is located * @param count number of bytes of data to write * @throws IOException failed to write the data + * @throws IllegalStateException if the file is in read-only mode */ public void directWrite(long offset, @Nonnull byte[] data, int start, int count) throws IOException { + checkNotInReadOnlyMode(); + Preconditions.checkArgument(offset >= 0, "offset < 0"); Preconditions.checkArgument(start >= 0, "start >= 0"); Preconditions.checkArgument(count >= 0, "count >= 0"); @@ -2184,8 +2250,10 @@ public class ZFile implements Closeable { * @param offset the offset at which data should be written * @param data the data to write, may be an empty array * @throws IOException failed to write the data + * @throws IllegalStateException if the file is in read-only mode */ public void directWrite(long offset, @Nonnull byte[] data) throws IOException { + checkNotInReadOnlyMode(); directWrite(offset, data, 0, data.length); } @@ -2322,8 +2390,10 @@ public class ZFile implements Closeable { * @param file a file or directory; if it is a directory, all files and directories will be * added recursively * @throws IOException failed to some (or all ) of the files + * @throws IllegalStateException if the file is in read-only mode */ public void addAllRecursively(@Nonnull File file) throws IOException { + checkNotInReadOnlyMode(); addAllRecursively(file, f -> true); } @@ -2334,10 +2404,13 @@ public class ZFile implements Closeable { * added recursively * @param mayCompress a function that decides whether files may be compressed * @throws IOException failed to some (or all ) of the files + * @throws IllegalStateException if the file is in read-only mode */ public void addAllRecursively( @Nonnull File file, @Nonnull Function mayCompress) throws IOException { + checkNotInReadOnlyMode(); + /* * The case of file.isFile() is different because if file.isFile() we will add it to the * zip in the root. However, if file.isDirectory() we won't add it and add its children. @@ -2469,8 +2542,11 @@ public class ZFile implements Closeable { * * @param comment the new comment; no conversion is done, these exact bytes will be placed in * the EOCD comment + * @throws IllegalStateException if file is in read-only mode */ public void setEocdComment(@Nonnull byte[] comment) { + checkNotInReadOnlyMode(); + if (comment.length > MAX_EOCD_COMMENT_SIZE) { throw new IllegalArgumentException( "EOCD comment size (" @@ -2514,8 +2590,10 @@ public class ZFile implements Closeable { * updated. * * @param offset the offset or {@code 0} to write the central directory at its current location + * @throws IllegalStateException if file is in read-only mode */ public void setExtraDirectoryOffset(long offset) { + checkNotInReadOnlyMode(); Preconditions.checkArgument(offset >= 0, "offset < 0"); if (extraDirectoryOffset != offset) { @@ -2551,8 +2629,10 @@ public class ZFile implements Closeable { * written to disk. * * @throws IOException failed to load or move a file in the zip + * @throws IllegalStateException if file is in read-only mode */ public void sortZipContents() throws IOException { + checkNotInReadOnlyMode(); reopenRw(); processAllReadyEntriesWithWait(); diff --git a/src/test/java/com/android/apkzlib/zip/ZFileReadOnlyTest.java b/src/test/java/com/android/apkzlib/zip/ZFileReadOnlyTest.java new file mode 100644 index 0000000..a030a83 --- /dev/null +++ b/src/test/java/com/android/apkzlib/zip/ZFileReadOnlyTest.java @@ -0,0 +1,240 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * 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.android.apkzlib.zip; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import javax.annotation.Nonnull; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class ZFileReadOnlyTest { + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void cannotCreateRoFileOnNonExistingFile() throws Exception { + try { + new ZFile(new File(temporaryFolder.getRoot(), "foo.zip"), new ZFileOptions(), true); + fail(); + } catch (IOException e) { + // Expected. + } + } + + @Nonnull + private File makeTestZip() throws IOException { + File zip = new File(temporaryFolder.getRoot(), "foo.zip"); + try (ZFile zf = new ZFile(zip)) { + zf.add("bar", new ByteArrayInputStream(new byte[] { 0, 1, 2, 3, 4, 5 })); + } + + return zip; + } + + @Test + public void cannotUpdateInRoMode() throws Exception { + try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { + try { + zf.update(); + fail(); + } catch (IllegalStateException e) { + // Expeted. + } + } + } + + @Test + public void cannotAddFilesInRoMode() throws Exception { + try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { + try { + zf.add("bar2", new ByteArrayInputStream(new byte[] { 6, 7, })); + fail(); + } catch (IllegalStateException e) { + // Expeted. + } + } + } + + @Test + public void cannotAddRecursivelyInRoMode() throws Exception { + File folder = temporaryFolder.newFolder(); + try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { + try { + zf.addAllRecursively(folder); + fail(); + } catch (IllegalStateException e) { + // Expeted. + } + } + } + + @Test + public void cannotReplaceFilesInRoMode() throws Exception { + try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { + try { + zf.add("bar", new ByteArrayInputStream(new byte[] { 6, 7 })); + fail(); + } catch (IllegalStateException e) { + // Expeted. + } + } + } + + @Test + public void cannotDeleteFilesInRoMode() throws Exception { + try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { + StoredEntry bar = zf.get("bar"); + assertNotNull(bar); + try { + bar.delete(); + fail(); + } catch (IllegalStateException e) { + // Expeted. + } + } + } + + @Test + public void cannotMergeInRoMode() throws Exception { + try (ZFile toMerge = new ZFile(new File(temporaryFolder.getRoot(), "a.zip"))) { + try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { + try { + zf.mergeFrom(toMerge, s -> false); + fail(); + } catch (IllegalStateException e) { + // Expeted. + } + } + } + } + + @Test + public void cannotTouchInRoMode() throws Exception { + try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { + try { + zf.touch(); + fail(); + } catch (IllegalStateException e) { + // Expeted. + } + } + } + + @Test + public void cannotRealignInRoMode() throws Exception { + try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { + try { + zf.realign(); + fail(); + } catch (IllegalStateException e) { + // Expeted. + } + } + } + + @Test + public void cannotAddExtensionInRoMode() throws Exception { + try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { + try { + zf.addZFileExtension(new ZFileExtension() {}); + fail(); + } catch (IllegalStateException e) { + // Expeted. + } + } + } + + @Test + public void cannotDirectWriteInRoMode() throws Exception { + try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { + try { + zf.directWrite(0, new byte[1]); + fail(); + } catch (IllegalStateException e) { + // Expeted. + } + } + } + + @Test + public void cannotSetEocdCommentInRoMode() throws Exception { + try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { + try { + zf.setEocdComment(new byte[2]); + fail(); + } catch (IllegalStateException e) { + // Expeted. + } + } + } + + @Test + public void cannotSetCentralDirectoryOffsetInRoMode() throws Exception { + try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { + try { + zf.setExtraDirectoryOffset(4); + fail(); + } catch (IllegalStateException e) { + // Expeted. + } + } + } + + @Test + public void cannotSortZipContentsInRoMode() throws Exception { + try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { + try { + zf.sortZipContents(); + fail(); + } catch (IllegalStateException e) { + // Expeted. + } + } + } + + @Test + public void canOpenAndReadFilesInRoMode() throws Exception { + try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { + StoredEntry bar = zf.get("bar"); + assertNotNull(bar); + assertArrayEquals(new byte[] { 0, 1, 2, 3, 4, 5 }, bar.read()); + } + } + + @Test + public void canGetDirectoryAndEocdBytesInRoMode() throws Exception { + try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { + zf.getCentralDirectoryBytes(); + zf.getEocdBytes(); + zf.getEocdComment(); + } + } + + @Test + public void canDirectReadInRoMode() throws Exception { + try (ZFile zf = new ZFile(makeTestZip(), new ZFileOptions(), true)) { + zf.directRead(0, new byte[2]); + } + } +} -- cgit v1.2.3