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.shared;
16 
17 import java.io.File;
18 import java.io.IOException;
19 import java.io.OutputStream;
20 import java.util.ArrayList;
21 import java.util.List;
22 
23 /**
24  * Utilities for generating delta-friendly files.
25  */
26 public class DeltaFriendlyFile {
27 
28   /**
29    * The default size of the copy buffer to use for copying between streams.
30    */
31   public static final int DEFAULT_COPY_BUFFER_SIZE = 32768;
32 
33   /**
34    * Invoke {@link #generateDeltaFriendlyFile(List, File, OutputStream, boolean, int)} with <code>
35    * generateInverse</code> set to <code>true</code> and a copy buffer size of {@link
36    * #DEFAULT_COPY_BUFFER_SIZE}.
37    *
38    * @param <T> the type of the data associated with the ranges
39    * @param rangesToUncompress the ranges to be uncompressed during transformation to a
40    *     delta-friendly form
41    * @param file the file to read from
42    * @param deltaFriendlyOut a stream to write the delta-friendly file to
43    * @return the ranges in the delta-friendly file that correspond to the ranges in the original
44    *     file, with identical metadata and in the same order
45    * @throws IOException if anything goes wrong
46    */
generateDeltaFriendlyFile( List<TypedRange<T>> rangesToUncompress, File file, OutputStream deltaFriendlyOut)47   public static <T> List<TypedRange<T>> generateDeltaFriendlyFile(
48       List<TypedRange<T>> rangesToUncompress, File file, OutputStream deltaFriendlyOut)
49       throws IOException {
50     return generateDeltaFriendlyFile(
51         rangesToUncompress, file, deltaFriendlyOut, true, DEFAULT_COPY_BUFFER_SIZE);
52   }
53 
54   /**
55    * Generate one delta-friendly file and (optionally) return the ranges necessary to invert the
56    * transform, in file order. There is a 1:1 correspondence between the ranges in the input list
57    * and the returned list, but the offsets and lengths will be different (the input list represents
58    * compressed data, the output list represents uncompressed data). The ability to suppress
59    * generation of the inverse range and to specify the size of the copy buffer are provided for
60    * clients that desire a minimal memory footprint.
61    *
62    * @param <T> the type of the data associated with the ranges
63    * @param rangesToUncompress the ranges to be uncompressed during transformation to a
64    *     delta-friendly form
65    * @param file the file to read from
66    * @param deltaFriendlyOut a stream to write the delta-friendly file to
67    * @param generateInverse if <code>true</code>, generate and return a list of inverse ranges in
68    *     file order; otherwise, do all the normal work but return null instead of the inverse ranges
69    * @param copyBufferSize the size of the buffer to use for copying bytes between streams
70    * @return if <code>generateInverse</code> was true, returns the ranges in the delta-friendly file
71    *     that correspond to the ranges in the original file, with identical metadata and in the same
72    *     order; otherwise, return null
73    * @throws IOException if anything goes wrong
74    */
generateDeltaFriendlyFile( List<TypedRange<T>> rangesToUncompress, File file, OutputStream deltaFriendlyOut, boolean generateInverse, int copyBufferSize)75   public static <T> List<TypedRange<T>> generateDeltaFriendlyFile(
76       List<TypedRange<T>> rangesToUncompress,
77       File file,
78       OutputStream deltaFriendlyOut,
79       boolean generateInverse,
80       int copyBufferSize)
81       throws IOException {
82     List<TypedRange<T>> inverseRanges = null;
83     if (generateInverse) {
84       inverseRanges = new ArrayList<TypedRange<T>>(rangesToUncompress.size());
85     }
86     long lastReadOffset = 0;
87     RandomAccessFileInputStream oldFileRafis = null;
88     PartiallyUncompressingPipe filteredOut =
89         new PartiallyUncompressingPipe(deltaFriendlyOut, copyBufferSize);
90     try {
91       oldFileRafis = new RandomAccessFileInputStream(file);
92       for (TypedRange<T> rangeToUncompress : rangesToUncompress) {
93         long gap = rangeToUncompress.getOffset() - lastReadOffset;
94         if (gap > 0) {
95           // Copy bytes up to the range start point
96           oldFileRafis.setRange(lastReadOffset, gap);
97           filteredOut.pipe(oldFileRafis, PartiallyUncompressingPipe.Mode.COPY);
98         }
99 
100         // Now uncompress the range.
101         oldFileRafis.setRange(rangeToUncompress.getOffset(), rangeToUncompress.getLength());
102         long inverseRangeStart = filteredOut.getNumBytesWritten();
103         // TODO(andrewhayden): Support nowrap=false here? Never encountered in practice.
104         // This would involve catching the ZipException, checking if numBytesWritten is still zero,
105         // resetting the stream and trying again.
106         filteredOut.pipe(oldFileRafis, PartiallyUncompressingPipe.Mode.UNCOMPRESS_NOWRAP);
107         lastReadOffset = rangeToUncompress.getOffset() + rangeToUncompress.getLength();
108 
109         if (generateInverse) {
110           long inverseRangeEnd = filteredOut.getNumBytesWritten();
111           long inverseRangeLength = inverseRangeEnd - inverseRangeStart;
112           TypedRange<T> inverseRange =
113               new TypedRange<T>(
114                   inverseRangeStart, inverseRangeLength, rangeToUncompress.getMetadata());
115           inverseRanges.add(inverseRange);
116         }
117       }
118       // Finish the final bytes of the file
119       long bytesLeft = oldFileRafis.length() - lastReadOffset;
120       if (bytesLeft > 0) {
121         oldFileRafis.setRange(lastReadOffset, bytesLeft);
122         filteredOut.pipe(oldFileRafis, PartiallyUncompressingPipe.Mode.COPY);
123       }
124     } finally {
125       try {
126         oldFileRafis.close();
127       } catch (Exception ignored) {
128         // Nothing
129       }
130       try {
131         filteredOut.close();
132       } catch (Exception ignored) {
133         // Nothing
134       }
135     }
136     return inverseRanges;
137   }
138 }
139