aboutsummaryrefslogtreecommitdiff
path: root/applier/src/test/java/com/google/archivepatcher/applier/FileByFileV1DeltaApplierTest.java
blob: b2ebf93bfce8a2837643afdef5d515624e8a4333 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
// 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());
  }
}