Merge "Ports DiffScriptBackupWriter from gmscore to AOSP."

This commit is contained in:
Bram Bonné
2019-02-07 09:36:51 +00:00
committed by Android (Google) Code Review
8 changed files with 621 additions and 0 deletions

View File

@@ -0,0 +1,80 @@
/*
* 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;
import com.android.internal.util.Preconditions;
/** Representation of a range of bytes to be downloaded. */
final class ByteRange {
private final long mStart;
private final long mEnd;
/** Creates a range of bytes which includes {@code mStart} and {@code mEnd}. */
ByteRange(long start, long end) {
Preconditions.checkArgument(start >= 0);
Preconditions.checkArgument(end >= start);
mStart = start;
mEnd = end;
}
/** Returns the start of the {@code ByteRange}. The start is included in the range. */
long getStart() {
return mStart;
}
/** Returns the end of the {@code ByteRange}. The end is included in the range. */
long getEnd() {
return mEnd;
}
/** Returns the number of bytes included in the {@code ByteRange}. */
int getLength() {
return (int) (mEnd - mStart + 1);
}
/** Creates a new {@link ByteRange} from {@code mStart} to {@code mEnd + length}. */
ByteRange extend(long length) {
Preconditions.checkArgument(length > 0);
return new ByteRange(mStart, mEnd + length);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof ByteRange)) {
return false;
}
ByteRange byteRange = (ByteRange) o;
return (mEnd == byteRange.mEnd && mStart == byteRange.mStart);
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + (int) (mStart ^ (mStart >>> 32));
result = 31 * result + (int) (mEnd ^ (mEnd >>> 32));
return result;
}
@Override
public String toString() {
return String.format("ByteRange{mStart=%d, mEnd=%d}", mStart, mEnd);
}
}

View File

@@ -0,0 +1,75 @@
/*
* 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;
import com.android.internal.annotations.VisibleForTesting;
import java.io.IOException;
import java.io.OutputStream;
/** Writes backup data to a diff script, using a {@link SingleStreamDiffScriptWriter}. */
public class DiffScriptBackupWriter implements BackupWriter {
/**
* The maximum size of a chunk in the diff script. The diff script writer {@code mWriter} will
* buffer this many bytes in memory.
*/
private static final int ENCRYPTION_DIFF_SCRIPT_MAX_CHUNK_SIZE_BYTES = 1024 * 1024;
private final SingleStreamDiffScriptWriter mWriter;
private long mBytesWritten;
/**
* Constructs a new writer which writes the diff script to the given output stream, using the
* maximum new chunk size {@code ENCRYPTION_DIFF_SCRIPT_MAX_CHUNK_SIZE_BYTES}.
*/
public static DiffScriptBackupWriter newInstance(OutputStream outputStream) {
SingleStreamDiffScriptWriter writer =
new SingleStreamDiffScriptWriter(
outputStream, ENCRYPTION_DIFF_SCRIPT_MAX_CHUNK_SIZE_BYTES);
return new DiffScriptBackupWriter(writer);
}
@VisibleForTesting
DiffScriptBackupWriter(SingleStreamDiffScriptWriter writer) {
mWriter = writer;
}
@Override
public void writeBytes(byte[] bytes) throws IOException {
for (byte b : bytes) {
mWriter.writeByte(b);
}
mBytesWritten += bytes.length;
}
@Override
public void writeChunk(long start, int length) throws IOException {
mWriter.writeChunk(start, length);
mBytesWritten += length;
}
@Override
public long getBytesWritten() {
return mBytesWritten;
}
@Override
public void flush() throws IOException {
mWriter.flush();
}
}

View File

@@ -0,0 +1,36 @@
/*
* 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;
import java.io.IOException;
import java.io.OutputStream;
/** Writer that formats a Diff Script and writes it to an output source. */
interface DiffScriptWriter {
/** Adds a new byte to the diff script. */
void writeByte(byte b) throws IOException;
/** Adds a known chunk to the diff script. */
void writeChunk(long chunkStart, int chunkLength) throws IOException;
/** Indicates that no more bytes or chunks will be added to the diff script. */
void flush() throws IOException;
interface Factory {
DiffScriptWriter create(OutputStream outputStream);
}
}

View File

@@ -0,0 +1,25 @@
/*
* 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;
import java.io.OutputStream;
/** An interface that wraps one {@link OutputStream} with another for filtration purposes. */
public interface OutputStreamWrapper {
/** Wraps a given {@link OutputStream}. */
OutputStream wrap(OutputStream outputStream);
}

View File

@@ -0,0 +1,130 @@
/*
* 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;
import android.annotation.Nullable;
import com.android.internal.util.Preconditions;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.Locale;
/**
* A {@link DiffScriptWriter} that writes an entire diff script to a single {@link OutputStream}.
*/
public class SingleStreamDiffScriptWriter implements DiffScriptWriter {
static final byte LINE_SEPARATOR = 0xA;
private static final Charset UTF_8 = Charset.forName("UTF-8");
private final int mMaxNewByteChunkSize;
private final OutputStream mOutputStream;
private final byte[] mByteBuffer;
private int mBufferSize = 0;
// Each chunk could be written immediately to the output stream. However,
// it is possible that chunks may overlap. We therefore cache the most recent
// reusable chunk and try to merge it with future chunks.
private ByteRange mReusableChunk;
public SingleStreamDiffScriptWriter(OutputStream outputStream, int maxNewByteChunkSize) {
mOutputStream = outputStream;
mMaxNewByteChunkSize = maxNewByteChunkSize;
mByteBuffer = new byte[maxNewByteChunkSize];
}
@Override
public void writeByte(byte b) throws IOException {
if (mReusableChunk != null) {
writeReusableChunk();
}
mByteBuffer[mBufferSize++] = b;
if (mBufferSize == mMaxNewByteChunkSize) {
writeByteBuffer();
}
}
@Override
public void writeChunk(long chunkStart, int chunkLength) throws IOException {
Preconditions.checkArgument(chunkStart >= 0);
Preconditions.checkArgument(chunkLength > 0);
if (mBufferSize != 0) {
writeByteBuffer();
}
if (mReusableChunk != null && mReusableChunk.getEnd() + 1 == chunkStart) {
// The new chunk overlaps the old, so combine them into a single byte range.
mReusableChunk = mReusableChunk.extend(chunkLength);
} else {
writeReusableChunk();
mReusableChunk = new ByteRange(chunkStart, chunkStart + chunkLength - 1);
}
}
@Override
public void flush() throws IOException {
Preconditions.checkState(!(mBufferSize != 0 && mReusableChunk != null));
if (mBufferSize != 0) {
writeByteBuffer();
}
if (mReusableChunk != null) {
writeReusableChunk();
}
mOutputStream.flush();
}
private void writeByteBuffer() throws IOException {
mOutputStream.write(Integer.toString(mBufferSize).getBytes(UTF_8));
mOutputStream.write(LINE_SEPARATOR);
mOutputStream.write(mByteBuffer, 0, mBufferSize);
mOutputStream.write(LINE_SEPARATOR);
mBufferSize = 0;
}
private void writeReusableChunk() throws IOException {
if (mReusableChunk != null) {
mOutputStream.write(
String.format(
Locale.US,
"%d-%d",
mReusableChunk.getStart(),
mReusableChunk.getEnd())
.getBytes(UTF_8));
mOutputStream.write(LINE_SEPARATOR);
mReusableChunk = null;
}
}
/** A factory that creates {@link SingleStreamDiffScriptWriter}s. */
public static class Factory implements DiffScriptWriter.Factory {
private final int mMaxNewByteChunkSize;
private final OutputStreamWrapper mOutputStreamWrapper;
public Factory(int maxNewByteChunkSize, @Nullable OutputStreamWrapper outputStreamWrapper) {
mMaxNewByteChunkSize = maxNewByteChunkSize;
mOutputStreamWrapper = outputStreamWrapper;
}
@Override
public SingleStreamDiffScriptWriter create(OutputStream outputStream) {
if (mOutputStreamWrapper != null) {
outputStream = mOutputStreamWrapper.wrap(outputStream);
}
return new SingleStreamDiffScriptWriter(outputStream, mMaxNewByteChunkSize);
}
}
}

View File

@@ -0,0 +1,57 @@
/*
* 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;
import static org.junit.Assert.assertEquals;
import static org.testng.Assert.assertThrows;
import android.platform.test.annotations.Presubmit;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
/** Tests for {@link ByteRange}. */
@RunWith(RobolectricTestRunner.class)
@Presubmit
public class ByteRangeTest {
@Test
public void getLength_includesEnd() throws Exception {
ByteRange byteRange = new ByteRange(5, 10);
int length = byteRange.getLength();
assertEquals(6, length);
}
@Test
public void constructor_rejectsNegativeStart() {
assertThrows(IllegalArgumentException.class, () -> new ByteRange(-1, 10));
}
@Test
public void constructor_rejectsEndBeforeStart() {
assertThrows(IllegalArgumentException.class, () -> new ByteRange(10, 9));
}
@Test
public void extend_withZeroLength_throwsException() {
ByteRange byteRange = new ByteRange(5, 10);
assertThrows(IllegalArgumentException.class, () -> byteRange.extend(0));
}
}

View File

@@ -0,0 +1,90 @@
/*
* 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;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import android.platform.test.annotations.Presubmit;
import com.google.common.primitives.Bytes;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.robolectric.RobolectricTestRunner;
import java.io.IOException;
/** Tests for {@link DiffScriptBackupWriter}. */
@RunWith(RobolectricTestRunner.class)
@Presubmit
public class DiffScriptBackupWriterTest {
private static final byte[] TEST_BYTES = {1, 2, 3, 4, 5, 6, 7, 8, 9};
@Captor private ArgumentCaptor<Byte> mBytesCaptor;
@Mock private SingleStreamDiffScriptWriter mDiffScriptWriter;
private BackupWriter mBackupWriter;
@Before
public void setUp() {
mDiffScriptWriter = mock(SingleStreamDiffScriptWriter.class);
mBackupWriter = new DiffScriptBackupWriter(mDiffScriptWriter);
mBytesCaptor = ArgumentCaptor.forClass(Byte.class);
}
@Test
public void writeBytes_writesBytesToWriter() throws Exception {
mBackupWriter.writeBytes(TEST_BYTES);
verify(mDiffScriptWriter, atLeastOnce()).writeByte(mBytesCaptor.capture());
assertThat(mBytesCaptor.getAllValues())
.containsExactlyElementsIn(Bytes.asList(TEST_BYTES))
.inOrder();
}
@Test
public void writeChunk_writesChunkToWriter() throws Exception {
mBackupWriter.writeChunk(0, 10);
verify(mDiffScriptWriter).writeChunk(0, 10);
}
@Test
public void getBytesWritten_returnsTotalSum() throws Exception {
mBackupWriter.writeBytes(TEST_BYTES);
mBackupWriter.writeBytes(TEST_BYTES);
mBackupWriter.writeChunk(/*start=*/ 0, /*length=*/ 10);
long bytesWritten = mBackupWriter.getBytesWritten();
assertThat(bytesWritten).isEqualTo(2 * TEST_BYTES.length + 10);
}
@Test
public void flush_flushesWriter() throws IOException {
mBackupWriter.flush();
verify(mDiffScriptWriter).flush();
}
}

View File

@@ -0,0 +1,128 @@
/*
* 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;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.testng.Assert.assertThrows;
import android.platform.test.annotations.Presubmit;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Locale;
/** Tests for {@link SingleStreamDiffScriptWriter}. */
@RunWith(RobolectricTestRunner.class)
@Presubmit
public class SingleStreamDiffScriptWriterTest {
private static final int MAX_CHUNK_SIZE_IN_BYTES = 256;
/** By default this Locale does not use Arabic numbers for %d formatting. */
private static final Locale HINDI = new Locale("hi", "IN");
private Locale mDefaultLocale;
private ByteArrayOutputStream mOutputStream;
private SingleStreamDiffScriptWriter mDiffScriptWriter;
@Before
public void setUp() {
mDefaultLocale = Locale.getDefault();
mOutputStream = new ByteArrayOutputStream();
mDiffScriptWriter =
new SingleStreamDiffScriptWriter(mOutputStream, MAX_CHUNK_SIZE_IN_BYTES);
}
@After
public void tearDown() {
Locale.setDefault(mDefaultLocale);
}
@Test
public void writeChunk_withNegativeStart_throwsException() {
assertThrows(
IllegalArgumentException.class,
() -> mDiffScriptWriter.writeChunk(-1, 50));
}
@Test
public void writeChunk_withZeroLength_throwsException() {
assertThrows(
IllegalArgumentException.class,
() -> mDiffScriptWriter.writeChunk(0, 0));
}
@Test
public void writeChunk_withExistingBytesInBuffer_writesBufferFirst()
throws IOException {
String testString = "abcd";
writeStringAsBytesToWriter(testString, mDiffScriptWriter);
mDiffScriptWriter.writeChunk(0, 20);
mDiffScriptWriter.flush();
// Expected format: length of abcd, newline, abcd, newline, chunk start - chunk end
assertThat(mOutputStream.toString("UTF-8")).isEqualTo(
String.format("%d\n%s\n%d-%d\n", testString.length(), testString, 0, 19));
}
@Test
public void writeChunk_overlappingPreviousChunk_combinesChunks() throws IOException {
mDiffScriptWriter.writeChunk(3, 4);
mDiffScriptWriter.writeChunk(7, 5);
mDiffScriptWriter.flush();
assertThat(mOutputStream.toString("UTF-8")).isEqualTo(String.format("3-11\n"));
}
@Test
public void writeChunk_formatsByteIndexesUsingArabicNumbers() throws Exception {
Locale.setDefault(HINDI);
mDiffScriptWriter.writeChunk(0, 12345);
mDiffScriptWriter.flush();
assertThat(mOutputStream.toString("UTF-8")).isEqualTo("0-12344\n");
}
@Test
public void flush_flushesOutputStream() throws IOException {
ByteArrayOutputStream mockOutputStream = mock(ByteArrayOutputStream.class);
SingleStreamDiffScriptWriter diffScriptWriter =
new SingleStreamDiffScriptWriter(mockOutputStream, MAX_CHUNK_SIZE_IN_BYTES);
diffScriptWriter.flush();
verify(mockOutputStream).flush();
}
private void writeStringAsBytesToWriter(String string, SingleStreamDiffScriptWriter writer)
throws IOException {
byte[] bytes = string.getBytes("UTF-8");
for (int i = 0; i < bytes.length; i++) {
writer.writeByte(bytes[i]);
}
}
}