• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
setUp()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    */
writePatch()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
applyDelta(File oldBlob, InputStream deltaIn, OutputStream newBlobOut)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
tearDown()236   public void tearDown() {
237     try {
238       oldFile.delete();
239     } catch (Exception ignored) {
240       // Nothing
241     }
242   }
243 
244   @Test
testApplyDelta()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
testApplyDelta_DoesntCloseStream()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