Import ProtoStore

Bug: 111386661
Test: make RunBackupEncryptionRoboTests
Change-Id: I9cbaf2c1f1e933b08ac578e4243e8555e552ef1d
This commit is contained in:
Al Sutton
2019-09-17 15:43:39 +01:00
parent 295aaad6a6
commit 7d54d6aa6f
2 changed files with 438 additions and 0 deletions

View File

@@ -0,0 +1,174 @@
/*
* Copyright (C) 2019 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.android.internal.util.Preconditions.checkNotNull;
import android.content.Context;
import android.text.TextUtils;
import android.util.AtomicFile;
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
import com.android.server.backup.encryption.protos.nano.KeyValueListingProto;
import com.google.protobuf.nano.MessageNano;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Optional;
/**
* Stores a nano proto for each package, persisting the proto to disk.
*
* <p>This is used to store {@link ChunksMetadataProto.ChunkListing}.
*
* @param <T> the type of nano proto to store.
*/
public class ProtoStore<T extends MessageNano> {
private static final String CHUNK_LISTING_FOLDER = "backup_chunk_listings";
private static final String KEY_VALUE_LISTING_FOLDER = "backup_kv_listings";
private static final String TAG = "BupEncProtoStore";
private final File mStoreFolder;
private final Class<T> mClazz;
/** Creates a new instance which stores chunk listings at the default location. */
public static ProtoStore<ChunksMetadataProto.ChunkListing> createChunkListingStore(
Context context) throws IOException {
return new ProtoStore<>(
ChunksMetadataProto.ChunkListing.class,
new File(context.getFilesDir().getAbsoluteFile(), CHUNK_LISTING_FOLDER));
}
/** Creates a new instance which stores key value listings in the default location. */
public static ProtoStore<KeyValueListingProto.KeyValueListing> createKeyValueListingStore(
Context context) throws IOException {
return new ProtoStore<>(
KeyValueListingProto.KeyValueListing.class,
new File(context.getFilesDir().getAbsoluteFile(), KEY_VALUE_LISTING_FOLDER));
}
/**
* Creates a new instance which stores protos in the given folder.
*
* @param storeFolder The location where the serialized form is stored.
*/
@VisibleForTesting
ProtoStore(Class<T> clazz, File storeFolder) throws IOException {
mClazz = checkNotNull(clazz);
mStoreFolder = ensureDirectoryExistsOrThrow(storeFolder);
}
private static File ensureDirectoryExistsOrThrow(File directory) throws IOException {
if (directory.exists() && !directory.isDirectory()) {
throw new IOException("Store folder already exists, but isn't a directory.");
}
if (!directory.exists() && !directory.mkdir()) {
throw new IOException("Unable to create store folder.");
}
return directory;
}
/**
* Returns the chunk listing for the given package, or {@link Optional#empty()} if no listing
* exists.
*/
public Optional<T> loadProto(String packageName)
throws IOException, IllegalAccessException, InstantiationException,
NoSuchMethodException, InvocationTargetException {
File file = getFileForPackage(packageName);
if (!file.exists()) {
Slog.d(
TAG,
"No chunk listing existed for " + packageName + ", returning empty listing.");
return Optional.empty();
}
AtomicFile protoStore = new AtomicFile(file);
byte[] data = protoStore.readFully();
Constructor<T> constructor = mClazz.getDeclaredConstructor();
T proto = constructor.newInstance();
MessageNano.mergeFrom(proto, data);
return Optional.of(proto);
}
/** Saves a proto to disk, associating it with the given package. */
public void saveProto(String packageName, T proto) throws IOException {
checkNotNull(proto);
File file = getFileForPackage(packageName);
try (FileOutputStream os = new FileOutputStream(file)) {
os.write(MessageNano.toByteArray(proto));
} catch (IOException e) {
Slog.e(
TAG,
"Exception occurred when saving the listing for "
+ packageName
+ ", deleting saved listing.",
e);
// If a problem occurred when writing the listing then it might be corrupt, so delete
// it.
file.delete();
throw e;
}
}
/** Deletes the proto for the given package, or does nothing if the package has no proto. */
public void deleteProto(String packageName) {
File file = getFileForPackage(packageName);
file.delete();
}
/** Deletes every proto of this type, for all package names. */
public void deleteAllProtos() {
File[] files = mStoreFolder.listFiles();
// We ensure that the storeFolder exists in the constructor, but check just in case it has
// mysteriously disappeared.
if (files == null) {
return;
}
for (File file : files) {
file.delete();
}
}
private File getFileForPackage(String packageName) {
checkPackageName(packageName);
return new File(mStoreFolder, packageName);
}
private static void checkPackageName(String packageName) {
if (TextUtils.isEmpty(packageName) || packageName.contains("/")) {
throw new IllegalArgumentException(
"Package name must not contain '/' or be empty: " + packageName);
}
}
}

View File

@@ -0,0 +1,264 @@
/*
* Copyright (C) 2019 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 com.google.common.truth.Truth.assertWithMessage;
import static org.testng.Assert.assertThrows;
import android.content.Context;
import android.platform.test.annotations.Presubmit;
import androidx.test.core.app.ApplicationProvider;
import com.android.server.backup.encryption.chunk.ChunkHash;
import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
import com.android.server.backup.encryption.protos.nano.KeyValueListingProto;
import com.google.common.collect.ImmutableMap;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map.Entry;
import java.util.Optional;
@RunWith(RobolectricTestRunner.class)
@Presubmit
public class ProtoStoreTest {
private static final String TEST_KEY_1 = "test_key_1";
private static final ChunkHash TEST_HASH_1 =
new ChunkHash(Arrays.copyOf(new byte[] {1}, EncryptedChunk.KEY_LENGTH_BYTES));
private static final ChunkHash TEST_HASH_2 =
new ChunkHash(Arrays.copyOf(new byte[] {2}, EncryptedChunk.KEY_LENGTH_BYTES));
private static final int TEST_LENGTH_1 = 10;
private static final int TEST_LENGTH_2 = 18;
private static final String TEST_PACKAGE_1 = "com.example.test1";
private static final String TEST_PACKAGE_2 = "com.example.test2";
@Rule public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
private File mStoreFolder;
private ProtoStore<ChunksMetadataProto.ChunkListing> mProtoStore;
@Before
public void setUp() throws Exception {
mStoreFolder = mTemporaryFolder.newFolder();
mProtoStore = new ProtoStore<>(ChunksMetadataProto.ChunkListing.class, mStoreFolder);
}
@Test
public void differentStoreTypes_operateSimultaneouslyWithoutInterfering() throws Exception {
ChunksMetadataProto.ChunkListing chunkListing =
createChunkListing(ImmutableMap.of(TEST_HASH_1, TEST_LENGTH_1));
KeyValueListingProto.KeyValueListing keyValueListing =
new KeyValueListingProto.KeyValueListing();
keyValueListing.entries = new KeyValueListingProto.KeyValueEntry[1];
keyValueListing.entries[0] = new KeyValueListingProto.KeyValueEntry();
keyValueListing.entries[0].key = TEST_KEY_1;
keyValueListing.entries[0].hash = TEST_HASH_1.getHash();
Context application = ApplicationProvider.getApplicationContext();
ProtoStore<ChunksMetadataProto.ChunkListing> chunkListingStore =
ProtoStore.createChunkListingStore(application);
ProtoStore<KeyValueListingProto.KeyValueListing> keyValueListingStore =
ProtoStore.createKeyValueListingStore(application);
chunkListingStore.saveProto(TEST_PACKAGE_1, chunkListing);
keyValueListingStore.saveProto(TEST_PACKAGE_1, keyValueListing);
ChunksMetadataProto.ChunkListing actualChunkListing =
chunkListingStore.loadProto(TEST_PACKAGE_1).get();
KeyValueListingProto.KeyValueListing actualKeyValueListing =
keyValueListingStore.loadProto(TEST_PACKAGE_1).get();
assertListingsEqual(actualChunkListing, chunkListing);
assertThat(actualKeyValueListing.entries.length).isEqualTo(1);
assertThat(actualKeyValueListing.entries[0].key).isEqualTo(TEST_KEY_1);
assertThat(actualKeyValueListing.entries[0].hash).isEqualTo(TEST_HASH_1.getHash());
}
@Test
public void construct_storeLocationIsFile_throws() throws Exception {
assertThrows(
IOException.class,
() ->
new ProtoStore<>(
ChunksMetadataProto.ChunkListing.class,
mTemporaryFolder.newFile()));
}
@Test
public void loadChunkListing_noListingExists_returnsEmptyListing() throws Exception {
Optional<ChunksMetadataProto.ChunkListing> chunkListing =
mProtoStore.loadProto(TEST_PACKAGE_1);
assertThat(chunkListing.isPresent()).isFalse();
}
@Test
public void loadChunkListing_listingExists_returnsExistingListing() throws Exception {
ChunksMetadataProto.ChunkListing expected =
createChunkListing(
ImmutableMap.of(TEST_HASH_1, TEST_LENGTH_1, TEST_HASH_2, TEST_LENGTH_2));
mProtoStore.saveProto(TEST_PACKAGE_1, expected);
ChunksMetadataProto.ChunkListing result = mProtoStore.loadProto(TEST_PACKAGE_1).get();
assertListingsEqual(result, expected);
}
@Test
public void loadProto_emptyPackageName_throwsException() throws Exception {
assertThrows(IllegalArgumentException.class, () -> mProtoStore.loadProto(""));
}
@Test
public void loadProto_nullPackageName_throwsException() throws Exception {
assertThrows(IllegalArgumentException.class, () -> mProtoStore.loadProto(null));
}
@Test
public void loadProto_packageNameContainsSlash_throwsException() throws Exception {
assertThrows(
IllegalArgumentException.class, () -> mProtoStore.loadProto(TEST_PACKAGE_1 + "/"));
}
@Test
public void saveProto_persistsToNewInstance() throws Exception {
ChunksMetadataProto.ChunkListing expected =
createChunkListing(
ImmutableMap.of(TEST_HASH_1, TEST_LENGTH_1, TEST_HASH_2, TEST_LENGTH_2));
mProtoStore.saveProto(TEST_PACKAGE_1, expected);
mProtoStore = new ProtoStore<>(ChunksMetadataProto.ChunkListing.class, mStoreFolder);
ChunksMetadataProto.ChunkListing result = mProtoStore.loadProto(TEST_PACKAGE_1).get();
assertListingsEqual(result, expected);
}
@Test
public void saveProto_emptyPackageName_throwsException() throws Exception {
assertThrows(
IllegalArgumentException.class,
() -> mProtoStore.saveProto("", new ChunksMetadataProto.ChunkListing()));
}
@Test
public void saveProto_nullPackageName_throwsException() throws Exception {
assertThrows(
IllegalArgumentException.class,
() -> mProtoStore.saveProto(null, new ChunksMetadataProto.ChunkListing()));
}
@Test
public void saveProto_packageNameContainsSlash_throwsException() throws Exception {
assertThrows(
IllegalArgumentException.class,
() ->
mProtoStore.saveProto(
TEST_PACKAGE_1 + "/", new ChunksMetadataProto.ChunkListing()));
}
@Test
public void saveProto_nullListing_throwsException() throws Exception {
assertThrows(NullPointerException.class, () -> mProtoStore.saveProto(TEST_PACKAGE_1, null));
}
@Test
public void deleteProto_noListingExists_doesNothing() throws Exception {
ChunksMetadataProto.ChunkListing listing =
createChunkListing(ImmutableMap.of(TEST_HASH_1, TEST_LENGTH_1));
mProtoStore.saveProto(TEST_PACKAGE_1, listing);
mProtoStore.deleteProto(TEST_PACKAGE_2);
assertThat(mProtoStore.loadProto(TEST_PACKAGE_1).get().chunks.length).isEqualTo(1);
}
@Test
public void deleteProto_listingExists_deletesListing() throws Exception {
ChunksMetadataProto.ChunkListing listing =
createChunkListing(ImmutableMap.of(TEST_HASH_1, TEST_LENGTH_1));
mProtoStore.saveProto(TEST_PACKAGE_1, listing);
mProtoStore.deleteProto(TEST_PACKAGE_1);
assertThat(mProtoStore.loadProto(TEST_PACKAGE_1).isPresent()).isFalse();
}
@Test
public void deleteAllProtos_deletesAllProtos() throws Exception {
ChunksMetadataProto.ChunkListing listing1 =
createChunkListing(ImmutableMap.of(TEST_HASH_1, TEST_LENGTH_1));
ChunksMetadataProto.ChunkListing listing2 =
createChunkListing(ImmutableMap.of(TEST_HASH_2, TEST_LENGTH_2));
mProtoStore.saveProto(TEST_PACKAGE_1, listing1);
mProtoStore.saveProto(TEST_PACKAGE_2, listing2);
mProtoStore.deleteAllProtos();
assertThat(mProtoStore.loadProto(TEST_PACKAGE_1).isPresent()).isFalse();
assertThat(mProtoStore.loadProto(TEST_PACKAGE_2).isPresent()).isFalse();
}
@Test
public void deleteAllProtos_folderDeleted_doesNotCrash() throws Exception {
mStoreFolder.delete();
mProtoStore.deleteAllProtos();
}
private static ChunksMetadataProto.ChunkListing createChunkListing(
ImmutableMap<ChunkHash, Integer> chunks) {
ChunksMetadataProto.ChunkListing listing = new ChunksMetadataProto.ChunkListing();
listing.cipherType = ChunksMetadataProto.AES_256_GCM;
listing.chunkOrderingType = ChunksMetadataProto.CHUNK_ORDERING_TYPE_UNSPECIFIED;
List<ChunksMetadataProto.Chunk> chunkProtos = new ArrayList<>();
for (Entry<ChunkHash, Integer> entry : chunks.entrySet()) {
ChunksMetadataProto.Chunk chunk = new ChunksMetadataProto.Chunk();
chunk.hash = entry.getKey().getHash();
chunk.length = entry.getValue();
chunkProtos.add(chunk);
}
listing.chunks = chunkProtos.toArray(new ChunksMetadataProto.Chunk[0]);
return listing;
}
private void assertListingsEqual(
ChunksMetadataProto.ChunkListing result, ChunksMetadataProto.ChunkListing expected) {
assertThat(result.chunks.length).isEqualTo(expected.chunks.length);
for (int i = 0; i < result.chunks.length; i++) {
assertWithMessage("Chunk " + i)
.that(result.chunks[i].length)
.isEqualTo(expected.chunks[i].length);
assertWithMessage("Chunk " + i)
.that(result.chunks[i].hash)
.isEqualTo(expected.chunks[i].hash);
}
}
}