Import ProtoStore
Bug: 111386661
Test: make RunBackupEncryptionRoboTests
Change-Id: I9cbaf2c1f1e933b08ac578e4243e8555e552ef1d
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/ProtoStore.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/ProtoStore.java
new file mode 100644
index 0000000..3ba5f2b
--- /dev/null
+++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/chunking/ProtoStore.java
@@ -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);
+ }
+ }
+}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/ProtoStoreTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/ProtoStoreTest.java
new file mode 100644
index 0000000..d73c8e4
--- /dev/null
+++ b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/chunking/ProtoStoreTest.java
@@ -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);
+ }
+ }
+}