diff options
-rw-r--r-- | BUILD | 2 | ||||
-rw-r--r-- | apkzlib.iml | 1 | ||||
-rw-r--r-- | build.gradle | 2 | ||||
-rw-r--r-- | src/main/java/com/android/apkzlib/zip/ExtraField.java | 10 | ||||
-rw-r--r-- | src/main/java/com/android/apkzlib/zip/FileUseMap.java | 37 | ||||
-rw-r--r-- | src/main/java/com/android/apkzlib/zip/StoredEntry.java | 2 | ||||
-rw-r--r-- | src/main/java/com/android/apkzlib/zip/ZFile.java | 129 | ||||
-rw-r--r-- | src/test/java/com/android/apkzlib/zip/ExtraFieldTest.java | 26 | ||||
-rw-r--r-- | src/test/java/com/android/apkzlib/zip/ZFileReadOnlyTest.java | 240 | ||||
-rw-r--r-- | src/test/java/com/android/apkzlib/zip/ZFileTest.java | 36 | ||||
-rw-r--r-- | src/test/resources/testData/packaging/entry-outside-file.zip | bin | 0 -> 364 bytes | |||
-rw-r--r-- | src/test/resources/testData/packaging/overlapping.zip | bin | 0 -> 502 bytes | |||
-rw-r--r-- | src/test/resources/testData/packaging/overlapping2.zip | bin | 0 -> 502 bytes |
13 files changed, 481 insertions, 4 deletions
@@ -7,7 +7,7 @@ java_library( ]), visibility = ["//tools/base/build-system/builder:__pkg__"], deps = [ - "//tools/base/build-system:tools.apksig", + "//tools/apksig", "//tools/base/third_party:com.google.code.findbugs_jsr305", "//tools/base/third_party:com.google.guava_guava", "//tools/base/third_party:org.bouncycastle_bcpkix-jdk15on", diff --git a/apkzlib.iml b/apkzlib.iml index 3c33d1b..999fffa 100644 --- a/apkzlib.iml +++ b/apkzlib.iml @@ -16,5 +16,6 @@ <orderEntry type="library" name="bouncy-castle" level="project" /> <orderEntry type="module" module-name="testutils" scope="TEST" /> <orderEntry type="module" module-name="apksig" /> + <orderEntry type="library" name="KotlinJavaRuntime" level="project" /> </component> </module>
\ No newline at end of file diff --git a/build.gradle b/build.gradle index f49541a..771d1b8 100644 --- a/build.gradle +++ b/build.gradle @@ -1,4 +1,4 @@ -apply plugin: 'java' +apply from: "$rootDir/buildSrc/base/baseJava.gradle" dependencies { compile 'com.google.code.findbugs:jsr305:1.3.9' diff --git a/src/main/java/com/android/apkzlib/zip/ExtraField.java b/src/main/java/com/android/apkzlib/zip/ExtraField.java index 90c6fae..d70fa7f 100644 --- a/src/main/java/com/android/apkzlib/zip/ExtraField.java +++ b/src/main/java/com/android/apkzlib/zip/ExtraField.java @@ -158,6 +158,16 @@ public class ExtraField { } byte[] data = new byte[dataSize]; + if (buffer.remaining() < dataSize) { + throw new IOException( + "Invalid data size for extra field segment with header ID " + + headerId + + ": " + + dataSize + + " (only " + + buffer.remaining() + + " bytes are available)"); + } buffer.get(data); SegmentFactory factory = identifySegmentFactory(headerId); diff --git a/src/main/java/com/android/apkzlib/zip/FileUseMap.java b/src/main/java/com/android/apkzlib/zip/FileUseMap.java index a72a956..8a76878 100644 --- a/src/main/java/com/android/apkzlib/zip/FileUseMap.java +++ b/src/main/java/com/android/apkzlib/zip/FileUseMap.java @@ -538,6 +538,43 @@ class FileUseMap { return map.lower(entry); } + /** + * Obtains the entry that is located after the one provided. + * + * @param entry the map entry to get the next one for; must belong to the map + * @return the entry after the provided one, {@code null} if {@code entry} is the last entry in + * the map + */ + @Nullable + FileUseMapEntry<?> after(@Nonnull FileUseMapEntry<?> entry) { + Preconditions.checkNotNull(entry, "entry == null"); + + return map.higher(entry); + } + + /** + * Obtains the entry at the given offset. + * + * @param offset the offset to look for + * @return the entry found or {@code null} if there is no entry (not even a free one) at the + * given offset + */ + @Nullable + FileUseMapEntry<?> at(long offset) { + Preconditions.checkArgument(offset >= 0, "offset < 0"); + Preconditions.checkArgument(offset < size, "offset >= size"); + + FileUseMapEntry<?> entry = map.floor(FileUseMapEntry.makeFree(offset, offset + 1)); + if (entry == null) { + return null; + } + + Verify.verify(entry.getStart() <= offset); + Verify.verify(entry.getEnd() > offset); + + return entry; + } + @Override public String toString() { StringJoiner j = new StringJoiner(", "); 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..9034f4c 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; } @@ -572,7 +597,7 @@ public class ZFile implements Closeable { readCentralDirectory(); /* - * Compute where the last file ends. We will need this to compute thee extra offset. + * Go over all files and create the usage map, verifying there is no overlap in the files. */ long entryEndOffset; long directoryStartOffset; @@ -600,6 +625,51 @@ public class ZFile implements Closeable { * file. */ + Verify.verify(start >= 0, "start < 0"); + Verify.verify(end < map.size(), "end >= map.size()"); + + FileUseMapEntry<?> found = map.at(start); + Verify.verifyNotNull(found); + + // We've got a problem if the found entry is not free or is a free entry but + // doesn't cover the whole file. + if (!found.isFree() || found.getEnd() < end) { + if (found.isFree()) { + found = map.after(found); + Verify.verify(found != null && !found.isFree()); + } + + Object foundEntry = found.getStore(); + Verify.verify(foundEntry != null); + + // Obtains a custom description of an entry. + IOExceptionFunction<StoredEntry, String> describe = + e -> + String.format( + "'%s' (offset: %d, size: %d)", + e.getCentralDirectoryHeader().getName(), + e.getCentralDirectoryHeader().getOffset(), + e.getInFileSize()); + + String overlappingEntryDescription; + if (foundEntry instanceof StoredEntry) { + StoredEntry foundStored = (StoredEntry) foundEntry; + overlappingEntryDescription = describe.apply((StoredEntry) foundEntry); + } else { + overlappingEntryDescription = + "Central Directory / EOCD: " + + found.getStart() + + " - " + + found.getEnd(); + } + + throw new IOException( + "Cannot read entry " + + describe.apply(entry) + + " because it overlaps with " + + overlappingEntryDescription); + } + FileUseMapEntry<StoredEntry> mapEntry = map.add(start, end, entry); entries.put(entry.getCentralDirectoryHeader().getName(), mapEntry); @@ -873,8 +943,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 +964,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 +982,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 +1279,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 +1577,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 +1628,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 +1749,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 +1963,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 +2058,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 +2089,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 +2207,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 +2218,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 +2265,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 +2295,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 +2435,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 +2449,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 +2587,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 +2635,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 +2674,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/ExtraFieldTest.java b/src/test/java/com/android/apkzlib/zip/ExtraFieldTest.java index d80ccc4..2371849 100644 --- a/src/test/java/com/android/apkzlib/zip/ExtraFieldTest.java +++ b/src/test/java/com/android/apkzlib/zip/ExtraFieldTest.java @@ -19,6 +19,7 @@ package com.android.apkzlib.zip; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; import com.google.common.collect.ImmutableList; @@ -332,4 +333,29 @@ public class ExtraFieldTest { assertArrayEquals(new byte[] { 0x54, 0x76, 0x04, 0x00, 2, 4, 2, 4 }, sData); } } + + @Test + public void parseInvalidExtraFieldWithInvalidHeader() throws Exception { + byte[] raw = new byte[1]; + ExtraField ef = new ExtraField(raw); + try { + ef.getSegments(); + fail(); + } catch (IOException e) { + // Expected. + } + } + + @Test + public void parseInvalidExtraFieldWithInsufficientData() throws Exception { + // Remember: 0x05, 0x00 = 5 in little endian! + byte[] raw = new byte[] { /* Header */ 0x01, 0x02, /* Size */ 0x05, 0x00, /* Data */ 0x01 }; + ExtraField ef = new ExtraField(raw); + try { + ef.getSegments(); + fail(); + } catch (IOException e) { + // Expected. + } + } } 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]); + } + } +} diff --git a/src/test/java/com/android/apkzlib/zip/ZFileTest.java b/src/test/java/com/android/apkzlib/zip/ZFileTest.java index bce0286..b7f2979 100644 --- a/src/test/java/com/android/apkzlib/zip/ZFileTest.java +++ b/src/test/java/com/android/apkzlib/zip/ZFileTest.java @@ -33,6 +33,7 @@ import com.android.apkzlib.zip.utils.CloseableByteSource; import com.android.apkzlib.zip.utils.RandomAccessFileUtils; import com.google.common.base.Charsets; import com.google.common.base.Strings; +import com.google.common.base.Throwables; import com.google.common.hash.Hashing; import com.google.common.io.ByteStreams; import com.google.common.io.Closer; @@ -1782,4 +1783,39 @@ public class ZFileTest { assertArrayEquals(comment, zf.getEocdComment()); } } + + @Test + public void overlappingZipEntries() throws Exception { + File myZip = ZipTestUtils.cloneRsrc("overlapping.zip", mTemporaryFolder); + try (ZFile zf = new ZFile(myZip)) { + fail(); + } catch (IOException e) { + assertTrue(Throwables.getStackTraceAsString(e).contains("overlapping/bbb")); + assertTrue(Throwables.getStackTraceAsString(e).contains("overlapping/ddd")); + assertFalse(Throwables.getStackTraceAsString(e).contains("Central Directory")); + } + } + + @Test + public void overlappingZipEntryWithCentralDirectory() throws Exception { + File myZip = ZipTestUtils.cloneRsrc("overlapping2.zip", mTemporaryFolder); + try (ZFile zf = new ZFile(myZip)) { + fail(); + } catch (IOException e) { + assertFalse(Throwables.getStackTraceAsString(e).contains("overlapping/bbb")); + assertTrue(Throwables.getStackTraceAsString(e).contains("overlapping/ddd")); + assertTrue(Throwables.getStackTraceAsString(e).contains("Central Directory")); + } + } + + @Test + public void readFileWithOffsetBeyondFileEnd() throws Exception { + File myZip = ZipTestUtils.cloneRsrc("entry-outside-file.zip", mTemporaryFolder); + try (ZFile zf = new ZFile(myZip)) { + fail(); + } catch (IOException e) { + assertTrue(Throwables.getStackTraceAsString(e).contains("entry-outside-file/foo")); + assertTrue(Throwables.getStackTraceAsString(e).contains("EOF")); + } + } } diff --git a/src/test/resources/testData/packaging/entry-outside-file.zip b/src/test/resources/testData/packaging/entry-outside-file.zip Binary files differnew file mode 100644 index 0000000..ffd6be9 --- /dev/null +++ b/src/test/resources/testData/packaging/entry-outside-file.zip diff --git a/src/test/resources/testData/packaging/overlapping.zip b/src/test/resources/testData/packaging/overlapping.zip Binary files differnew file mode 100644 index 0000000..7f6144c --- /dev/null +++ b/src/test/resources/testData/packaging/overlapping.zip diff --git a/src/test/resources/testData/packaging/overlapping2.zip b/src/test/resources/testData/packaging/overlapping2.zip Binary files differnew file mode 100644 index 0000000..eecefa9 --- /dev/null +++ b/src/test/resources/testData/packaging/overlapping2.zip |