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.generator;
16 
17 import com.google.archivepatcher.generator.DefaultDeflateCompressionDiviner.DivinationResult;
18 import com.google.archivepatcher.shared.DeltaFriendlyFile;
19 import com.google.archivepatcher.shared.JreDeflateParameters;
20 import com.google.archivepatcher.shared.TypedRange;
21 import java.io.BufferedOutputStream;
22 import java.io.File;
23 import java.io.FileOutputStream;
24 import java.io.IOException;
25 import java.util.ArrayList;
26 import java.util.Collections;
27 import java.util.HashMap;
28 import java.util.List;
29 import java.util.Map;
30 
31 /**
32  * Prepares resources for differencing.
33  */
34 public class PreDiffExecutor {
35 
36   /** A helper class to build a {@link PreDiffExecutor} with a variety of configurations. */
37   public static final class Builder {
38     private File originalOldFile;
39     private File originalNewFile;
40     private File deltaFriendlyOldFile;
41     private File deltaFriendlyNewFile;
42     private List<RecommendationModifier> recommendationModifiers =
43         new ArrayList<RecommendationModifier>();
44 
45     /**
46      * Sets the original, read-only input files to the patch generation process. This has to be
47      * called at least once, and both arguments must be non-null.
48      *
49      * @param originalOldFile the original old file to read (will not be modified).
50      * @param originalNewFile the original new file to read (will not be modified).
51      * @return this builder
52      */
readingOriginalFiles(File originalOldFile, File originalNewFile)53     public Builder readingOriginalFiles(File originalOldFile, File originalNewFile) {
54       if (originalOldFile == null || originalNewFile == null) {
55         throw new IllegalStateException("do not set nul original input files");
56       }
57       this.originalOldFile = originalOldFile;
58       this.originalNewFile = originalNewFile;
59       return this;
60     }
61 
62     /**
63      * Sets the output files that will hold the delta-friendly intermediate binaries used in patch
64      * generation. If called, both arguments must be non-null.
65      *
66      * @param deltaFriendlyOldFile the intermediate file to write (will be overwritten if it exists)
67      * @param deltaFriendlyNewFile the intermediate file to write (will be overwritten if it exists)
68      * @return this builder
69      */
writingDeltaFriendlyFiles(File deltaFriendlyOldFile, File deltaFriendlyNewFile)70     public Builder writingDeltaFriendlyFiles(File deltaFriendlyOldFile, File deltaFriendlyNewFile) {
71       if (deltaFriendlyOldFile == null || deltaFriendlyNewFile == null) {
72         throw new IllegalStateException("do not set null delta-friendly files");
73       }
74       this.deltaFriendlyOldFile = deltaFriendlyOldFile;
75       this.deltaFriendlyNewFile = deltaFriendlyNewFile;
76       return this;
77     }
78 
79     /**
80      * Appends an optional {@link RecommendationModifier} to be used during the generation of the
81      * {@link PreDiffPlan} and/or delta-friendly blobs.
82      *
83      * @param recommendationModifier the modifier to set
84      * @return this builder
85      */
withRecommendationModifier(RecommendationModifier recommendationModifier)86     public Builder withRecommendationModifier(RecommendationModifier recommendationModifier) {
87       if (recommendationModifier == null) {
88         throw new IllegalArgumentException("recommendationModifier cannot be null");
89       }
90       this.recommendationModifiers.add(recommendationModifier);
91       return this;
92     }
93 
94     /**
95      * Builds and returns a {@link PreDiffExecutor} according to the currnet configuration.
96      *
97      * @return the executor
98      */
build()99     public PreDiffExecutor build() {
100       if (originalOldFile == null) {
101         // readingOriginalFiles() ensures old and new are non-null when called, so check either.
102         throw new IllegalStateException("original input files cannot be null");
103       }
104       return new PreDiffExecutor(
105           originalOldFile,
106           originalNewFile,
107           deltaFriendlyOldFile,
108           deltaFriendlyNewFile,
109           recommendationModifiers);
110     }
111   }
112 
113   /** The original old file to read (will not be modified). */
114   private final File originalOldFile;
115 
116   /** The original new file to read (will not be modified). */
117   private final File originalNewFile;
118 
119   /**
120    * Optional file to write the delta-friendly version of the original old file to (will be created,
121    * overwriting if it already exists). If null, only the read-only planning step can be performed.
122    */
123   private final File deltaFriendlyOldFile;
124 
125   /**
126    * Optional file to write the delta-friendly version of the original new file to (will be created,
127    * overwriting if it already exists). If null, only the read-only planning step can be performed.
128    */
129   private final File deltaFriendlyNewFile;
130 
131   /**
132    * Optional {@link RecommendationModifier}s to be used for modifying the patch to be generated.
133    */
134   private final List<RecommendationModifier> recommendationModifiers;
135 
136   /** Constructs a new PreDiffExecutor to work with the specified configuration. */
PreDiffExecutor( File originalOldFile, File originalNewFile, File deltaFriendlyOldFile, File deltaFriendlyNewFile, List<RecommendationModifier> recommendationModifiers)137   private PreDiffExecutor(
138       File originalOldFile,
139       File originalNewFile,
140       File deltaFriendlyOldFile,
141       File deltaFriendlyNewFile,
142       List<RecommendationModifier> recommendationModifiers) {
143     this.originalOldFile = originalOldFile;
144     this.originalNewFile = originalNewFile;
145     this.deltaFriendlyOldFile = deltaFriendlyOldFile;
146     this.deltaFriendlyNewFile = deltaFriendlyNewFile;
147     this.recommendationModifiers = recommendationModifiers;
148   }
149 
150   /**
151    * Prepare resources for diffing and returns the completed plan.
152    *
153    * @return the plan
154    * @throws IOException if unable to complete the operation due to an I/O error
155    */
prepareForDiffing()156   public PreDiffPlan prepareForDiffing() throws IOException {
157     PreDiffPlan preDiffPlan = generatePreDiffPlan();
158     List<TypedRange<JreDeflateParameters>> deltaFriendlyNewFileRecompressionPlan = null;
159     if (deltaFriendlyOldFile != null) {
160       // Builder.writingDeltaFriendlyFiles() ensures old and new are non-null when called, so a
161       // check on either is sufficient.
162       deltaFriendlyNewFileRecompressionPlan =
163           Collections.unmodifiableList(generateDeltaFriendlyFiles(preDiffPlan));
164     }
165     return new PreDiffPlan(
166         preDiffPlan.getQualifiedRecommendations(),
167         preDiffPlan.getOldFileUncompressionPlan(),
168         preDiffPlan.getNewFileUncompressionPlan(),
169         deltaFriendlyNewFileRecompressionPlan);
170   }
171 
172   /**
173    * Generate the delta-friendly files and return the plan for recompressing the delta-friendly new
174    * file back into the original new file.
175    *
176    * @param preDiffPlan the plan to execute
177    * @return as described
178    * @throws IOException if anything goes wrong
179    */
generateDeltaFriendlyFiles(PreDiffPlan preDiffPlan)180   private List<TypedRange<JreDeflateParameters>> generateDeltaFriendlyFiles(PreDiffPlan preDiffPlan)
181       throws IOException {
182     try (FileOutputStream out = new FileOutputStream(deltaFriendlyOldFile);
183         BufferedOutputStream bufferedOut = new BufferedOutputStream(out)) {
184       DeltaFriendlyFile.generateDeltaFriendlyFile(
185           preDiffPlan.getOldFileUncompressionPlan(), originalOldFile, bufferedOut);
186     }
187     try (FileOutputStream out = new FileOutputStream(deltaFriendlyNewFile);
188         BufferedOutputStream bufferedOut = new BufferedOutputStream(out)) {
189       return DeltaFriendlyFile.generateDeltaFriendlyFile(
190           preDiffPlan.getNewFileUncompressionPlan(), originalNewFile, bufferedOut);
191     }
192   }
193 
194   /**
195    * Analyze the original old and new files and generate a plan to transform them into their
196    * delta-friendly equivalents.
197    *
198    * @return the plan, which does not yet contain information for recompressing the delta-friendly
199    *     new archive.
200    * @throws IOException if anything goes wrong
201    */
generatePreDiffPlan()202   private PreDiffPlan generatePreDiffPlan() throws IOException {
203     Map<ByteArrayHolder, MinimalZipEntry> originalOldArchiveZipEntriesByPath =
204         new HashMap<ByteArrayHolder, MinimalZipEntry>();
205     Map<ByteArrayHolder, MinimalZipEntry> originalNewArchiveZipEntriesByPath =
206         new HashMap<ByteArrayHolder, MinimalZipEntry>();
207     Map<ByteArrayHolder, JreDeflateParameters> originalNewArchiveJreDeflateParametersByPath =
208         new HashMap<ByteArrayHolder, JreDeflateParameters>();
209 
210     for (MinimalZipEntry zipEntry : MinimalZipArchive.listEntries(originalOldFile)) {
211       ByteArrayHolder key = new ByteArrayHolder(zipEntry.getFileNameBytes());
212       originalOldArchiveZipEntriesByPath.put(key, zipEntry);
213     }
214 
215     DefaultDeflateCompressionDiviner diviner = new DefaultDeflateCompressionDiviner();
216     for (DivinationResult divinationResult : diviner.divineDeflateParameters(originalNewFile)) {
217       ByteArrayHolder key =
218           new ByteArrayHolder(divinationResult.minimalZipEntry.getFileNameBytes());
219       originalNewArchiveZipEntriesByPath.put(key, divinationResult.minimalZipEntry);
220       originalNewArchiveJreDeflateParametersByPath.put(key, divinationResult.divinedParameters);
221     }
222 
223     PreDiffPlanner preDiffPlanner =
224         new PreDiffPlanner(
225             originalOldFile,
226             originalOldArchiveZipEntriesByPath,
227             originalNewFile,
228             originalNewArchiveZipEntriesByPath,
229             originalNewArchiveJreDeflateParametersByPath,
230             recommendationModifiers.toArray(new RecommendationModifier[] {}));
231     return preDiffPlanner.generatePreDiffPlan();
232   }
233 }
234