合成代码如下
/**
* Apply a specified patch to the specified old file, creating the specified new file.
* @param oldFile the old file (will be read)
* @param patchFile the patch file (will be read)
* @param newFile the new file (will be written)
* @throws IOException if anything goes wrong
*/
public static void applyPatch(File oldFile, File patchFile, File newFile) throws IOException {
// Figure out temp directory
File tempFile = File.createTempFile("fbftool", "tmp");
File tempDir = tempFile.getParentFile();
tempFile.delete();
FileByFileV1DeltaApplier applier = new FileByFileV1DeltaApplier(tempDir);
try (FileInputStream patchIn = new FileInputStream(patchFile);
BufferedInputStream bufferedPatchIn = new BufferedInputStream(patchIn);
FileOutputStream newOut = new FileOutputStream(newFile);
BufferedOutputStream bufferedNewOut = new BufferedOutputStream(newOut)) {
applier.applyDelta(oldFile, bufferedPatchIn, bufferedNewOut);
bufferedNewOut.flush();
}
}
@Override
public void applyDelta(File oldBlob, InputStream deltaIn, OutputStream newBlobOut)
throws IOException {
if (!tempDir.exists()) {
// Be nice, try to create the temp directory. Don't bother to check return value as the code
// will fail when it tries to create the file in a few more lines anyways.
tempDir.mkdirs();
}
File tempFile = File.createTempFile("gfbfv1", "old", tempDir);
try {
applyDeltaInternal(oldBlob, tempFile, deltaIn, newBlobOut);
} finally {
tempFile.delete();
}
}
/**
* Does the work for applying a delta.
* @param oldBlob the old blob
* @param deltaFriendlyOldBlob the location in which to store the delta-friendly old blob
* @param deltaIn the patch stream
* @param newBlobOut the stream to write the new blob to after applying the delta
* @throws IOException if anything goes wrong
*/
private void applyDeltaInternal(
File oldBlob, File deltaFriendlyOldBlob, InputStream deltaIn, OutputStream newBlobOut)
throws IOException {
// First, read the patch plan from the patch stream.
PatchReader patchReader = new PatchReader();
PatchApplyPlan plan = patchReader.readPatchApplyPlan(deltaIn);
writeDeltaFriendlyOldBlob(plan, oldBlob, deltaFriendlyOldBlob);
// Apply the delta. In v1 there is always exactly one delta descriptor, it is bsdiff, and it
// takes up the rest of the patch stream - so there is no need to examine the list of
// DeltaDescriptors in the patch at all.
long deltaLength = plan.getDeltaDescriptors().get(0).getDeltaLength();
DeltaApplier deltaApplier = getDeltaApplier();
// Don't close this stream, as it is just a limiting wrapper.
@SuppressWarnings("resource")
LimitedInputStream limitedDeltaIn = new LimitedInputStream(deltaIn, deltaLength);
// Don't close this stream, as it would close the underlying OutputStream (that we don't own).
@SuppressWarnings("resource")
PartiallyCompressingOutputStream recompressingNewBlobOut =
new PartiallyCompressingOutputStream(
plan.getDeltaFriendlyNewFileRecompressionPlan(),
newBlobOut,
DEFAULT_COPY_BUFFER_SIZE);
deltaApplier.applyDelta(deltaFriendlyOldBlob, limitedDeltaIn, recompressingNewBlobOut);
recompressingNewBlobOut.flush();
}
主要做的如下的事情
1.解析patch文件生成PatchApplyPlan对象
2.生成旧文件old.zip差量友好文件
3.应用合成算法生成new.zip的差量友好文件
4.写入zip文件流完成
先看一下patch文件的解析过程:
/**
* Reads patch data from the specified {@link InputStream} up to but not including the first byte
* of delta bytes, and returns a {@link PatchApplyPlan} that describes all the operations that
* need to be performed in order to apply the patch. When this method returns, the stream is
* positioned so that the next read will be the first byte of delta bytes corresponding to the
* first {@link DeltaDescriptor} in the returned plan.
* @param in the stream to read from
* @return the plan for applying the patch
* @throws IOException if anything goes wrong
*/
public PatchApplyPlan readPatchApplyPlan(InputStream in) throws IOException {
// Use DataOutputStream for ease of writing. This is deliberately left open, as closing it would
// close the output stream that was passed in and that is not part of the method's documented
// behavior.
@SuppressWarnings("resource")
DataInputStream dataIn = new DataInputStream(in);
// Read header and flags.
byte[] expectedIdentifier = PatchConstants.IDENTIFIER.getBytes("US-ASCII");
byte[] actualIdentifier = new byte[expectedIdentifier.length];
dataIn.readFully(actualIdentifier);
if (!Arrays.equals(expectedIdentifier, actualIdentifier)) {
throw new PatchFormatException("Bad identifier");
}
dataIn.skip(4); // Flags (ignored in v1)
long deltaFriendlyOldFileSize = checkNonNegative(
dataIn.readLong(), "delta-friendly old file size");
// Read old file uncompression instructions.
int numOldFileUncompressionInstructions = (int) checkNonNegative(
dataIn.readInt(), "old file uncompression instruction count");
List> oldFileUncompressionPlan =
new ArrayList>(numOldFileUncompressionInstructions);
long lastReadOffset = -1;
for (int x = 0; x < numOldFileUncompressionInstructions; x++) {
long offset = checkNonNegative(dataIn.readLong(), "old file uncompression range offset");
long length = checkNonNegative(dataIn.readLong(), "old file uncompression range length");
if (offset < lastReadOffset) {
throw new PatchFormatException("old file uncompression ranges out of order or overlapping");
}
TypedRange range = new TypedRange(offset, length, null);
oldFileUncompressionPlan.add(range);
lastReadOffset = offset + length; // To check that the next range starts after the current one
}
// Read new file recompression instructions
int numDeltaFriendlyNewFileRecompressionInstructions = dataIn.readInt();
checkNonNegative(
numDeltaFriendlyNewFileRecompressionInstructions,
"delta-friendly new file recompression instruction count");
List> deltaFriendlyNewFileRecompressionPlan =
new ArrayList>(
numDeltaFriendlyNewFileRecompressionInstructions);
lastReadOffset = -1;
for (int x = 0; x < numDeltaFriendlyNewFileRecompressionInstructions; x++) {
long offset = checkNonNegative(
dataIn.readLong(), "delta-friendly new file recompression range offset");
long length = checkNonNegative(
dataIn.readLong(), "delta-friendly new file recompression range length");
if (offset < lastReadOffset) {
throw new PatchFormatException(
"delta-friendly new file recompression ranges out of order or overlapping");
}
lastReadOffset = offset + length; // To check that the next range starts after the current one
// Read the JreDeflateParameters
// Note that v1 only supports the default deflate compatibility window.
checkRange(
dataIn.readByte(),
PatchConstants.CompatibilityWindowId.DEFAULT_DEFLATE.patchValue,
PatchConstants.CompatibilityWindowId.DEFAULT_DEFLATE.patchValue,
"compatibility window id");
int level = (int) checkRange(dataIn.readUnsignedByte(), 1, 9, "recompression level");
int strategy = (int) checkRange(dataIn.readUnsignedByte(), 0, 2, "recompression strategy");
int nowrapInt = (int) checkRange(dataIn.readUnsignedByte(), 0, 1, "recompression nowrap");
TypedRange range =
new TypedRange(
offset,
length,
JreDeflateParameters.of(level, strategy, nowrapInt == 0 ? false : true));
deltaFriendlyNewFileRecompressionPlan.add(range);
}
// Read the delta metadata, but stop before the first byte of the actual delta.
// V1 has exactly one delta and it must be bsdiff.
int numDeltaRecords = (int) checkRange(dataIn.readInt(), 1, 1, "num delta records");
List deltaDescriptors = new ArrayList(numDeltaRecords);
for (int x = 0; x < numDeltaRecords; x++) {
byte deltaFormatByte = (byte)
checkRange(
dataIn.readByte(),
PatchConstants.DeltaFormat.BSDIFF.patchValue,
PatchConstants.DeltaFormat.BSDIFF.patchValue,
"delta format");
long deltaFriendlyOldFileWorkRangeOffset = checkNonNegative(
dataIn.readLong(), "delta-friendly old file work range offset");
long deltaFriendlyOldFileWorkRangeLength = checkNonNegative(
dataIn.readLong(), "delta-friendly old file work range length");
long deltaFriendlyNewFileWorkRangeOffset = checkNonNegative(
dataIn.readLong(), "delta-friendly new file work range offset");
long deltaFriendlyNewFileWorkRangeLength = checkNonNegative(
dataIn.readLong(), "delta-friendly new file work range length");
long deltaLength = checkNonNegative(dataIn.readLong(), "delta length");
DeltaDescriptor descriptor =
new DeltaDescriptor(
PatchConstants.DeltaFormat.fromPatchValue(deltaFormatByte),
new TypedRange(
deltaFriendlyOldFileWorkRangeOffset, deltaFriendlyOldFileWorkRangeLength, null),
new TypedRange(
deltaFriendlyNewFileWorkRangeOffset, deltaFriendlyNewFileWorkRangeLength, null),
deltaLength);
deltaDescriptors.add(descriptor);
}
return new PatchApplyPlan(
Collections.unmodifiableList(oldFileUncompressionPlan),
deltaFriendlyOldFileSize,
Collections.unmodifiableList(deltaFriendlyNewFileRecompressionPlan),
Collections.unmodifiableList(deltaDescriptors));
}
主要就是对patch文件格式(上一篇文章有介绍写path,实际就是它的逆过程)进行解析:
1.读取文件头"GFbFv1_0",校验文件头
2.忽略4byte的标记位
3.旧文件差量友好文件的大小,校验
4.旧文件差量友好文件个数,并校验
5.读取n个旧文件待解压的偏移、长度、并校验
6.读取新文件待压缩个数,并校验
7.新文件待压缩文件的偏移、大小、压缩级别、编码策略、nowrap值,总共n个
8.新文件差量描述个数
9.读n个差量算法描述。差量算法id,旧文件应用差量算法的偏移和长度,新文件应用差量算法的偏移和长度,生成的差量文件的大小
返回PatchApplyPlan对象
写旧文件差量友好文件的过程跟上一篇一样,不复述:
/**
* Writes the delta-friendly old blob to temporary storage.
* @param plan the plan to use for uncompressing
* @param oldBlob the blob to turn into a delta-friendly blob
* @param deltaFriendlyOldBlob where to write the blob
* @throws IOException if anything goes wrong
*/
private void writeDeltaFriendlyOldBlob(
PatchApplyPlan plan, File oldBlob, File deltaFriendlyOldBlob) throws IOException {
RandomAccessFileOutputStream deltaFriendlyOldFileOut = null;
try {
deltaFriendlyOldFileOut =
new RandomAccessFileOutputStream(
deltaFriendlyOldBlob, plan.getDeltaFriendlyOldFileSize());
DeltaFriendlyFile.generateDeltaFriendlyFile(
plan.getOldFileUncompressionPlan(),
oldBlob,
deltaFriendlyOldFileOut,
false,
DEFAULT_COPY_BUFFER_SIZE);
} finally {
try {
deltaFriendlyOldFileOut.close();
} catch (Exception ignored) {
// Nothing
}
}
}
差量友好文件的合成使用bspatch算法写到输出流中
/**
* An implementation of {@link DeltaApplier} that uses {@link BsPatch} to apply a bsdiff patch.
*/
public class BsDiffDeltaApplier implements DeltaApplier {
@Override
public void applyDelta(File oldBlob, InputStream deltaIn, OutputStream newBlobOut)
throws IOException {
RandomAccessFile oldBlobRaf = null;
try {
oldBlobRaf = new RandomAccessFile(oldBlob, "r");
BsPatch.applyPatch(oldBlobRaf, newBlobOut, deltaIn);
} finally {
try {
oldBlobRaf.close();
} catch (Exception ignored) {
// Nothing
}
}
}
}
bspatch算法我们不看了,看一下这个输出流PartiallyCompressingOutputStream
,最终调用的是这个地方
/**
* Write up to length bytes from the specified buffer to the underlying stream. For
* simplicity, this method stops at the edges of ranges; it is always either copying OR
* compressing bytes, but never both. All manipulation of the compression state machinery is done
* within this method. When the end of a compression range is reached it is completely flushed to
* the output stream, to keep things as straightforward as possible.
* @param buffer the buffer to copy/compress bytes from
* @param offset the offset at which to start copying/compressing
* @param length the maximum number of bytes to copy or compress
* @return the number of bytes of the buffer that have been consumed
*/
private int writeChunk(byte[] buffer, int offset, int length) throws IOException {
if (bytesTillCompressionStarts() == 0 && !currentlyCompressing()) {
// Compression will begin immediately.
JreDeflateParameters parameters = nextCompressedRange.getMetadata();
if (deflater == null) {
deflater = new Deflater(parameters.level, parameters.nowrap);
} else if (lastDeflateParameters.nowrap != parameters.nowrap) {
// Last deflater must be destroyed because nowrap settings do not match.
deflater.end();
deflater = new Deflater(parameters.level, parameters.nowrap);
}
// Deflater will already have been reset at the end of this method, no need to do it again.
// Just set up the right parameters.
deflater.setLevel(parameters.level);
deflater.setStrategy(parameters.strategy);
deflaterOut = new DeflaterOutputStream(normalOut, deflater, compressionBufferSize);
}
int numBytesToWrite;
OutputStream writeTarget;
if (currentlyCompressing()) {
// Don't write past the end of the compressed range.
numBytesToWrite = (int) Math.min(length, bytesTillCompressionEnds());
writeTarget = deflaterOut;
} else {
writeTarget = normalOut;
if (nextCompressedRange == null) {
// All compression ranges have been consumed.
numBytesToWrite = length;
} else {
// Don't write past the point where the next compressed range begins.
numBytesToWrite = (int) Math.min(length, bytesTillCompressionStarts());
}
}
writeTarget.write(buffer, offset, numBytesToWrite);
numBytesWritten += numBytesToWrite;
if (currentlyCompressing() && bytesTillCompressionEnds() == 0) {
// Compression range complete. Finish the output and set up for the next run.
deflaterOut.finish();
deflaterOut.flush();
deflaterOut = null;
deflater.reset();
lastDeflateParameters = nextCompressedRange.getMetadata();
if (rangeIterator.hasNext()) {
// More compression ranges await in the future.
nextCompressedRange = rangeIterator.next();
} else {
// All compression ranges have been consumed.
nextCompressedRange = null;
deflater.end();
deflater = null;
}
}
return numBytesToWrite;
}
1.在PartiallyCompressingOutputStream的构造函数中,获得了compressionRanges的第一个数据
2.判断写入的数据距离下一个压缩数据开始如果是0,且当前并不在压缩,则获得压缩设置,即压缩级别,编码策略,nowrap,并进行设置。并包装输出流为压缩流。
3.如果当前正在压缩,则判断当前写入的数据长度和待压缩的数据长度,取其中小的一个,设置目标输出流为压缩流,即负责压缩工作,而不是拷贝工作。
4.如果当前不在压缩,如果没有下一个压缩数据了,则直接写入对应长度的数据,如果还有下一个压缩数据,则取当前写入数据的长度和距离下一个压缩数据的偏移位置的长度,取其中小的一个,设置目标输出流为正常流,即进行拷贝工作,而不是压缩工作
5.判断当前是否正在压缩,并且当前节点所有压缩数据都已经写入完全,执行压缩流的finish和flush操作,重置压缩相关配置项,并移动待压缩的数据到下一条记录。
6.重复以上操作,直到所有数据写入完全。