blob: 9ba39e540eced902bdc66e7ad4f6d491eaf9654a [file] [log] [blame]
// Copyright 2016 Google Inc. 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.archivepatcher.generator;
import com.google.archivepatcher.generator.DefaultDeflateCompressionDiviner.DivinationResult;
import com.google.archivepatcher.shared.DefaultDeflateCompatibilityWindow;
import com.google.archivepatcher.shared.JreDeflateParameters;
import com.google.archivepatcher.shared.RandomAccessFileInputStream;
import com.google.archivepatcher.shared.TypedRange;
import com.google.archivepatcher.shared.UnitTestZipArchive;
import com.google.archivepatcher.shared.UnitTestZipEntry;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/**
* Tests for {@link PreDiffPlanner}.
*/
@RunWith(JUnit4.class)
@SuppressWarnings("javadoc")
public class PreDiffPlannerTest {
// All the A and B entries consist of a chunk of text followed by a standard corpus of text from
// the DefaultDeflateCompatibilityDiviner that ensures the tests will be able to discriminate
// between any compression level. Without this additional corpus text, multiple compression levels
// can match the entry and the unit tests would not be accurate.
private static final UnitTestZipEntry ENTRY_A_LEVEL_6 =
UnitTestZipArchive.makeUnitTestZipEntry("/path A", 6, "entry A", null);
private static final UnitTestZipEntry ENTRY_A_LEVEL_9 =
UnitTestZipArchive.makeUnitTestZipEntry("/path A", 9, "entry A", null);
private static final UnitTestZipEntry ENTRY_A_STORED =
UnitTestZipArchive.makeUnitTestZipEntry("/path A", 0, "entry A", null);
private static final UnitTestZipEntry ENTRY_B_LEVEL_6 =
UnitTestZipArchive.makeUnitTestZipEntry("/path B", 6, "entry B", null);
private static final UnitTestZipEntry ENTRY_B_LEVEL_9 =
UnitTestZipArchive.makeUnitTestZipEntry("/path B", 9, "entry B", null);
/**
* Entry C1 is a small entry WITHOUT the standard corpus of text from
* {@link DefaultDeflateCompatibilityWindow} appended. It has exactly the same compressed length
* as {@link #FIXED_LENGTH_ENTRY_C2_LEVEL_6}, and can be used to test the byte-matching logic in
* the code when the compressed lengths are identical.
*/
private static final UnitTestZipEntry FIXED_LENGTH_ENTRY_C1_LEVEL_6 =
new UnitTestZipEntry("/path C", 6, "qqqqqqqqqqqqqqqqqqqqqqqqqqqq", null);
/**
* Entry C2 is a small entry WITHOUT the standard corpus of text from
* {@link DefaultDeflateCompatibilityWindow} appended. It has exactly the same compressed length
* as {@link #FIXED_LENGTH_ENTRY_C1_LEVEL_6}, and can be used to test the byte-matching logic in
* the code when the compressed lengths are identical.
*/
private static final UnitTestZipEntry FIXED_LENGTH_ENTRY_C2_LEVEL_6 =
new UnitTestZipEntry("/path C", 6, "rrrrrrrrrrrrrrrrrrrrrrrrrrrr", null);
// The "shadow" entries are exact copies of ENTRY_A_* but have a different path. These are used
// for the detection of renames that don't involve modification (i.e., the uncompressed CRC32 is
// exactly the same as the ENTRY_A_* entries)
private static final UnitTestZipEntry SHADOW_ENTRY_A_LEVEL_1 =
UnitTestZipArchive.makeUnitTestZipEntry("/uncompressed data same as A", 1, "entry A", null);
private static final UnitTestZipEntry SHADOW_ENTRY_A_LEVEL_6 =
UnitTestZipArchive.makeUnitTestZipEntry("/same as A level 6", 6, "entry A", null);
private static final UnitTestZipEntry SHADOW_ENTRY_A_LEVEL_9 =
UnitTestZipArchive.makeUnitTestZipEntry("/same as A level 9", 9, "entry A", null);
private static final UnitTestZipEntry SHADOW_ENTRY_A_STORED =
UnitTestZipArchive.makeUnitTestZipEntry("/same as A stored", 0, "entry A", null);
private List<File> tempFilesCreated;
private Map<File, Map<ByteArrayHolder, MinimalZipEntry>> entriesByPathByTempFile;
@Before
public void setup() {
tempFilesCreated = new LinkedList<File>();
entriesByPathByTempFile = new HashMap<File, Map<ByteArrayHolder, MinimalZipEntry>>();
}
@After
public void tearDown() {
for (File file : tempFilesCreated) {
try {
file.delete();
} catch (Exception ignored) {
// Nothing
}
}
}
/**
* Stores the specified bytes to disk in a temp file, returns the temp file and caches the zip
* entries for the file for use in later code.
* @param data the bytes to store, expected to be a valid zip file
* @throws IOException if it fails
*/
private File storeAndMapArchive(byte[] data) throws IOException {
File file = File.createTempFile("pdpt", "zip");
tempFilesCreated.add(file);
file.deleteOnExit();
FileOutputStream out = new FileOutputStream(file);
out.write(data);
out.flush();
out.close();
Map<ByteArrayHolder, MinimalZipEntry> entriesByPath = new HashMap<>();
for (MinimalZipEntry zipEntry : MinimalZipArchive.listEntries(file)) {
ByteArrayHolder key = new ByteArrayHolder(zipEntry.getFileNameBytes());
entriesByPath.put(key, zipEntry);
}
entriesByPathByTempFile.put(file, entriesByPath);
return file;
}
/**
* Finds a unit test entry in the specified temp file.
* @param tempFile the archive to search within
* @param unitTestEntry the unit test entry to look up
* @return the {@link MinimalZipEntry} corresponding to the unit test entry
*/
private MinimalZipEntry findEntry(File tempFile, UnitTestZipEntry unitTestEntry) {
Map<ByteArrayHolder, MinimalZipEntry> subMap = entriesByPathByTempFile.get(tempFile);
Assert.assertNotNull("temp file not mapped", subMap);
ByteArrayHolder key;
try {
key = new ByteArrayHolder(unitTestEntry.path.getBytes("UTF8"));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
return subMap.get(key);
}
/**
* Finds the {@link TypedRange} corresponding to the compressed data for the specified unit test
* entry in the specified temp file.
* @param tempFile the archive to search within
* @param unitTestEntry the unit test entry to look up
* @return the {@link TypedRange} for the unit test entry's compressed data
*/
private TypedRange<Void> findRangeWithoutParams(File tempFile, UnitTestZipEntry unitTestEntry) {
MinimalZipEntry found = findEntry(tempFile, unitTestEntry);
Assert.assertNotNull("entry not found in temp file", found);
return new TypedRange<Void>(
found.getFileOffsetOfCompressedData(), found.getCompressedSize(), null);
}
/**
* Finds the {@link TypedRange} corresponding to the compressed data for the specified unit test
* entry in the specified temp file.
* @param tempFile the archive to search within
* @param unitTestEntry the unit test entry to look up
* @return the {@link TypedRange} for the unit test entry's compressed data
*/
private TypedRange<JreDeflateParameters> findRangeWithParams(
File tempFile, UnitTestZipEntry unitTestEntry) {
MinimalZipEntry found = findEntry(tempFile, unitTestEntry);
Assert.assertNotNull("entry not found in temp file", found);
return new TypedRange<JreDeflateParameters>(
found.getFileOffsetOfCompressedData(),
found.getCompressedSize(),
JreDeflateParameters.of(unitTestEntry.level, 0, true));
}
/**
* Deliberately introduce an error into the specified entry. This will make the entry impossible
* to divine the settings for, because it is broken.
* @param tempFile the archive to search within
* @param unitTestEntry the unit test entry to deliberately corrupt
*/
private void corruptEntryData(File tempFile, UnitTestZipEntry unitTestEntry) throws IOException {
TypedRange<Void> range = findRangeWithoutParams(tempFile, unitTestEntry);
Assert.assertTrue("range too short to corrupt with 'junk'", range.getLength() >= 4);
try (RandomAccessFile raf = new RandomAccessFile(tempFile, "rw")) {
raf.seek(range.getOffset());
raf.write("junk".getBytes("UTF8"));
}
}
/**
* Deliberately garble the compression method in the specified entry such that it is no longer
* deflate.
* @param tempFile the archive to search within
* @param unitTestEntry the unit test entry to deliberately corrupt
*/
private void corruptCompressionMethod(File tempFile, UnitTestZipEntry unitTestEntry)
throws IOException {
long centralDirectoryRecordOffset = -1;
try (RandomAccessFileInputStream rafis = new RandomAccessFileInputStream(tempFile)) {
long startOfEocd = MinimalZipParser.locateStartOfEocd(rafis, 32768);
rafis.setRange(startOfEocd, tempFile.length() - startOfEocd);
MinimalCentralDirectoryMetadata centralDirectoryMetadata = MinimalZipParser.parseEocd(rafis);
int numEntries = centralDirectoryMetadata.getNumEntriesInCentralDirectory();
rafis.setRange(
centralDirectoryMetadata.getOffsetOfCentralDirectory(),
centralDirectoryMetadata.getLengthOfCentralDirectory());
for (int x = 0; x < numEntries; x++) {
long recordStartOffset = rafis.getPosition();
MinimalZipEntry candidate = MinimalZipParser.parseCentralDirectoryEntry(rafis);
if (candidate.getFileName().equals(unitTestEntry.path)) {
// Located! Track offset and bail out.
centralDirectoryRecordOffset = recordStartOffset;
x = numEntries;
}
}
}
Assert.assertNotEquals("Entry not found", -1L, centralDirectoryRecordOffset);
try (RandomAccessFile raf = new RandomAccessFile(tempFile, "rw")) {
// compression method is a 2 byte field stored 10 bytes into the record
raf.seek(centralDirectoryRecordOffset + 10);
raf.write(7);
raf.write(7);
}
}
private PreDiffPlan invokeGeneratePreDiffPlan(
File oldFile, File newFile, RecommendationModifier... recommendationModifiers)
throws IOException {
Map<ByteArrayHolder, MinimalZipEntry> originalOldArchiveZipEntriesByPath =
new LinkedHashMap<ByteArrayHolder, MinimalZipEntry>();
Map<ByteArrayHolder, MinimalZipEntry> originalNewArchiveZipEntriesByPath =
new LinkedHashMap<ByteArrayHolder, MinimalZipEntry>();
Map<ByteArrayHolder, JreDeflateParameters> originalNewArchiveJreDeflateParametersByPath =
new LinkedHashMap<ByteArrayHolder, JreDeflateParameters>();
for (MinimalZipEntry zipEntry : MinimalZipArchive.listEntries(oldFile)) {
ByteArrayHolder key = new ByteArrayHolder(zipEntry.getFileNameBytes());
originalOldArchiveZipEntriesByPath.put(key, zipEntry);
}
DefaultDeflateCompressionDiviner diviner = new DefaultDeflateCompressionDiviner();
for (DivinationResult divinationResult : diviner.divineDeflateParameters(newFile)) {
ByteArrayHolder key = new ByteArrayHolder(divinationResult.minimalZipEntry.getFileNameBytes());
originalNewArchiveZipEntriesByPath.put(key, divinationResult.minimalZipEntry);
originalNewArchiveJreDeflateParametersByPath.put(key, divinationResult.divinedParameters);
}
PreDiffPlanner preDiffPlanner =
new PreDiffPlanner(
oldFile,
originalOldArchiveZipEntriesByPath,
newFile,
originalNewArchiveZipEntriesByPath,
originalNewArchiveJreDeflateParametersByPath,
recommendationModifiers);
return preDiffPlanner.generatePreDiffPlan();
}
private void checkRecommendation(PreDiffPlan plan, QualifiedRecommendation... expected) {
Assert.assertNotNull(plan.getQualifiedRecommendations());
Assert.assertEquals(expected.length, plan.getQualifiedRecommendations().size());
for (int x = 0; x < expected.length; x++) {
QualifiedRecommendation actual = plan.getQualifiedRecommendations().get(x);
Assert.assertEquals(
expected[x].getOldEntry().getFileName(), actual.getOldEntry().getFileName());
Assert.assertEquals(
expected[x].getNewEntry().getFileName(), actual.getNewEntry().getFileName());
Assert.assertEquals(expected[x].getRecommendation(), actual.getRecommendation());
Assert.assertEquals(expected[x].getReason(), actual.getReason());
}
}
@Test
public void testGeneratePreDiffPlan_OneCompressedEntry_Unchanged() throws IOException {
byte[] bytes = UnitTestZipArchive.makeTestZip(Collections.singletonList(ENTRY_A_LEVEL_6));
File oldFile = storeAndMapArchive(bytes);
File newFile = storeAndMapArchive(bytes);
PreDiffPlan plan = invokeGeneratePreDiffPlan(oldFile, newFile);
Assert.assertNotNull(plan);
// The plan should be to leave the entry alone in both the old and new archives (empty plans).
Assert.assertTrue(plan.getOldFileUncompressionPlan().isEmpty());
Assert.assertTrue(plan.getNewFileUncompressionPlan().isEmpty());
checkRecommendation(plan, new QualifiedRecommendation(
findEntry(oldFile, ENTRY_A_LEVEL_6),
findEntry(newFile, ENTRY_A_LEVEL_6),
Recommendation.UNCOMPRESS_NEITHER,
RecommendationReason.COMPRESSED_BYTES_IDENTICAL));
}
@Test
public void testGeneratePreDiffPlan_OneCompressedEntry_LengthsChanged() throws IOException {
// Test detection of compressed entry differences based on length mismatch.
byte[] oldBytes = UnitTestZipArchive.makeTestZip(Collections.singletonList(ENTRY_A_LEVEL_6));
byte[] newBytes = UnitTestZipArchive.makeTestZip(Collections.singletonList(ENTRY_A_LEVEL_9));
File oldFile = storeAndMapArchive(oldBytes);
File newFile = storeAndMapArchive(newBytes);
PreDiffPlan plan = invokeGeneratePreDiffPlan(oldFile, newFile);
Assert.assertNotNull(plan);
// The plan should be to uncompress the entry in both the old and new archives.
Assert.assertEquals(1, plan.getOldFileUncompressionPlan().size());
Assert.assertEquals(
findRangeWithoutParams(oldFile, ENTRY_A_LEVEL_6),
plan.getOldFileUncompressionPlan().get(0));
Assert.assertEquals(1, plan.getNewFileUncompressionPlan().size());
Assert.assertEquals(
findRangeWithParams(newFile, ENTRY_A_LEVEL_9), plan.getNewFileUncompressionPlan().get(0));
checkRecommendation(plan, new QualifiedRecommendation(
findEntry(oldFile, ENTRY_A_LEVEL_6),
findEntry(newFile, ENTRY_A_LEVEL_9),
Recommendation.UNCOMPRESS_BOTH,
RecommendationReason.COMPRESSED_BYTES_CHANGED));
}
@Test
public void testGeneratePreDiffPlan_OneCompressedEntry_BytesChanged() throws IOException {
// Test detection of compressed entry differences based on binary content mismatch where the
// compressed lengths are exactly the same - i.e., force a byte-by-byte comparison of the
// compressed data in the two entries.
byte[] oldBytes =
UnitTestZipArchive.makeTestZip(Collections.singletonList(FIXED_LENGTH_ENTRY_C1_LEVEL_6));
byte[] newBytes =
UnitTestZipArchive.makeTestZip(Collections.singletonList(FIXED_LENGTH_ENTRY_C2_LEVEL_6));
File oldFile = storeAndMapArchive(oldBytes);
File newFile = storeAndMapArchive(newBytes);
PreDiffPlan plan = invokeGeneratePreDiffPlan(oldFile, newFile);
Assert.assertNotNull(plan);
// The plan should be to uncompress the entry in both the old and new archives.
Assert.assertEquals(1, plan.getOldFileUncompressionPlan().size());
Assert.assertEquals(1, plan.getNewFileUncompressionPlan().size());
Assert.assertEquals(
findRangeWithoutParams(oldFile, FIXED_LENGTH_ENTRY_C1_LEVEL_6),
plan.getOldFileUncompressionPlan().get(0));
Assert.assertEquals(
findRangeWithParams(newFile, FIXED_LENGTH_ENTRY_C2_LEVEL_6),
plan.getNewFileUncompressionPlan().get(0));
checkRecommendation(plan, new QualifiedRecommendation(
findEntry(oldFile, FIXED_LENGTH_ENTRY_C1_LEVEL_6),
findEntry(newFile, FIXED_LENGTH_ENTRY_C2_LEVEL_6),
Recommendation.UNCOMPRESS_BOTH,
RecommendationReason.COMPRESSED_BYTES_CHANGED));
}
@Test
public void testGeneratePreDiffPlan_OneUncompressedEntry() throws IOException {
// Test with uncompressed old and new. It doesn't matter whether the bytes are changed or
// unchanged in this case.
byte[] oldBytes = UnitTestZipArchive.makeTestZip(Collections.singletonList(ENTRY_A_STORED));
byte[] newBytes = UnitTestZipArchive.makeTestZip(Collections.singletonList(ENTRY_A_STORED));
File oldFile = storeAndMapArchive(oldBytes);
File newFile = storeAndMapArchive(newBytes);
PreDiffPlan plan = invokeGeneratePreDiffPlan(oldFile, newFile);
Assert.assertNotNull(plan);
// The plan should be to do nothing because both entries are already uncompressed
Assert.assertTrue(plan.getOldFileUncompressionPlan().isEmpty());
Assert.assertTrue(plan.getNewFileUncompressionPlan().isEmpty());
checkRecommendation(plan, new QualifiedRecommendation(
findEntry(oldFile, ENTRY_A_STORED),
findEntry(newFile, ENTRY_A_STORED),
Recommendation.UNCOMPRESS_NEITHER,
RecommendationReason.BOTH_ENTRIES_UNCOMPRESSED));
}
@Test
public void testGeneratePreDiffPlan_OneEntry_CompressedToUncompressed() throws IOException {
// Test the migration of an entry from compressed (old archive) to uncompressed (new archive).
byte[] oldBytes = UnitTestZipArchive.makeTestZip(Collections.singletonList(ENTRY_A_LEVEL_9));
byte[] newBytes = UnitTestZipArchive.makeTestZip(Collections.singletonList(ENTRY_A_STORED));
File oldFile = storeAndMapArchive(oldBytes);
File newFile = storeAndMapArchive(newBytes);
PreDiffPlan plan = invokeGeneratePreDiffPlan(oldFile, newFile);
Assert.assertNotNull(plan);
// The plan should be to uncompress the entry in the old archive and do nothing in the new
// archive (empty plan)
Assert.assertEquals(1, plan.getOldFileUncompressionPlan().size());
Assert.assertEquals(
findRangeWithoutParams(oldFile, ENTRY_A_LEVEL_9),
plan.getOldFileUncompressionPlan().get(0));
Assert.assertTrue(plan.getNewFileUncompressionPlan().isEmpty());
checkRecommendation(plan, new QualifiedRecommendation(
findEntry(oldFile, ENTRY_A_LEVEL_9),
findEntry(newFile, ENTRY_A_STORED),
Recommendation.UNCOMPRESS_OLD,
RecommendationReason.COMPRESSED_CHANGED_TO_UNCOMPRESSED));
}
@Test
public void testGeneratePreDiffPlan_OneEntry_UncompressedToCompressed() throws IOException {
// Test the migration of an entry from uncompressed (old archive) to compressed (new archive).
byte[] oldBytes = UnitTestZipArchive.makeTestZip(Collections.singletonList(ENTRY_A_STORED));
byte[] newBytes = UnitTestZipArchive.makeTestZip(Collections.singletonList(ENTRY_A_LEVEL_6));
File oldFile = storeAndMapArchive(oldBytes);
File newFile = storeAndMapArchive(newBytes);
PreDiffPlan plan = invokeGeneratePreDiffPlan(oldFile, newFile);
Assert.assertNotNull(plan);
// The plan should be to do nothing in the old archive (empty plan) and uncompress the entry in
// the new archive
Assert.assertTrue(plan.getOldFileUncompressionPlan().isEmpty());
Assert.assertEquals(1, plan.getNewFileUncompressionPlan().size());
Assert.assertEquals(
findRangeWithParams(newFile, ENTRY_A_LEVEL_6), plan.getNewFileUncompressionPlan().get(0));
checkRecommendation(plan, new QualifiedRecommendation(
findEntry(oldFile, ENTRY_A_STORED),
findEntry(newFile, ENTRY_A_LEVEL_6),
Recommendation.UNCOMPRESS_NEW,
RecommendationReason.UNCOMPRESSED_CHANGED_TO_COMPRESSED));
}
@Test
public void testGeneratePreDiffPlan_OneEntry_UncompressedToUndivinable() throws IOException {
// Test the migration of an entry from uncompressed (old archive) to compressed (new archive),
// but make the new entry un-divinable and therefore un-recompressible.
byte[] oldBytes = UnitTestZipArchive.makeTestZip(Collections.singletonList(ENTRY_A_STORED));
byte[] newBytes = UnitTestZipArchive.makeTestZip(Collections.singletonList(ENTRY_A_LEVEL_6));
File oldFile = storeAndMapArchive(oldBytes);
File newFile = storeAndMapArchive(newBytes);
// Deliberately break the entry in the new file so that it will not be divinable
corruptEntryData(newFile, ENTRY_A_LEVEL_6);
PreDiffPlan plan = invokeGeneratePreDiffPlan(oldFile, newFile);
Assert.assertNotNull(plan);
// The plan WOULD be to do nothing in the old archive (empty plan) and uncompress the entry in
// the new archive, but because the new entry is un-divinable it cannot be recompressed and so
// the plan for the new archive should be empty as well.
Assert.assertTrue(plan.getOldFileUncompressionPlan().isEmpty());
Assert.assertTrue(plan.getNewFileUncompressionPlan().isEmpty());
checkRecommendation(plan, new QualifiedRecommendation(
findEntry(oldFile, ENTRY_A_STORED),
findEntry(newFile, ENTRY_A_LEVEL_6),
Recommendation.UNCOMPRESS_NEITHER,
RecommendationReason.UNSUITABLE));
}
@Test
public void testGeneratePreDiffPlan_OneEntry_OldUncompressed_NewNonDeflate() throws IOException {
// Test the case where the entry is compressed with something other than deflate in the new
// archive; it is thus not reproducible, not divinable, and therefore cannot be uncompressed.
byte[] oldBytes = UnitTestZipArchive.makeTestZip(Collections.singletonList(ENTRY_A_STORED));
byte[] newBytes = UnitTestZipArchive.makeTestZip(Collections.singletonList(ENTRY_A_LEVEL_9));
File oldFile = storeAndMapArchive(oldBytes);
File newFile = storeAndMapArchive(newBytes);
corruptCompressionMethod(newFile, ENTRY_A_LEVEL_9);
PreDiffPlan plan = invokeGeneratePreDiffPlan(oldFile, newFile);
Assert.assertNotNull(plan);
// The plan should be to do nothing (empty plans) because the the entry in the old archive is
// already uncompressed and the entry in the new archive is not compressed with deflate (i.e.,
// cannot be recompressed so cannot be touched).
Assert.assertTrue(plan.getOldFileUncompressionPlan().isEmpty());
Assert.assertTrue(plan.getNewFileUncompressionPlan().isEmpty());
checkRecommendation(plan, new QualifiedRecommendation(
findEntry(oldFile, ENTRY_A_STORED),
findEntry(newFile, ENTRY_A_LEVEL_9),
Recommendation.UNCOMPRESS_NEITHER,
RecommendationReason.UNSUITABLE));
}
@Test
public void testGeneratePreDiffPlan_OneEntry_OldNonDeflate_NewUncompressed() throws IOException {
// Test the case where the entry is compressed with something other than deflate in the old
// archive; it can't be uncompressed, so there's no point in modifying the new entry either.
byte[] oldBytes = UnitTestZipArchive.makeTestZip(Collections.singletonList(ENTRY_A_LEVEL_9));
byte[] newBytes = UnitTestZipArchive.makeTestZip(Collections.singletonList(ENTRY_A_STORED));
File oldFile = storeAndMapArchive(oldBytes);
File newFile = storeAndMapArchive(newBytes);
corruptCompressionMethod(oldFile, ENTRY_A_LEVEL_9);
PreDiffPlan plan = invokeGeneratePreDiffPlan(oldFile, newFile);
Assert.assertNotNull(plan);
// The plan should be to do nothing (empty plans) because the the entry in the old archive is
// not compressed with deflate, so there is no point in trying to do anything at all.
Assert.assertTrue(plan.getOldFileUncompressionPlan().isEmpty());
Assert.assertTrue(plan.getNewFileUncompressionPlan().isEmpty());
checkRecommendation(plan, new QualifiedRecommendation(
findEntry(oldFile, ENTRY_A_LEVEL_9),
findEntry(newFile, ENTRY_A_STORED),
Recommendation.UNCOMPRESS_NEITHER,
RecommendationReason.UNSUITABLE));
}
@Test
public void testGeneratePreDiffPlan_OneEntry_BothNonDeflate() throws IOException {
// Test the case where the entry is compressed with something other than deflate; it is thus
// not reproducible, not divinable, and therefore cannot be uncompressed.
byte[] oldBytes = UnitTestZipArchive.makeTestZip(Collections.singletonList(ENTRY_A_LEVEL_6));
byte[] newBytes = UnitTestZipArchive.makeTestZip(Collections.singletonList(ENTRY_A_LEVEL_9));
File oldFile = storeAndMapArchive(oldBytes);
File newFile = storeAndMapArchive(newBytes);
corruptCompressionMethod(oldFile, ENTRY_A_LEVEL_6);
corruptCompressionMethod(newFile, ENTRY_A_LEVEL_9);
PreDiffPlan plan = invokeGeneratePreDiffPlan(oldFile, newFile);
Assert.assertNotNull(plan);
// The plan should be to do nothing (empty plans) because the entries are not compressed with
// deflate
Assert.assertTrue(plan.getOldFileUncompressionPlan().isEmpty());
Assert.assertTrue(plan.getNewFileUncompressionPlan().isEmpty());
checkRecommendation(plan, new QualifiedRecommendation(
findEntry(oldFile, ENTRY_A_LEVEL_6),
findEntry(newFile, ENTRY_A_LEVEL_9),
Recommendation.UNCOMPRESS_NEITHER,
RecommendationReason.UNSUITABLE));
}
@Test
public void testGeneratePreDiffPlan_TwoDifferentEntries_DifferentPaths() throws IOException {
// Test the case where file paths are different as well as content within those files, i.e. each
// entry is exclusive to its archive and is not the same
byte[] oldBytes = UnitTestZipArchive.makeTestZip(Collections.singletonList(ENTRY_A_LEVEL_6));
byte[] newBytes = UnitTestZipArchive.makeTestZip(Collections.singletonList(ENTRY_B_LEVEL_6));
File oldFile = storeAndMapArchive(oldBytes);
File newFile = storeAndMapArchive(newBytes);
PreDiffPlan plan = invokeGeneratePreDiffPlan(oldFile, newFile);
Assert.assertNotNull(plan);
// The plan should be to do nothing (empty plans) because entry A is only in the old archive and
// entry B is only in the new archive, so there is nothing to diff.
Assert.assertTrue(plan.getOldFileUncompressionPlan().isEmpty());
Assert.assertTrue(plan.getNewFileUncompressionPlan().isEmpty());
Assert.assertTrue(plan.getQualifiedRecommendations().isEmpty());
}
@Test
public void testGeneratePreDiffPlan_TwoEntriesEachArchive_SwappingOrder() throws IOException {
// Test the case where two entries in each archive have both changed, AND they have changed
// places in the file. The plan is supposed to be in file order, so that streaming is possible;
// check that it is so.
byte[] oldBytes =
UnitTestZipArchive.makeTestZip(Arrays.asList(ENTRY_A_LEVEL_6, ENTRY_B_LEVEL_6));
byte[] newBytes =
UnitTestZipArchive.makeTestZip(Arrays.asList(ENTRY_B_LEVEL_9, ENTRY_A_LEVEL_9));
File oldFile = storeAndMapArchive(oldBytes);
File newFile = storeAndMapArchive(newBytes);
PreDiffPlan plan = invokeGeneratePreDiffPlan(oldFile, newFile);
Assert.assertNotNull(plan);
// The plan should be to uncompress both entries, but the order is important. File order should
// be in both plans.
Assert.assertEquals(2, plan.getOldFileUncompressionPlan().size());
Assert.assertEquals(2, plan.getNewFileUncompressionPlan().size());
Assert.assertEquals(
findRangeWithoutParams(oldFile, ENTRY_A_LEVEL_6),
plan.getOldFileUncompressionPlan().get(0));
Assert.assertEquals(
findRangeWithoutParams(oldFile, ENTRY_B_LEVEL_6),
plan.getOldFileUncompressionPlan().get(1));
Assert.assertEquals(
findRangeWithParams(newFile, ENTRY_B_LEVEL_9), plan.getNewFileUncompressionPlan().get(0));
Assert.assertEquals(
findRangeWithParams(newFile, ENTRY_A_LEVEL_9), plan.getNewFileUncompressionPlan().get(1));
}
@Test
public void testGeneratePreDiffPlan_SimpleRename_Unchanged() throws IOException {
// Test the case where file paths are different but the uncompressed content is the same.
// The compression method used for both entries is identical, as are the compressed bytes.
byte[] oldBytes = UnitTestZipArchive.makeTestZip(Collections.singletonList(ENTRY_A_LEVEL_6));
byte[] newBytes =
UnitTestZipArchive.makeTestZip(Collections.singletonList(SHADOW_ENTRY_A_LEVEL_6));
File oldFile = storeAndMapArchive(oldBytes);
File newFile = storeAndMapArchive(newBytes);
PreDiffPlan plan = invokeGeneratePreDiffPlan(oldFile, newFile);
Assert.assertNotNull(plan);
// The plan should be to do nothing (empty plans) because the bytes are identical in both files
// so the entries should remain compressed. However, unlike the case where there was no match,
// there is now a qualified recommendation in the returned list.
Assert.assertTrue(plan.getOldFileUncompressionPlan().isEmpty());
Assert.assertTrue(plan.getNewFileUncompressionPlan().isEmpty());
checkRecommendation(
plan,
new QualifiedRecommendation(
findEntry(oldFile, ENTRY_A_LEVEL_6),
findEntry(newFile, SHADOW_ENTRY_A_LEVEL_6),
Recommendation.UNCOMPRESS_NEITHER,
RecommendationReason.COMPRESSED_BYTES_IDENTICAL));
}
@Test
public void testGeneratePreDiffPlan_SimpleRename_CompressionLevelChanged() throws IOException {
// Test the case where file paths are different but the uncompressed content is the same.
// The compression method used for each entry is different but the CRC32 is still the same, so
// unlike like the plan with identical entries this time the plan should be to uncompress both
// entries, allowing a super-efficient delta.
byte[] oldBytes = UnitTestZipArchive.makeTestZip(Collections.singletonList(ENTRY_A_LEVEL_6));
byte[] newBytes =
UnitTestZipArchive.makeTestZip(Collections.singletonList(SHADOW_ENTRY_A_LEVEL_9));
File oldFile = storeAndMapArchive(oldBytes);
File newFile = storeAndMapArchive(newBytes);
PreDiffPlan plan = invokeGeneratePreDiffPlan(oldFile, newFile);
Assert.assertNotNull(plan);
// The plan should be to uncompress both entries so that a super-efficient delta can be done.
Assert.assertEquals(1, plan.getOldFileUncompressionPlan().size());
Assert.assertEquals(
findRangeWithoutParams(oldFile, ENTRY_A_LEVEL_6),
plan.getOldFileUncompressionPlan().get(0));
Assert.assertEquals(1, plan.getNewFileUncompressionPlan().size());
Assert.assertEquals(
findRangeWithParams(newFile, SHADOW_ENTRY_A_LEVEL_9),
plan.getNewFileUncompressionPlan().get(0));
checkRecommendation(
plan,
new QualifiedRecommendation(
findEntry(oldFile, ENTRY_A_LEVEL_6),
findEntry(newFile, SHADOW_ENTRY_A_LEVEL_9),
Recommendation.UNCOMPRESS_BOTH,
RecommendationReason.COMPRESSED_BYTES_CHANGED));
}
@Test
public void testGeneratePreDiffPlan_ClonedAndCompressionLevelChanged() throws IOException {
// Test the case where an entry exists in both old and new APK with identical uncompressed
// content but different compressed content ***AND*** additionally a new copy exists in the new
// archive, also with identical uncompressed content and different compressed content, i.e.:
//
// OLD APK: NEW APK:
// ------------------------------------ -----------------------------------------------
// foo.xml (compressed level 6) foo.xml (compressed level 9, content unchanged)
// bar.xml (copy of foo.xml, compressed level 1)
//
// This test ensures that in such cases the foo.xml from the old apk is only enqueued for
// uncompression ONE TIME.
byte[] oldBytes = UnitTestZipArchive.makeTestZip(Collections.singletonList(ENTRY_A_LEVEL_6));
byte[] newBytes =
UnitTestZipArchive.makeTestZip(
Arrays.asList(SHADOW_ENTRY_A_LEVEL_1, SHADOW_ENTRY_A_LEVEL_9));
File oldFile = storeAndMapArchive(oldBytes);
File newFile = storeAndMapArchive(newBytes);
PreDiffPlan plan = invokeGeneratePreDiffPlan(oldFile, newFile);
Assert.assertNotNull(plan);
// The plan should be to uncompress both entries so that a super-efficient delta can be done.
// Critically there should only be ONE command for the old file uncompression step!
Assert.assertEquals(1, plan.getOldFileUncompressionPlan().size());
Assert.assertEquals(
findRangeWithoutParams(oldFile, ENTRY_A_LEVEL_6),
plan.getOldFileUncompressionPlan().get(0));
Assert.assertEquals(2, plan.getNewFileUncompressionPlan().size());
Assert.assertEquals(
findRangeWithParams(newFile, SHADOW_ENTRY_A_LEVEL_1),
plan.getNewFileUncompressionPlan().get(0));
Assert.assertEquals(
findRangeWithParams(newFile, SHADOW_ENTRY_A_LEVEL_9),
plan.getNewFileUncompressionPlan().get(1));
checkRecommendation(
plan,
new QualifiedRecommendation(
findEntry(oldFile, ENTRY_A_LEVEL_6),
findEntry(newFile, SHADOW_ENTRY_A_LEVEL_1),
Recommendation.UNCOMPRESS_BOTH,
RecommendationReason.COMPRESSED_BYTES_CHANGED),
new QualifiedRecommendation(
findEntry(oldFile, ENTRY_A_LEVEL_6),
findEntry(newFile, SHADOW_ENTRY_A_LEVEL_9),
Recommendation.UNCOMPRESS_BOTH,
RecommendationReason.COMPRESSED_BYTES_CHANGED));
}
@Test
public void testGeneratePreDiffPlan_SimpleRename_CompressedToUncompressed() throws IOException {
// Test the case where file paths are different but the uncompressed content is the same.
// The compression method is changed from compressed to uncompressed but the rename should still
// be detected and the plan should be to uncompress the old entry only.
byte[] oldBytes = UnitTestZipArchive.makeTestZip(Collections.singletonList(ENTRY_A_LEVEL_6));
byte[] newBytes =
UnitTestZipArchive.makeTestZip(Collections.singletonList(SHADOW_ENTRY_A_STORED));
File oldFile = storeAndMapArchive(oldBytes);
File newFile = storeAndMapArchive(newBytes);
PreDiffPlan plan = invokeGeneratePreDiffPlan(oldFile, newFile);
Assert.assertNotNull(plan);
// The plan should be to uncompress the old entry so that a super-efficient delta can be done.
// The new entry isn't touched because it is already uncompressed.
Assert.assertEquals(1, plan.getOldFileUncompressionPlan().size());
Assert.assertEquals(
findRangeWithoutParams(oldFile, ENTRY_A_LEVEL_6),
plan.getOldFileUncompressionPlan().get(0));
Assert.assertTrue(plan.getNewFileUncompressionPlan().isEmpty());
checkRecommendation(
plan,
new QualifiedRecommendation(
findEntry(oldFile, ENTRY_A_LEVEL_6),
findEntry(newFile, SHADOW_ENTRY_A_STORED),
Recommendation.UNCOMPRESS_OLD,
RecommendationReason.COMPRESSED_CHANGED_TO_UNCOMPRESSED));
}
@Test
public void testGeneratePreDiffPlan_SimpleRename_UncompressedToCompressed() throws IOException {
// Test the case where file paths are different but the uncompressed content is the same.
// The compression method is changed from uncompressed to compressed but the rename should still
// be detected and the plan should be to uncompress the new entry only.
byte[] oldBytes = UnitTestZipArchive.makeTestZip(Collections.singletonList(ENTRY_A_STORED));
byte[] newBytes =
UnitTestZipArchive.makeTestZip(Collections.singletonList(SHADOW_ENTRY_A_LEVEL_6));
File oldFile = storeAndMapArchive(oldBytes);
File newFile = storeAndMapArchive(newBytes);
PreDiffPlan plan = invokeGeneratePreDiffPlan(oldFile, newFile);
Assert.assertNotNull(plan);
// The plan should be to uncompress the new entry so that a super-efficient delta can be done.
// The old entry isn't touched because it is already uncompressed.
Assert.assertTrue(plan.getOldFileUncompressionPlan().isEmpty());
Assert.assertEquals(1, plan.getNewFileUncompressionPlan().size());
Assert.assertEquals(
findRangeWithParams(newFile, SHADOW_ENTRY_A_LEVEL_6),
plan.getNewFileUncompressionPlan().get(0));
checkRecommendation(
plan,
new QualifiedRecommendation(
findEntry(oldFile, ENTRY_A_STORED),
findEntry(newFile, SHADOW_ENTRY_A_LEVEL_6),
Recommendation.UNCOMPRESS_NEW,
RecommendationReason.UNCOMPRESSED_CHANGED_TO_COMPRESSED));
}
}