summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaulo Casanova <pasc@google.com>2017-11-02 21:02:29 -0700
committerPaulo Casanova <pasc@google.com>2017-11-03 15:25:31 +0000
commite35235f71f08f7216833964e325ea57c96da965d (patch)
tree4db1758f69c0e87d411bb2c8dde6beda1f0ef6ea
parent9b82a24f0e7a4f128782269be9e97fea64b393f8 (diff)
downloadapkzlib-e35235f71f08f7216833964e325ea57c96da965d.tar.gz
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
-rw-r--r--src/main/java/com/android/apkzlib/zip/StoredEntry.java2
-rw-r--r--src/main/java/com/android/apkzlib/zip/ZFile.java82
-rw-r--r--src/test/java/com/android/apkzlib/zip/ZFileReadOnlyTest.java240
3 files changed, 323 insertions, 1 deletions
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<StoredEntry> mapEntry = entries.get(path);
Preconditions.checkNotNull(mapEntry, "mapEntry == null");
@@ -891,6 +919,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 <em>src</em> that
* should be ignored by merging; merging will behave as if these files were not there
* @throws IOException failed to read from <em>src</em> or write on the output
+ * @throws IllegalStateException if the file is in read-only mode
*/
public void mergeFrom(@Nonnull ZFile src, @Nonnull Predicate<String> 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<? super File, Boolean> 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]);
+ }
+ }
+}