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.bsdiff;
16 
17 import com.google.archivepatcher.applier.PatchFormatException;
18 
19 import org.junit.After;
20 import org.junit.Assert;
21 import org.junit.Before;
22 import org.junit.Test;
23 import org.junit.runner.RunWith;
24 import org.junit.runners.JUnit4;
25 
26 import java.io.ByteArrayInputStream;
27 import java.io.ByteArrayOutputStream;
28 import java.io.File;
29 import java.io.FileOutputStream;
30 import java.io.IOException;
31 import java.io.InputStream;
32 import java.io.OutputStream;
33 import java.io.RandomAccessFile;
34 
35 /**
36  * Tests for {@link BsPatch}.
37  */
38 @RunWith(JUnit4.class)
39 public class BsPatchTest {
40 
41   private static final String SIGNATURE = "ENDSLEY/BSDIFF43";
42   private byte[] buffer1;
43   private byte[] buffer2;
44 
45   /**
46    * The tests need access to an actual File object for the "old file", so that it can be used as
47    * the argument to a RandomAccessFile constructor... but the old file is a resource loaded at test
48    * run-time, potentially from a JAR, and therefore a copy must be made in the filesystem to access
49    * via RandomAccessFile. This is not true for the new file or the patch file, both of which are
50    * streamable.
51    */
52   private File oldFile;
53 
54   @Before
setUp()55   public void setUp() throws IOException {
56     buffer1 = new byte[6];
57     buffer2 = new byte[6];
58     try {
59       oldFile = File.createTempFile("archive_patcher", "old");
60       oldFile.deleteOnExit();
61     } catch (IOException e) {
62       if (oldFile != null) {
63         oldFile.delete();
64       }
65       throw e;
66     }
67   }
68 
69   @After
tearDown()70   public void tearDown() {
71     if (oldFile != null) {
72       oldFile.delete();
73     }
74     oldFile = null;
75   }
76 
77   @Test
testTransformBytes()78   public void testTransformBytes() throws IOException {
79     // In this case the "patch stream" is just a stream of addends that transformBytes(...) will
80     // apply to the old data file.
81     final byte[] patchInput = "this is a sample string to read".getBytes("US-ASCII");
82     final ByteArrayInputStream patchInputStream = new ByteArrayInputStream(patchInput);
83     copyToOldFile("bsdifftest_partial_a.txt");
84     RandomAccessFile oldData = new RandomAccessFile(oldFile, "r");
85     final byte[] expectedNewData = readTestData("bsdifftest_partial_b.bin");
86     ByteArrayOutputStream newData = new ByteArrayOutputStream();
87     BsPatch.transformBytes(patchInput.length, patchInputStream, oldData, newData, buffer1, buffer2);
88     byte[] actual = newData.toByteArray();
89     Assert.assertArrayEquals(expectedNewData, actual);
90   }
91 
92   @Test
testTransformBytes_Error_NotEnoughBytes()93   public void testTransformBytes_Error_NotEnoughBytes() throws IOException {
94     // This test sets up a trivial 1-byte "patch" (addends) stream but then asks
95     // transformBytes(...) to apply *2* bytes, which should fail when it hits EOF.
96     final InputStream patchIn = new ByteArrayInputStream(new byte[] {(byte) 0x00});
97     copyToOldFile("bsdifftest_partial_a.txt"); // Any file would work here
98     RandomAccessFile oldData = new RandomAccessFile(oldFile, "r");
99     try {
100       BsPatch.transformBytes(2, patchIn, oldData, new ByteArrayOutputStream(), buffer1, buffer2);
101       Assert.fail("Read past EOF");
102     } catch (IOException expected) {
103       // Pass
104     }
105   }
106 
107   @Test
testTransformBytes_Error_JunkPatch()108   public void testTransformBytes_Error_JunkPatch() throws IOException {
109     final byte[] patchInput = "this is a second sample string to read".getBytes("US-ASCII");
110     final ByteArrayInputStream patchInputStream = new ByteArrayInputStream(patchInput);
111     copyToOldFile("bsdifftest_partial_a.txt"); // Any file would work here
112     RandomAccessFile oldData = new RandomAccessFile(oldFile, "r");
113     ByteArrayOutputStream newData = new ByteArrayOutputStream();
114     try {
115       BsPatch.transformBytes(
116           patchInput.length, patchInputStream, oldData, newData, buffer1, buffer2);
117       Assert.fail("Should have thrown an IOException");
118     } catch (IOException expected) {
119       // Pass
120     }
121   }
122 
123   @Test
testTransformBytes_Error_JunkPatch_Underflow()124   public void testTransformBytes_Error_JunkPatch_Underflow() throws IOException {
125     final byte[] patchInput = "this is a sample string".getBytes("US-ASCII");
126     final ByteArrayInputStream patchInputStream = new ByteArrayInputStream(patchInput);
127     copyToOldFile("bsdifftest_partial_a.txt");
128     RandomAccessFile oldData = new RandomAccessFile(oldFile, "r");
129     final byte[] buffer1 = new byte[6];
130     final byte[] buffer2 = new byte[6];
131 
132     ByteArrayOutputStream newData = new ByteArrayOutputStream();
133     try {
134       BsPatch.transformBytes(
135           patchInput.length + 1, patchInputStream, oldData, newData, buffer1, buffer2);
136       Assert.fail("Should have thrown an IOException");
137     } catch (IOException expected) {
138       // Pass
139     }
140   }
141 
142   @Test
testApplyPatch_ContrivedData()143   public void testApplyPatch_ContrivedData() throws Exception {
144     invokeApplyPatch(
145         "bsdifftest_internal_blob_a.bin",
146         "bsdifftest_internal_patch_a_to_b.bin",
147         "bsdifftest_internal_blob_b.bin");
148   }
149 
150   @Test
testApplyPatch_BetterData()151   public void testApplyPatch_BetterData() throws Exception {
152     invokeApplyPatch(
153         "bsdifftest_minimal_blob_a.bin",
154         "bsdifftest_minimal_patch_a_to_b.bin",
155         "bsdifftest_minimal_blob_b.bin");
156   }
157 
158   @Test
testApplyPatch_BadSignature()159   public void testApplyPatch_BadSignature() throws Exception {
160     createEmptyOldFile(10);
161     String junkSignature = "WOOOOOO/BSDIFF43"; // Correct length, wrong content
162     InputStream patchIn =
163         makePatch(
164             junkSignature,
165             10, // newLength
166             10, // diffSegmentLength
167             0, // copySegmentLength
168             0, // offsetToNextInput
169             new byte[10] // addends
170             );
171     ByteArrayOutputStream newData = new ByteArrayOutputStream();
172     try {
173       BsPatch.applyPatch(new RandomAccessFile(oldFile, "r"), newData, patchIn);
174       Assert.fail("Read patch with bad signature");
175     } catch (PatchFormatException expected) {
176       // No way to mock the internal logic, so resort to testing exception string for coverage
177       String actual = expected.getMessage();
178       Assert.assertEquals("bad signature", actual);
179     }
180   }
181 
182   @Test
testApplyPatch_NewLengthNegative()183   public void testApplyPatch_NewLengthNegative() throws Exception {
184     createEmptyOldFile(10);
185     InputStream patchIn =
186         makePatch(
187             SIGNATURE,
188             -10, // newLength (illegal)
189             10, // diffSegmentLength
190             0, // copySegmentLength
191             0, // offsetToNextInput
192             new byte[10] // addends
193             );
194     ByteArrayOutputStream newData = new ByteArrayOutputStream();
195     try {
196       BsPatch.applyPatch(new RandomAccessFile(oldFile, "r"), newData, patchIn);
197       Assert.fail("Read patch with negative newLength");
198     } catch (PatchFormatException expected) {
199       // No way to mock the internal logic, so resort to testing exception string for coverage
200       String actual = expected.getMessage();
201       Assert.assertEquals("bad newSize", actual);
202     }
203   }
204 
205   @Test
testApplyPatch_NewLengthTooLarge()206   public void testApplyPatch_NewLengthTooLarge() throws Exception {
207     createEmptyOldFile(10);
208     InputStream patchIn =
209         makePatch(
210             SIGNATURE,
211             Integer.MAX_VALUE + 1, // newLength (max supported is Integer.MAX_VALUE)
212             10, // diffSegmentLength
213             0, // copySegmentLength
214             0, // offsetToNextInput
215             new byte[10] // addends
216             );
217     ByteArrayOutputStream newData = new ByteArrayOutputStream();
218     try {
219       BsPatch.applyPatch(
220           new RandomAccessFile(oldFile, "r"), newData, patchIn);
221       Assert.fail("Read patch with excessive newLength");
222     } catch (PatchFormatException expected) {
223       // No way to mock the internal logic, so resort to testing exception string for coverage
224       String actual = expected.getMessage();
225       Assert.assertEquals("bad newSize", actual);
226     }
227   }
228 
229   @Test
testApplyPatch_DiffSegmentLengthNegative()230   public void testApplyPatch_DiffSegmentLengthNegative() throws Exception {
231     createEmptyOldFile(10);
232     InputStream patchIn =
233         makePatch(
234             SIGNATURE,
235             10, // newLength
236             -10, // diffSegmentLength (negative)
237             0, // copySegmentLength
238             0, // offsetToNextInput
239             new byte[10] // addends
240             );
241     ByteArrayOutputStream newData = new ByteArrayOutputStream();
242     try {
243       BsPatch.applyPatch(new RandomAccessFile(oldFile, "r"), newData, patchIn);
244       Assert.fail("Read patch with negative diffSegmentLength");
245     } catch (PatchFormatException expected) {
246       // No way to mock the internal logic, so resort to testing exception string for coverage
247       String actual = expected.getMessage();
248       Assert.assertEquals("bad diffSegmentLength", actual);
249     }
250   }
251 
252   @Test
testApplyPatch_DiffSegmentLengthTooLarge()253   public void testApplyPatch_DiffSegmentLengthTooLarge() throws Exception {
254     createEmptyOldFile(10);
255     InputStream patchIn =
256         makePatch(
257             SIGNATURE,
258             10, // newLength
259             Integer.MAX_VALUE + 1, // diffSegmentLength (too big)
260             0, // copySegmentLength
261             0, // offsetToNextInput
262             new byte[10] // addends
263             );
264     ByteArrayOutputStream newData = new ByteArrayOutputStream();
265     try {
266       BsPatch.applyPatch(new RandomAccessFile(oldFile, "r"), newData, patchIn);
267       Assert.fail("Read patch with excessive diffSegmentLength");
268     } catch (PatchFormatException expected) {
269       // No way to mock the internal logic, so resort to testing exception string for coverage
270       String actual = expected.getMessage();
271       Assert.assertEquals("bad diffSegmentLength", actual);
272     }
273   }
274 
275   @Test
testApplyPatch_CopySegmentLengthNegative()276   public void testApplyPatch_CopySegmentLengthNegative() throws Exception {
277     createEmptyOldFile(10);
278     InputStream patchIn =
279         makePatch(
280             SIGNATURE,
281             10, // newLength
282             10, // diffSegmentLength
283             -10, // copySegmentLength (negative)
284             0, // offsetToNextInput
285             new byte[10] // addends
286             );
287     ByteArrayOutputStream newData = new ByteArrayOutputStream();
288     try {
289       BsPatch.applyPatch(new RandomAccessFile(oldFile, "r"), newData, patchIn);
290       Assert.fail("Read patch with negative copySegmentLength");
291     } catch (PatchFormatException expected) {
292       // No way to mock the internal logic, so resort to testing exception string for coverage
293       String actual = expected.getMessage();
294       Assert.assertEquals("bad copySegmentLength", actual);
295     }
296   }
297 
298   @Test
testApplyPatch_CopySegmentLengthTooLarge()299   public void testApplyPatch_CopySegmentLengthTooLarge() throws Exception {
300     createEmptyOldFile(10);
301     InputStream patchIn =
302         makePatch(
303             SIGNATURE,
304             10, // newLength
305             0, // diffSegmentLength
306             Integer.MAX_VALUE + 1, // copySegmentLength (too big)
307             0, // offsetToNextInput
308             new byte[10] // addends
309             );
310     ByteArrayOutputStream newData = new ByteArrayOutputStream();
311     try {
312       BsPatch.applyPatch(new RandomAccessFile(oldFile, "r"), newData, patchIn);
313       Assert.fail("Read patch with excessive copySegmentLength");
314     } catch (PatchFormatException expected) {
315       // No way to mock the internal logic, so resort to testing exception string for coverage
316       String actual = expected.getMessage();
317       Assert.assertEquals("bad copySegmentLength", actual);
318     }
319   }
320 
321   // ExpectedFinalNewDataBytesWritten_Negative case is impossible in code, so no need to test
322   // that; just the TooLarge condition.
323   @Test
testApplyPatch_ExpectedFinalNewDataBytesWritten_PastEOF()324   public void testApplyPatch_ExpectedFinalNewDataBytesWritten_PastEOF() throws Exception {
325     createEmptyOldFile(10);
326     // Make diffSegmentLength + copySegmentLength > newLength
327     InputStream patchIn =
328         makePatch(
329             SIGNATURE,
330             10, // newLength
331             10, // diffSegmentLength
332             1, // copySegmentLength
333             0, // offsetToNextInput
334             new byte[10] // addends
335             );
336     ByteArrayOutputStream newData = new ByteArrayOutputStream();
337     try {
338       BsPatch.applyPatch(new RandomAccessFile(oldFile, "r"), newData, patchIn);
339       Assert.fail("Read patch that moves past EOF in new file");
340     } catch (PatchFormatException expected) {
341       // No way to mock the internal logic, so resort to testing exception string for coverage
342       String actual = expected.getMessage();
343       Assert.assertEquals("expectedFinalNewDataBytesWritten too large", actual);
344     }
345   }
346 
347   @Test
testApplyPatch_ExpectedFinalOldDataOffset_Negative()348   public void testApplyPatch_ExpectedFinalOldDataOffset_Negative() throws Exception {
349     createEmptyOldFile(10);
350     // Make diffSegmentLength + offsetToNextInput < 0
351     InputStream patchIn =
352         makePatch(
353             SIGNATURE,
354             10, // newLength
355             10, // diffSegmentLength
356             0, // copySegmentLength
357             -11, // offsetToNextInput
358             new byte[10] // addends
359             );
360     ByteArrayOutputStream newData = new ByteArrayOutputStream();
361     try {
362       BsPatch.applyPatch(new RandomAccessFile(oldFile, "r"), newData, patchIn);
363       Assert.fail("Read patch with that moves to a negative offset in old file");
364     } catch (PatchFormatException expected) {
365       // No way to mock the internal logic, so resort to testing exception string for coverage
366       String actual = expected.getMessage();
367       Assert.assertEquals("expectedFinalOldDataOffset is negative", actual);
368     }
369   }
370 
371   @Test
testApplyPatch_ExpectedFinalOldDataOffset_PastEOF()372   public void testApplyPatch_ExpectedFinalOldDataOffset_PastEOF() throws Exception {
373     createEmptyOldFile(10);
374     // Make diffSegmentLength + offsetToNextInput > oldLength
375     InputStream patchIn =
376         makePatch(
377             SIGNATURE,
378             10, // newLength
379             10, // diffSegmentLength
380             0, // copySegmentLength
381             1, // offsetToNextInput
382             new byte[10] // addends
383             );
384     ByteArrayOutputStream newData = new ByteArrayOutputStream();
385     try {
386       BsPatch.applyPatch(new RandomAccessFile(oldFile, "r"), newData, patchIn);
387       Assert.fail("Read patch with that moves past EOF in old file");
388     } catch (PatchFormatException expected) {
389       // No way to mock the internal logic, so resort to testing exception string for coverage
390       String actual = expected.getMessage();
391       Assert.assertEquals("expectedFinalOldDataOffset too large", actual);
392     }
393   }
394 
395   @Test
testApplyPatch_TruncatedSignature()396   public void testApplyPatch_TruncatedSignature() throws Exception {
397     createEmptyOldFile(10);
398     InputStream patchIn = new ByteArrayInputStream("X".getBytes("US-ASCII"));
399     ByteArrayOutputStream newData = new ByteArrayOutputStream();
400     try {
401       BsPatch.applyPatch(new RandomAccessFile(oldFile, "r"), newData, patchIn);
402       Assert.fail("Read patch with truncated signature");
403     } catch (PatchFormatException expected) {
404       // No way to mock the internal logic, so resort to testing exception string for coverage
405       String actual = expected.getMessage();
406       Assert.assertEquals("truncated signature", actual);
407     }
408   }
409 
410   @Test
testReadBsdiffLong()411   public void testReadBsdiffLong() throws Exception {
412     byte[] data = {
413       (byte) 0x78,
414       (byte) 0x56,
415       (byte) 0x34,
416       (byte) 0x12,
417       (byte) 0,
418       (byte) 0,
419       (byte) 0,
420       (byte) 0,
421       (byte) 0xef,
422       (byte) 0xbe,
423       (byte) 0xad,
424       (byte) 0x0e,
425       (byte) 0,
426       (byte) 0,
427       (byte) 0,
428       (byte) 0
429     };
430     ByteArrayInputStream inputStream = new ByteArrayInputStream(data);
431     long actual = BsPatch.readBsdiffLong(inputStream);
432     Assert.assertEquals(0x12345678, actual);
433     actual = BsPatch.readBsdiffLong(inputStream);
434     Assert.assertEquals(0x0eadbeef, actual);
435   }
436 
437   @Test
testReadBsdiffLong_Zero()438   public void testReadBsdiffLong_Zero() throws Exception {
439     long expected = 0x00000000L;
440     long actual =
441         BsPatch.readBsdiffLong(
442             new ByteArrayInputStream(
443                 new byte[] {
444                   (byte) 0x00,
445                   (byte) 0x00,
446                   (byte) 0x00,
447                   (byte) 0x00,
448                   (byte) 0x00,
449                   (byte) 0x00,
450                   (byte) 0x00,
451                   (byte) 0x00
452                 }));
453     Assert.assertEquals(expected, actual);
454   }
455 
456   @Test
testReadBsdiffLong_IntegerMaxValue()457   public void testReadBsdiffLong_IntegerMaxValue() throws Exception {
458     long expected = 0x7fffffffL;
459     long actual =
460         BsPatch.readBsdiffLong(
461             new ByteArrayInputStream(
462                 new byte[] {
463                   (byte) 0xff,
464                   (byte) 0xff,
465                   (byte) 0xff,
466                   (byte) 0x7f,
467                   (byte) 0x00,
468                   (byte) 0x00,
469                   (byte) 0x00,
470                   (byte) 0x00
471                 }));
472     Assert.assertEquals(expected, actual);
473   }
474 
475   @Test
testReadBsdiffLong_IntegerMinValue()476   public void testReadBsdiffLong_IntegerMinValue() throws Exception {
477     long expected = -0x80000000L;
478     long actual =
479         BsPatch.readBsdiffLong(
480             new ByteArrayInputStream(
481                 new byte[] {
482                   (byte) 0x00,
483                   (byte) 0x00,
484                   (byte) 0x00,
485                   (byte) 0x80,
486                   (byte) 0x00,
487                   (byte) 0x00,
488                   (byte) 0x00,
489                   (byte) 0x80
490                 }));
491     Assert.assertEquals(expected, actual);
492   }
493 
494   @Test
testReadBsdiffLong_LongMaxValue()495   public void testReadBsdiffLong_LongMaxValue() throws Exception {
496     long expected = 0x7fffffffffffffffL;
497     long actual =
498         BsPatch.readBsdiffLong(
499             new ByteArrayInputStream(
500                 new byte[] {
501                   (byte) 0xff,
502                   (byte) 0xff,
503                   (byte) 0xff,
504                   (byte) 0xff,
505                   (byte) 0xff,
506                   (byte) 0xff,
507                   (byte) 0xff,
508                   (byte) 0x7f
509                 }));
510     Assert.assertEquals(expected, actual);
511   }
512 
513   // Can't read Long.MIN_VALUE because the signed-magnitude representation stops at
514   // Long.MIN_VALUE+1.
515   @Test
testReadBsdiffLong_LongMinValueIsh()516   public void testReadBsdiffLong_LongMinValueIsh() throws Exception {
517     long expected = -0x7fffffffffffffffL;
518     long actual =
519         BsPatch.readBsdiffLong(
520             new ByteArrayInputStream(
521                 new byte[] {
522                   (byte) 0xff,
523                   (byte) 0xff,
524                   (byte) 0xff,
525                   (byte) 0xff,
526                   (byte) 0xff,
527                   (byte) 0xff,
528                   (byte) 0xff,
529                   (byte) 0xff
530                 }));
531     Assert.assertEquals(expected, actual);
532   }
533 
534   // This is also Java's Long.MAX_VALUE.
535   @Test
testReadBsdiffLong_NegativeZero()536   public void testReadBsdiffLong_NegativeZero() throws Exception {
537     try {
538       BsPatch.readBsdiffLong(
539           new ByteArrayInputStream(
540               new byte[] {
541                 (byte) 0x00,
542                 (byte) 0x00,
543                 (byte) 0x00,
544                 (byte) 0x00,
545                 (byte) 0x00,
546                 (byte) 0x00,
547                 (byte) 0x00,
548                 (byte) 0x80
549               }));
550       Assert.fail("Tolerated negative zero");
551     } catch (PatchFormatException expected) {
552       // Pass
553     }
554   }
555 
556   @Test
testReadFully()557   public void testReadFully() throws IOException {
558     final byte[] input = "this is a sample string to read".getBytes();
559     final ByteArrayInputStream inputStream = new ByteArrayInputStream(input);
560     final byte[] dst = new byte[50];
561 
562     try {
563       BsPatch.readFully(inputStream, dst, 0, 50);
564       Assert.fail("Should've thrown an IOException");
565     } catch (IOException expected) {
566       // Pass
567     }
568 
569     inputStream.reset();
570     BsPatch.readFully(inputStream, dst, 0, input.length);
571     Assert.assertTrue(regionEquals(dst, 0, input, 0, input.length));
572 
573     inputStream.reset();
574     BsPatch.readFully(inputStream, dst, 40, 10);
575     Assert.assertTrue(regionEquals(dst, 40, input, 0, 10));
576 
577     inputStream.reset();
578     try {
579       BsPatch.readFully(inputStream, dst, 45, 11);
580       Assert.fail("Should've thrown an IndexOutOfBoundsException");
581     } catch (IndexOutOfBoundsException expected) {
582       // Pass
583     }
584   }
585 
586   @Test
testPipe()587   public void testPipe() throws IOException {
588     final String inputString = "this is a sample string to read";
589     final byte[] input = inputString.getBytes("US-ASCII");
590     final ByteArrayInputStream inputStream = new ByteArrayInputStream(input);
591     final byte[] buffer = new byte[5];
592     ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
593 
594     BsPatch.pipe(inputStream, outputStream, buffer, 0);
595     int actualLength = outputStream.toByteArray().length;
596     Assert.assertEquals(0, actualLength);
597 
598     inputStream.reset();
599     BsPatch.pipe(inputStream, outputStream, buffer, 1);
600     actualLength = outputStream.toByteArray().length;
601     Assert.assertEquals(1, actualLength);
602     byte actualByte = outputStream.toByteArray()[0];
603     Assert.assertEquals((byte) 't', actualByte);
604 
605     outputStream = new ByteArrayOutputStream();
606     inputStream.reset();
607     BsPatch.pipe(inputStream, outputStream, buffer, 5);
608     actualLength = outputStream.toByteArray().length;
609     Assert.assertEquals(5, actualLength);
610     String actualOutput = outputStream.toString();
611     String expectedOutput = inputString.substring(0, 5);
612     Assert.assertEquals(expectedOutput, actualOutput);
613 
614     outputStream = new ByteArrayOutputStream();
615     inputStream.reset();
616     BsPatch.pipe(inputStream, outputStream, buffer, input.length);
617     actualLength = outputStream.toByteArray().length;
618     Assert.assertEquals(input.length, actualLength);
619     expectedOutput = outputStream.toString();
620     Assert.assertEquals(inputString, expectedOutput);
621   }
622 
623   @Test
testPipe_Underrun()624   public void testPipe_Underrun() {
625     int dataLength = 10;
626     ByteArrayInputStream in = new ByteArrayInputStream(new byte[dataLength]);
627     try {
628       // Tell pipe to copy 1 more byte than is actually available
629       BsPatch.pipe(in, new ByteArrayOutputStream(), new byte[dataLength], dataLength + 1);
630       Assert.fail("Should've thrown an IOException");
631     } catch (IOException expected) {
632       // Pass
633     }
634   }
635 
636   @Test
testPipe_CopyZeroBytes()637   public void testPipe_CopyZeroBytes() throws IOException {
638     int dataLength = 0;
639     ByteArrayInputStream in = new ByteArrayInputStream(new byte[dataLength]);
640     ByteArrayOutputStream out = new ByteArrayOutputStream();
641     BsPatch.pipe(in, out, new byte[100], dataLength);
642     int actualLength = out.toByteArray().length;
643     Assert.assertEquals(0, actualLength);
644   }
645 
646   /**
647    * Invoke applyPatch(...) and verify that the results are as expected.
648    * @param oldPath the path to the old asset in /assets
649    * @param patchPatch the path to the patch asset in /assets
650    * @param newPath the path to the new asset in /assets
651    * @throws IOException if unable to read/write
652    * @throws PatchFormatException if the patch is invalid
653    */
invokeApplyPatch(String oldPath, String patchPatch, String newPath)654   private void invokeApplyPatch(String oldPath, String patchPatch, String newPath)
655       throws IOException, PatchFormatException {
656     copyToOldFile(oldPath);
657     RandomAccessFile oldData = new RandomAccessFile(oldFile, "r");
658     InputStream patchInputStream = new ByteArrayInputStream(readTestData(patchPatch));
659     byte[] expectedNewDataBytes = readTestData(newPath);
660     ByteArrayOutputStream actualNewData = new ByteArrayOutputStream();
661     BsPatch.applyPatch(oldData, actualNewData, patchInputStream);
662     byte[] actualNewDataBytes = actualNewData.toByteArray();
663     Assert.assertArrayEquals(expectedNewDataBytes, actualNewDataBytes);
664   }
665 
666   /**
667    * Checks two byte ranges for equivalence.
668    *
669    * @param data1  first array
670    * @param data2  second array
671    * @param start1 first byte to compare in |data1|
672    * @param start2 first byte to compare in |data2|
673    * @param length the number of bytes to compare
674    */
regionEquals( final byte[] data1, final int start1, final byte[] data2, final int start2, final int length)675   private static boolean regionEquals(
676       final byte[] data1,
677       final int start1,
678       final byte[] data2,
679       final int start2,
680       final int length) {
681     for (int x = 0; x < length; x++) {
682       if (data1[x + start1] != data2[x + start2]) {
683         return false;
684       }
685     }
686     return true;
687   }
688 
689   // (Copied from BsDiffTest)
690   // Some systems force all text files to end in a newline, which screws up this test.
stripNewlineIfNecessary(byte[] b)691   private static byte[] stripNewlineIfNecessary(byte[] b) {
692     if (b[b.length - 1] != (byte) '\n') {
693       return b;
694     }
695 
696     byte[] ret = new byte[b.length - 1];
697     System.arraycopy(b, 0, ret, 0, ret.length);
698     return ret;
699   }
700 
701   // (Copied from BsDiffTest)
readTestData(String testDataFileName)702   private byte[] readTestData(String testDataFileName) throws IOException {
703     InputStream in = getClass().getResourceAsStream("testdata/" + testDataFileName);
704     Assert.assertNotNull("test data file doesn't exist: " + testDataFileName, in);
705     ByteArrayOutputStream result = new ByteArrayOutputStream();
706     byte[] buffer = new byte[32768];
707     int numRead = 0;
708     while ((numRead = in.read(buffer)) >= 0) {
709       result.write(buffer, 0, numRead);
710     }
711     return stripNewlineIfNecessary(result.toByteArray());
712   }
713 
714   /**
715    * Copy the contents of the specified testdata asset into {@link #oldFile}.
716    * @param testDataFileName the name of the testdata asset to read
717    * @throws IOException if unable to complete the copy
718    */
copyToOldFile(String testDataFileName)719   private void copyToOldFile(String testDataFileName) throws IOException {
720     oldFile = File.createTempFile("archive_patcher", "temp");
721     Assert.assertNotNull("cant create file!", oldFile);
722     byte[] buffer = readTestData(testDataFileName);
723     FileOutputStream out = new FileOutputStream(oldFile);
724     out.write(buffer);
725     out.flush();
726     out.close();
727   }
728 
729   /**
730    * Make {@link #oldFile} an empty file (full of binary zeroes) of the specified length.
731    * @param desiredLength the desired length in bytes
732    * @throws IOException if unable to write the file
733    */
createEmptyOldFile(int desiredLength)734   private void createEmptyOldFile(int desiredLength) throws IOException {
735     OutputStream out = new FileOutputStream(oldFile);
736     for (int x = 0; x < desiredLength; x++) {
737       out.write(0);
738     }
739     out.close();
740   }
741 
742   /**
743    * Create an arbitrary patch that consists of a signature, a length, and a directive sequence.
744    * Used to manufacture junk for failure and edge cases.
745    * @param signature the signature to use
746    * @param newLength the expected length of the "new" file produced by applying the patch
747    * @param diffSegmentLength the value to supply as diffSegmentLength
748    * @param copySegmentLength the value to supply as copySegmentLength
749    * @param offsetToNextInput the value to supply as offsetToNextInput
750    * @param addends a byte array of addends; all are written, ignoring |diffSegmentLength|.
751    * @return the bytes constituting the patch
752    * @throws IOException
753    */
makePatch( String signature, long newLength, long diffSegmentLength, long copySegmentLength, long offsetToNextInput, byte[] addends)754   private static InputStream makePatch(
755       String signature,
756       long newLength,
757       long diffSegmentLength,
758       long copySegmentLength,
759       long offsetToNextInput,
760       byte[] addends)
761       throws IOException {
762     ByteArrayOutputStream out = new ByteArrayOutputStream();
763     out.write(signature.getBytes("US-ASCII"));
764     writeBsdiffLong(newLength, out);
765     writeBsdiffLong(diffSegmentLength, out);
766     writeBsdiffLong(copySegmentLength, out);
767     writeBsdiffLong(offsetToNextInput, out);
768     out.write(addends);
769     return new ByteArrayInputStream(out.toByteArray());
770   }
771 
772   // Copied from com.google.archivepatcher.generator.bsdiff.BsUtil for convenience.
writeBsdiffLong(final long value, OutputStream out)773   private static void writeBsdiffLong(final long value, OutputStream out) throws IOException {
774     long y = value;
775     if (y < 0) {
776       y = (-y) | (1L << 63);
777     }
778     for (int i = 0; i < 8; ++i) {
779       out.write((byte) (y & 0xff));
780       y >>>= 8;
781     }
782   }
783 }
784