| // 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.applier; |
| |
| import com.google.archivepatcher.shared.JreDeflateParameters; |
| import com.google.archivepatcher.shared.PatchConstants; |
| import com.google.archivepatcher.shared.UnitTestZipEntry; |
| |
| 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; |
| |
| import java.io.ByteArrayInputStream; |
| import java.io.ByteArrayOutputStream; |
| import java.io.DataInputStream; |
| import java.io.DataOutputStream; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| |
| /** |
| * Tests for {@link FileByFileV1DeltaApplier}. |
| */ |
| @RunWith(JUnit4.class) |
| @SuppressWarnings("javadoc") |
| public class FileByFileV1DeltaApplierTest { |
| |
| // These constants are used to construct all the blobs (note the OLD and NEW contents): |
| // old file := UNCOMPRESSED_HEADER + COMPRESSED_OLD_CONTENT + UNCOMPRESSED_TRAILER |
| // delta-friendly old file := UNCOMPRESSED_HEADER + UNCOMPRESSED_OLD_CONTENT + |
| // UNCOMPRESSED_TRAILER |
| // delta-friendly new file := UNCOMPRESSED_HEADER + UNCOMPRESSED_NEW_CONTENT + |
| // UNCOMPRESSED_TRAILER |
| // new file := UNCOMPRESSED_HEADER + COMPRESSED_NEW_CONTENT + UNCOMPRESSED_TRAILIER |
| // NB: The patch *applietr* is agnostic to the format of the file, and so it doesn't have to be a |
| // valid zip or zip-like archive. |
| private static final JreDeflateParameters PARAMS1 = JreDeflateParameters.of(6, 0, true); |
| private static final String OLD_CONTENT = "This is Content the Old"; |
| private static final UnitTestZipEntry OLD_ENTRY = |
| new UnitTestZipEntry("/foo", PARAMS1.level, PARAMS1.nowrap, OLD_CONTENT, null); |
| private static final String NEW_CONTENT = "Rambunctious Absinthe-Loving Stegosaurus"; |
| private static final UnitTestZipEntry NEW_ENTRY = |
| new UnitTestZipEntry("/foo", PARAMS1.level, PARAMS1.nowrap, NEW_CONTENT, null); |
| private static final byte[] UNCOMPRESSED_HEADER = new byte[] {0, 1, 2, 3, 4}; |
| private static final byte[] UNCOMPRESSED_OLD_CONTENT = OLD_ENTRY.getUncompressedBinaryContent(); |
| private static final byte[] COMPRESSED_OLD_CONTENT = OLD_ENTRY.getCompressedBinaryContent(); |
| private static final byte[] UNCOMPRESSED_NEW_CONTENT = NEW_ENTRY.getUncompressedBinaryContent(); |
| private static final byte[] COMPRESSED_NEW_CONTENT = NEW_ENTRY.getCompressedBinaryContent(); |
| private static final byte[] UNCOMPRESSED_TRAILER = new byte[] {5, 6, 7, 8, 9}; |
| private static final String BSDIFF_DELTA = "1337 h4x0r"; |
| |
| /** |
| * Where to store temp files. |
| */ |
| private File tempDir; |
| |
| /** |
| * The old file. |
| */ |
| private File oldFile; |
| |
| /** |
| * Bytes that describe a patch to convert the old file to the new file. |
| */ |
| private byte[] patchBytes; |
| |
| /** |
| * Bytes that describe the new file. |
| */ |
| private byte[] expectedNewBytes; |
| |
| /** |
| * For debugging test issues, it is convenient to be able to see these bytes in the debugger |
| * instead of on the filesystem. |
| */ |
| private byte[] oldFileBytes; |
| |
| private byte[] expectedDeltaFriendlyOldFileBytes; |
| |
| @Before |
| public void setUp() throws IOException { |
| File tempFile = File.createTempFile("foo", "bar"); |
| tempDir = tempFile.getParentFile(); |
| tempFile.delete(); |
| oldFile = File.createTempFile("fbfv1dat", "old"); |
| oldFile.deleteOnExit(); |
| |
| // Write the old file to disk: |
| ByteArrayOutputStream buffer = new ByteArrayOutputStream(); |
| buffer.write(UNCOMPRESSED_HEADER); |
| buffer.write(COMPRESSED_OLD_CONTENT); |
| buffer.write(UNCOMPRESSED_TRAILER); |
| oldFileBytes = buffer.toByteArray(); |
| FileOutputStream out = new FileOutputStream(oldFile); |
| out.write(oldFileBytes); |
| out.flush(); |
| out.close(); |
| |
| // Write the delta-friendly old file to a byte array |
| buffer = new ByteArrayOutputStream(); |
| buffer.write(UNCOMPRESSED_HEADER); |
| buffer.write(UNCOMPRESSED_OLD_CONTENT); |
| buffer.write(UNCOMPRESSED_TRAILER); |
| expectedDeltaFriendlyOldFileBytes = buffer.toByteArray(); |
| |
| // Write the new file to a byte array |
| buffer = new ByteArrayOutputStream(); |
| buffer.write(UNCOMPRESSED_HEADER); |
| buffer.write(COMPRESSED_NEW_CONTENT); |
| buffer.write(UNCOMPRESSED_TRAILER); |
| expectedNewBytes = buffer.toByteArray(); |
| |
| // Finally, write the patch that should transform old to new |
| patchBytes = writePatch(); |
| } |
| |
| /** |
| * Write a patch that will convert the old file to the new file, and return it. |
| * @return the patch, as a byte array |
| * @throws IOException if anything goes wrong |
| */ |
| private byte[] writePatch() throws IOException { |
| long deltaFriendlyOldFileSize = |
| UNCOMPRESSED_HEADER.length + UNCOMPRESSED_OLD_CONTENT.length + UNCOMPRESSED_TRAILER.length; |
| long deltaFriendlyNewFileSize = |
| UNCOMPRESSED_HEADER.length + UNCOMPRESSED_NEW_CONTENT.length + UNCOMPRESSED_TRAILER.length; |
| |
| ByteArrayOutputStream buffer = new ByteArrayOutputStream(); |
| DataOutputStream dataOut = new DataOutputStream(buffer); |
| // Now write a patch, independent of the PatchWrite code. |
| dataOut.write(PatchConstants.IDENTIFIER.getBytes("US-ASCII")); |
| dataOut.writeInt(0); // Flags (reserved) |
| dataOut.writeLong(deltaFriendlyOldFileSize); |
| |
| // Write a single uncompress instruction to uncompress the compressed content in oldFile |
| dataOut.writeInt(1); // num instructions that follow |
| dataOut.writeLong(UNCOMPRESSED_HEADER.length); |
| dataOut.writeLong(COMPRESSED_OLD_CONTENT.length); |
| |
| // Write a single compress instruction to recompress the uncompressed content in the |
| // delta-friendly old file. |
| dataOut.writeInt(1); // num instructions that follow |
| dataOut.writeLong(UNCOMPRESSED_HEADER.length); |
| dataOut.writeLong(UNCOMPRESSED_NEW_CONTENT.length); |
| dataOut.write(PatchConstants.CompatibilityWindowId.DEFAULT_DEFLATE.patchValue); |
| dataOut.write(PARAMS1.level); |
| dataOut.write(PARAMS1.strategy); |
| dataOut.write(PARAMS1.nowrap ? 1 : 0); |
| |
| // Write a delta. This test class uses its own delta applier to intercept and mangle the data. |
| dataOut.writeInt(1); |
| dataOut.write(PatchConstants.DeltaFormat.BSDIFF.patchValue); |
| dataOut.writeLong(0); // i.e., start of the working range in the delta-friendly old file |
| dataOut.writeLong(deltaFriendlyOldFileSize); // i.e., length of the working range in old |
| dataOut.writeLong(0); // i.e., start of the working range in the delta-friendly new file |
| dataOut.writeLong(deltaFriendlyNewFileSize); // i.e., length of the working range in new |
| |
| // Write the length of the delta and the delta itself. Again, this test class uses its own |
| // delta applier; so this is irrelevant. |
| dataOut.writeLong(BSDIFF_DELTA.length()); |
| dataOut.write(BSDIFF_DELTA.getBytes("US-ASCII")); |
| dataOut.flush(); |
| return buffer.toByteArray(); |
| } |
| |
| private class FakeDeltaApplier implements DeltaApplier { |
| @SuppressWarnings("resource") |
| @Override |
| public void applyDelta(File oldBlob, InputStream deltaIn, OutputStream newBlobOut) |
| throws IOException { |
| // Check the patch is as expected |
| DataInputStream deltaData = new DataInputStream(deltaIn); |
| byte[] actualDeltaDataRead = new byte[BSDIFF_DELTA.length()]; |
| deltaData.readFully(actualDeltaDataRead); |
| Assert.assertArrayEquals(BSDIFF_DELTA.getBytes("US-ASCII"), actualDeltaDataRead); |
| |
| // Check that the old data is as expected |
| int oldSize = (int) oldBlob.length(); |
| byte[] oldData = new byte[oldSize]; |
| FileInputStream oldBlobIn = new FileInputStream(oldBlob); |
| DataInputStream oldBlobDataIn = new DataInputStream(oldBlobIn); |
| oldBlobDataIn.readFully(oldData); |
| Assert.assertArrayEquals(expectedDeltaFriendlyOldFileBytes, oldData); |
| |
| // "Convert" the old blob to the new blow as if this were a real patching algorithm. |
| newBlobOut.write(UNCOMPRESSED_HEADER); |
| newBlobOut.write(NEW_ENTRY.getUncompressedBinaryContent()); |
| newBlobOut.write(UNCOMPRESSED_TRAILER); |
| } |
| } |
| |
| @After |
| public void tearDown() { |
| try { |
| oldFile.delete(); |
| } catch (Exception ignored) { |
| // Nothing |
| } |
| } |
| |
| @Test |
| public void testApplyDelta() throws IOException { |
| // Test all aspects of patch apply: copying, uncompressing and recompressing ranges. |
| // |
| // To mock the dependency on bsdiff, a subclass of FileByFileV1DeltaApplier is made that always |
| // returns a testing delta applier. This delta applier asserts that the old content is as |
| // expected, and "patches" it by simply writing the expected *new* content to the output stream. |
| // |
| // The test harness creates the following resources: |
| // 1. The old file, on disk (and in-memory, for convenience). |
| // 2. The new file, in memory only (for comparing results at the end). |
| // 3. The patch, in memory. |
| // |
| // This test uses the subclasses applier to apply the test patch to the old file, producing the |
| // new file. Along the way the entry is uncompressed, altered by the testing delta applier, and |
| // recompressed. It's deceptively simple below, but this is a lot of moving parts. |
| FileByFileV1DeltaApplier applier = |
| new FileByFileV1DeltaApplier(tempDir) { |
| @Override |
| protected DeltaApplier getDeltaApplier() { |
| return new FakeDeltaApplier(); |
| } |
| }; |
| ByteArrayOutputStream actualNewBlobOut = new ByteArrayOutputStream(); |
| applier.applyDelta(oldFile, new ByteArrayInputStream(patchBytes), actualNewBlobOut); |
| Assert.assertArrayEquals(expectedNewBytes, actualNewBlobOut.toByteArray()); |
| } |
| } |