blob: 18011f620b244672e365ba86f808583aae420ee9 [file] [log] [blame]
/*
* Copyright (C) 2018 The Android Open Source Project
*
* 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.android.server.backup.encryption.chunking.cdc;
import static com.android.internal.util.Preconditions.checkArgument;
import com.android.server.backup.encryption.chunking.Chunker;
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
import java.util.Arrays;
/** Splits a stream of bytes into variable-sized chunks, using content-defined chunking. */
public class ContentDefinedChunker implements Chunker {
private static final int WINDOW_SIZE = 31;
private static final byte DEFAULT_OUT_BYTE = (byte) 0;
private final byte[] mChunkBuffer;
private final RabinFingerprint64 mRabinFingerprint64;
private final FingerprintMixer mFingerprintMixer;
private final BreakpointPredicate mBreakpointPredicate;
private final int mMinChunkSize;
private final int mMaxChunkSize;
/**
* Constructor.
*
* @param minChunkSize The minimum size of a chunk. No chunk will be produced of a size smaller
* than this except possibly at the very end of the stream.
* @param maxChunkSize The maximum size of a chunk. No chunk will be produced of a larger size.
* @param rabinFingerprint64 Calculates fingerprints, with which to determine breakpoints.
* @param breakpointPredicate Given a Rabin fingerprint, returns whether this ought to be a
* breakpoint.
*/
public ContentDefinedChunker(
int minChunkSize,
int maxChunkSize,
RabinFingerprint64 rabinFingerprint64,
FingerprintMixer fingerprintMixer,
BreakpointPredicate breakpointPredicate) {
checkArgument(
minChunkSize >= WINDOW_SIZE,
"Minimum chunk size must be greater than window size.");
checkArgument(
maxChunkSize >= minChunkSize,
"Maximum chunk size cannot be smaller than minimum chunk size.");
mChunkBuffer = new byte[maxChunkSize];
mRabinFingerprint64 = rabinFingerprint64;
mBreakpointPredicate = breakpointPredicate;
mFingerprintMixer = fingerprintMixer;
mMinChunkSize = minChunkSize;
mMaxChunkSize = maxChunkSize;
}
/**
* Breaks the input stream into variable-sized chunks.
*
* @param inputStream The input bytes to break into chunks.
* @param chunkConsumer A function to process each chunk as it's generated.
* @throws IOException Thrown if there is an issue reading from the input stream.
* @throws GeneralSecurityException Thrown if the {@link ChunkConsumer} throws it.
*/
@Override
public void chunkify(InputStream inputStream, ChunkConsumer chunkConsumer)
throws IOException, GeneralSecurityException {
int chunkLength;
int initialReadLength = mMinChunkSize - WINDOW_SIZE;
// Performance optimization - there is no reason to calculate fingerprints for windows
// ending before the minimum chunk size.
while ((chunkLength =
inputStream.read(mChunkBuffer, /*off=*/ 0, /*len=*/ initialReadLength))
!= -1) {
int b;
long fingerprint = 0L;
while ((b = inputStream.read()) != -1) {
byte inByte = (byte) b;
byte outByte = getCurrentWindowStartByte(chunkLength);
mChunkBuffer[chunkLength++] = inByte;
fingerprint =
mRabinFingerprint64.computeFingerprint64(inByte, outByte, fingerprint);
if (chunkLength >= mMaxChunkSize
|| (chunkLength >= mMinChunkSize
&& mBreakpointPredicate.isBreakpoint(
mFingerprintMixer.mix(fingerprint)))) {
chunkConsumer.accept(Arrays.copyOf(mChunkBuffer, chunkLength));
chunkLength = 0;
break;
}
}
if (chunkLength > 0) {
chunkConsumer.accept(Arrays.copyOf(mChunkBuffer, chunkLength));
}
}
}
private byte getCurrentWindowStartByte(int chunkLength) {
if (chunkLength < mMinChunkSize) {
return DEFAULT_OUT_BYTE;
} else {
return mChunkBuffer[chunkLength - WINDOW_SIZE];
}
}
/** Whether the current fingerprint indicates the end of a chunk. */
public interface BreakpointPredicate {
/**
* Returns {@code true} if the fingerprint of the last {@code WINDOW_SIZE} bytes indicates
* the chunk ought to end at this position.
*
* @param fingerprint Fingerprint of the last {@code WINDOW_SIZE} bytes.
* @return Whether this ought to be a chunk breakpoint.
*/
boolean isBreakpoint(long fingerprint);
}
}