Home | History | Annotate | Download | only in applier
      1 // Copyright 2016 Google Inc. All rights reserved.
      2 //
      3 // Licensed under the Apache License, Version 2.0 (the "License");
      4 // you may not use this file except in compliance with the License.
      5 // You may obtain a copy of the License at
      6 //
      7 //     http://www.apache.org/licenses/LICENSE-2.0
      8 //
      9 // Unless required by applicable law or agreed to in writing, software
     10 // distributed under the License is distributed on an "AS IS" BASIS,
     11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12 // See the License for the specific language governing permissions and
     13 // limitations under the License.
     14 
     15 package com.google.archivepatcher.applier;
     16 
     17 import com.google.archivepatcher.shared.JreDeflateParameters;
     18 import com.google.archivepatcher.shared.PatchConstants;
     19 import com.google.archivepatcher.shared.UnitTestZipEntry;
     20 
     21 import org.junit.After;
     22 import org.junit.Assert;
     23 import org.junit.Before;
     24 import org.junit.Test;
     25 import org.junit.runner.RunWith;
     26 import org.junit.runners.JUnit4;
     27 
     28 import java.io.ByteArrayInputStream;
     29 import java.io.ByteArrayOutputStream;
     30 import java.io.DataInputStream;
     31 import java.io.DataOutputStream;
     32 import java.io.File;
     33 import java.io.FileInputStream;
     34 import java.io.FileOutputStream;
     35 import java.io.IOException;
     36 import java.io.InputStream;
     37 import java.io.OutputStream;
     38 import java.util.concurrent.atomic.AtomicBoolean;
     39 
     40 /**
     41  * Tests for {@link FileByFileV1DeltaApplier}.
     42  */
     43 @RunWith(JUnit4.class)
     44 @SuppressWarnings("javadoc")
     45 public class FileByFileV1DeltaApplierTest {
     46 
     47   // These constants are used to construct all the blobs (note the OLD and NEW contents):
     48   //   old file := UNCOMPRESSED_HEADER + COMPRESSED_OLD_CONTENT + UNCOMPRESSED_TRAILER
     49   //   delta-friendly old file := UNCOMPRESSED_HEADER + UNCOMPRESSED_OLD_CONTENT +
     50   //                              UNCOMPRESSED_TRAILER
     51   //   delta-friendly new file := UNCOMPRESSED_HEADER + UNCOMPRESSED_NEW_CONTENT +
     52   //                              UNCOMPRESSED_TRAILER
     53   //   new file := UNCOMPRESSED_HEADER + COMPRESSED_NEW_CONTENT + UNCOMPRESSED_TRAILIER
     54   // NB: The patch *applier* is agnostic to the format of the file, and so it doesn't have to be a
     55   //     valid zip or zip-like archive.
     56   private static final JreDeflateParameters PARAMS1 = JreDeflateParameters.of(6, 0, true);
     57   private static final String OLD_CONTENT = "This is Content the Old";
     58   private static final UnitTestZipEntry OLD_ENTRY =
     59       new UnitTestZipEntry("/foo", PARAMS1.level, PARAMS1.nowrap, OLD_CONTENT, null);
     60   private static final String NEW_CONTENT = "Rambunctious Absinthe-Loving Stegosaurus";
     61   private static final UnitTestZipEntry NEW_ENTRY =
     62       new UnitTestZipEntry("/foo", PARAMS1.level, PARAMS1.nowrap, NEW_CONTENT, null);
     63   private static final byte[] UNCOMPRESSED_HEADER = new byte[] {0, 1, 2, 3, 4};
     64   private static final byte[] UNCOMPRESSED_OLD_CONTENT = OLD_ENTRY.getUncompressedBinaryContent();
     65   private static final byte[] COMPRESSED_OLD_CONTENT = OLD_ENTRY.getCompressedBinaryContent();
     66   private static final byte[] UNCOMPRESSED_NEW_CONTENT = NEW_ENTRY.getUncompressedBinaryContent();
     67   private static final byte[] COMPRESSED_NEW_CONTENT = NEW_ENTRY.getCompressedBinaryContent();
     68   private static final byte[] UNCOMPRESSED_TRAILER = new byte[] {5, 6, 7, 8, 9};
     69   private static final String BSDIFF_DELTA = "1337 h4x0r";
     70 
     71   /**
     72    * Where to store temp files.
     73    */
     74   private File tempDir;
     75 
     76   /**
     77    * The old file.
     78    */
     79   private File oldFile;
     80 
     81   /**
     82    * Bytes that describe a patch to convert the old file to the new file.
     83    */
     84   private byte[] patchBytes;
     85 
     86   /**
     87    * Bytes that describe the new file.
     88    */
     89   private byte[] expectedNewBytes;
     90 
     91   /**
     92    * For debugging test issues, it is convenient to be able to see these bytes in the debugger
     93    * instead of on the filesystem.
     94    */
     95   private byte[] oldFileBytes;
     96 
     97   /**
     98    * Again, for debugging test issues, it is convenient to be able to see these bytes in the
     99    * debugger instead of on the filesystem.
    100    */
    101   private byte[] expectedDeltaFriendlyOldFileBytes;
    102 
    103   /**
    104    * To mock the dependency on bsdiff, a subclass of FileByFileV1DeltaApplier is made that always
    105    * returns a testing delta applier. This delta applier asserts that the old content is as
    106    * expected, and "patches" it by simply writing the expected *new* content to the output stream.
    107    */
    108   private FileByFileV1DeltaApplier fakeApplier;
    109 
    110   @Before
    111   public void setUp() throws IOException {
    112     // Creates the following resources:
    113     // 1. The old file, on disk (and in-memory, for convenience).
    114     // 2. The new file, in memory only (for comparing results at the end).
    115     // 3. The patch, in memory.
    116 
    117     File tempFile = File.createTempFile("foo", "bar");
    118     tempDir = tempFile.getParentFile();
    119     tempFile.delete();
    120     oldFile = File.createTempFile("fbfv1dat", "old");
    121     oldFile.deleteOnExit();
    122 
    123     // Write the old file to disk:
    124     ByteArrayOutputStream buffer = new ByteArrayOutputStream();
    125     buffer.write(UNCOMPRESSED_HEADER);
    126     buffer.write(COMPRESSED_OLD_CONTENT);
    127     buffer.write(UNCOMPRESSED_TRAILER);
    128     oldFileBytes = buffer.toByteArray();
    129     FileOutputStream out = new FileOutputStream(oldFile);
    130     out.write(oldFileBytes);
    131     out.flush();
    132     out.close();
    133 
    134     // Write the delta-friendly old file to a byte array
    135     buffer = new ByteArrayOutputStream();
    136     buffer.write(UNCOMPRESSED_HEADER);
    137     buffer.write(UNCOMPRESSED_OLD_CONTENT);
    138     buffer.write(UNCOMPRESSED_TRAILER);
    139     expectedDeltaFriendlyOldFileBytes = buffer.toByteArray();
    140 
    141     // Write the new file to a byte array
    142     buffer = new ByteArrayOutputStream();
    143     buffer.write(UNCOMPRESSED_HEADER);
    144     buffer.write(COMPRESSED_NEW_CONTENT);
    145     buffer.write(UNCOMPRESSED_TRAILER);
    146     expectedNewBytes = buffer.toByteArray();
    147 
    148     // Finally, write the patch that should transform old to new
    149     patchBytes = writePatch();
    150 
    151     // Initialize fake delta applier to mock out dependency on bsdiff
    152     fakeApplier = new FileByFileV1DeltaApplier(tempDir) {
    153           @Override
    154           protected DeltaApplier getDeltaApplier() {
    155             return new FakeDeltaApplier();
    156           }
    157         };
    158   }
    159 
    160   /**
    161    * Write a patch that will convert the old file to the new file, and return it.
    162    * @return the patch, as a byte array
    163    * @throws IOException if anything goes wrong
    164    */
    165   private byte[] writePatch() throws IOException {
    166     long deltaFriendlyOldFileSize =
    167         UNCOMPRESSED_HEADER.length + UNCOMPRESSED_OLD_CONTENT.length + UNCOMPRESSED_TRAILER.length;
    168     long deltaFriendlyNewFileSize =
    169         UNCOMPRESSED_HEADER.length + UNCOMPRESSED_NEW_CONTENT.length + UNCOMPRESSED_TRAILER.length;
    170 
    171     ByteArrayOutputStream buffer = new ByteArrayOutputStream();
    172     DataOutputStream dataOut = new DataOutputStream(buffer);
    173     // Now write a patch, independent of the PatchWrite code.
    174     dataOut.write(PatchConstants.IDENTIFIER.getBytes("US-ASCII"));
    175     dataOut.writeInt(0); // Flags (reserved)
    176     dataOut.writeLong(deltaFriendlyOldFileSize);
    177 
    178     // Write a single uncompress instruction to uncompress the compressed content in oldFile
    179     dataOut.writeInt(1); // num instructions that follow
    180     dataOut.writeLong(UNCOMPRESSED_HEADER.length);
    181     dataOut.writeLong(COMPRESSED_OLD_CONTENT.length);
    182 
    183     // Write a single compress instruction to recompress the uncompressed content in the
    184     // delta-friendly old file.
    185     dataOut.writeInt(1); // num instructions that follow
    186     dataOut.writeLong(UNCOMPRESSED_HEADER.length);
    187     dataOut.writeLong(UNCOMPRESSED_NEW_CONTENT.length);
    188     dataOut.write(PatchConstants.CompatibilityWindowId.DEFAULT_DEFLATE.patchValue);
    189     dataOut.write(PARAMS1.level);
    190     dataOut.write(PARAMS1.strategy);
    191     dataOut.write(PARAMS1.nowrap ? 1 : 0);
    192 
    193     // Write a delta. This test class uses its own delta applier to intercept and mangle the data.
    194     dataOut.writeInt(1);
    195     dataOut.write(PatchConstants.DeltaFormat.BSDIFF.patchValue);
    196     dataOut.writeLong(0); // i.e., start of the working range in the delta-friendly old file
    197     dataOut.writeLong(deltaFriendlyOldFileSize); // i.e., length of the working range in old
    198     dataOut.writeLong(0); // i.e., start of the working range in the delta-friendly new file
    199     dataOut.writeLong(deltaFriendlyNewFileSize); // i.e., length of the working range in new
    200 
    201     // Write the length of the delta and the delta itself. Again, this test class uses its own
    202     // delta applier; so this is irrelevant.
    203     dataOut.writeLong(BSDIFF_DELTA.length());
    204     dataOut.write(BSDIFF_DELTA.getBytes("US-ASCII"));
    205     dataOut.flush();
    206     return buffer.toByteArray();
    207   }
    208 
    209   private class FakeDeltaApplier implements DeltaApplier {
    210   @SuppressWarnings("resource")
    211   @Override
    212     public void applyDelta(File oldBlob, InputStream deltaIn, OutputStream newBlobOut)
    213         throws IOException {
    214       // Check the patch is as expected
    215       DataInputStream deltaData = new DataInputStream(deltaIn);
    216       byte[] actualDeltaDataRead = new byte[BSDIFF_DELTA.length()];
    217       deltaData.readFully(actualDeltaDataRead);
    218       Assert.assertArrayEquals(BSDIFF_DELTA.getBytes("US-ASCII"), actualDeltaDataRead);
    219 
    220       // Check that the old data is as expected
    221       int oldSize = (int) oldBlob.length();
    222       byte[] oldData = new byte[oldSize];
    223       FileInputStream oldBlobIn = new FileInputStream(oldBlob);
    224       DataInputStream oldBlobDataIn = new DataInputStream(oldBlobIn);
    225       oldBlobDataIn.readFully(oldData);
    226       Assert.assertArrayEquals(expectedDeltaFriendlyOldFileBytes, oldData);
    227 
    228       // "Convert" the old blob to the new blow as if this were a real patching algorithm.
    229       newBlobOut.write(UNCOMPRESSED_HEADER);
    230       newBlobOut.write(NEW_ENTRY.getUncompressedBinaryContent());
    231       newBlobOut.write(UNCOMPRESSED_TRAILER);
    232     }
    233   }
    234 
    235   @After
    236   public void tearDown() {
    237     try {
    238       oldFile.delete();
    239     } catch (Exception ignored) {
    240       // Nothing
    241     }
    242   }
    243 
    244   @Test
    245   public void testApplyDelta() throws IOException {
    246     // Test all aspects of patch apply: copying, uncompressing and recompressing ranges.
    247     // This test uses the subclasses applier to apply the test patch to the old file, producing the
    248     // new file. Along the way the entry is uncompressed, altered by the testing delta applier, and
    249     // recompressed. It's deceptively simple below, but this is a lot of moving parts.
    250     ByteArrayOutputStream actualNewBlobOut = new ByteArrayOutputStream();
    251     fakeApplier.applyDelta(oldFile, new ByteArrayInputStream(patchBytes), actualNewBlobOut);
    252     Assert.assertArrayEquals(expectedNewBytes, actualNewBlobOut.toByteArray());
    253   }
    254 
    255   @Test
    256   public void testApplyDelta_DoesntCloseStream() throws IOException {
    257     // Test for https://github.com/andrewhayden/archive-patcher/issues/6
    258     final AtomicBoolean closed = new AtomicBoolean(false);
    259     ByteArrayOutputStream actualNewBlobOut = new ByteArrayOutputStream() {
    260       @Override
    261       public void close() throws IOException {
    262         closed.set(true);
    263       }
    264     };
    265     fakeApplier.applyDelta(oldFile, new ByteArrayInputStream(patchBytes), actualNewBlobOut);
    266     Assert.assertArrayEquals(expectedNewBytes, actualNewBlobOut.toByteArray());
    267     Assert.assertFalse(closed.get());
    268   }
    269 
    270 }
    271