// 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.shared; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.List; /** * Utilities for generating delta-friendly files. */ public class DeltaFriendlyFile { /** * The default size of the copy buffer to use for copying between streams. */ public static final int DEFAULT_COPY_BUFFER_SIZE = 32768; /** * Invoke {@link #generateDeltaFriendlyFile(List, File, OutputStream, boolean, int)} with * generateInverse set to true and a copy buffer size of * {@link #DEFAULT_COPY_BUFFER_SIZE}. * @param rangesToUncompress the ranges to be uncompressed during transformation to a * delta-friendly form * @param file the file to read from * @param deltaFriendlyOut a stream to write the delta-friendly file to * @return the ranges in the delta-friendly file that correspond to the ranges in the original * file, with identical metadata and in the same order * @throws IOException if anything goes wrong */ public static List> generateDeltaFriendlyFile( List> rangesToUncompress, File file, OutputStream deltaFriendlyOut) throws IOException { return generateDeltaFriendlyFile( rangesToUncompress, file, deltaFriendlyOut, true, DEFAULT_COPY_BUFFER_SIZE); } /** * Generate one delta-friendly file and (optionally) return the ranges necessary to invert the * transform, in file order. There is a 1:1 correspondence between the ranges in the input * list and the returned list, but the offsets and lengths will be different (the input list * represents compressed data, the output list represents uncompressed data). The ability to * suppress generation of the inverse range and to specify the size of the copy buffer are * provided for clients that desire a minimal memory footprint. * @param rangesToUncompress the ranges to be uncompressed during transformation to a * delta-friendly form * @param file the file to read from * @param deltaFriendlyOut a stream to write the delta-friendly file to * @param generateInverse if true, generate and return a list of inverse ranges in * file order; otherwise, do all the normal work but return null instead of the inverse ranges * @param copyBufferSize the size of the buffer to use for copying bytes between streams * @return if generateInverse was true, returns the ranges in the delta-friendly file * that correspond to the ranges in the original file, with identical metadata and in the same * order; otherwise, return null * @throws IOException if anything goes wrong */ public static List> generateDeltaFriendlyFile( List> rangesToUncompress, File file, OutputStream deltaFriendlyOut, boolean generateInverse, int copyBufferSize) throws IOException { List> inverseRanges = null; if (generateInverse) { inverseRanges = new ArrayList>(rangesToUncompress.size()); } long lastReadOffset = 0; RandomAccessFileInputStream oldFileRafis = null; PartiallyUncompressingPipe filteredOut = new PartiallyUncompressingPipe(deltaFriendlyOut, copyBufferSize); try { oldFileRafis = new RandomAccessFileInputStream(file); for (TypedRange rangeToUncompress : rangesToUncompress) { long gap = rangeToUncompress.getOffset() - lastReadOffset; if (gap > 0) { // Copy bytes up to the range start point oldFileRafis.setRange(lastReadOffset, gap); filteredOut.pipe(oldFileRafis, PartiallyUncompressingPipe.Mode.COPY); } // Now uncompress the range. oldFileRafis.setRange(rangeToUncompress.getOffset(), rangeToUncompress.getLength()); long inverseRangeStart = filteredOut.getNumBytesWritten(); // TODO(andrewhayden): Support nowrap=false here? Never encountered in practice. // This would involve catching the ZipException, checking if numBytesWritten is still zero, // resetting the stream and trying again. filteredOut.pipe(oldFileRafis, PartiallyUncompressingPipe.Mode.UNCOMPRESS_NOWRAP); lastReadOffset = rangeToUncompress.getOffset() + rangeToUncompress.getLength(); if (generateInverse) { long inverseRangeEnd = filteredOut.getNumBytesWritten(); long inverseRangeLength = inverseRangeEnd - inverseRangeStart; TypedRange inverseRange = new TypedRange( inverseRangeStart, inverseRangeLength, rangeToUncompress.getMetadata()); inverseRanges.add(inverseRange); } } // Finish the final bytes of the file long bytesLeft = oldFileRafis.length() - lastReadOffset; if (bytesLeft > 0) { oldFileRafis.setRange(lastReadOffset, bytesLeft); filteredOut.pipe(oldFileRafis, PartiallyUncompressingPipe.Mode.COPY); } } finally { try { oldFileRafis.close(); } catch (Exception ignored) { // Nothing } try { filteredOut.close(); } catch (Exception ignored) { // Nothing } } return inverseRanges; } }