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