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