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