1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.backup.encryption.chunking; 18 19 import android.content.Context; 20 import android.text.TextUtils; 21 import android.util.AtomicFile; 22 import android.util.Slog; 23 24 import com.android.internal.annotations.VisibleForTesting; 25 import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto; 26 import com.android.server.backup.encryption.protos.nano.KeyValueListingProto; 27 28 import com.google.protobuf.nano.MessageNano; 29 30 import java.io.File; 31 import java.io.FileOutputStream; 32 import java.io.IOException; 33 import java.lang.reflect.Constructor; 34 import java.lang.reflect.InvocationTargetException; 35 import java.util.Objects; 36 import java.util.Optional; 37 38 /** 39 * Stores a nano proto for each package, persisting the proto to disk. 40 * 41 * <p>This is used to store {@link ChunksMetadataProto.ChunkListing}. 42 * 43 * @param <T> the type of nano proto to store. 44 */ 45 public class ProtoStore<T extends MessageNano> { 46 private static final String CHUNK_LISTING_FOLDER = "backup_chunk_listings"; 47 private static final String KEY_VALUE_LISTING_FOLDER = "backup_kv_listings"; 48 49 private static final String TAG = "BupEncProtoStore"; 50 51 private final File mStoreFolder; 52 private final Class<T> mClazz; 53 54 /** Creates a new instance which stores chunk listings at the default location. */ createChunkListingStore( Context context)55 public static ProtoStore<ChunksMetadataProto.ChunkListing> createChunkListingStore( 56 Context context) throws IOException { 57 return new ProtoStore<>( 58 ChunksMetadataProto.ChunkListing.class, 59 new File(context.getFilesDir().getAbsoluteFile(), CHUNK_LISTING_FOLDER)); 60 } 61 62 /** Creates a new instance which stores key value listings in the default location. */ createKeyValueListingStore( Context context)63 public static ProtoStore<KeyValueListingProto.KeyValueListing> createKeyValueListingStore( 64 Context context) throws IOException { 65 return new ProtoStore<>( 66 KeyValueListingProto.KeyValueListing.class, 67 new File(context.getFilesDir().getAbsoluteFile(), KEY_VALUE_LISTING_FOLDER)); 68 } 69 70 /** 71 * Creates a new instance which stores protos in the given folder. 72 * 73 * @param storeFolder The location where the serialized form is stored. 74 */ 75 @VisibleForTesting ProtoStore(Class<T> clazz, File storeFolder)76 ProtoStore(Class<T> clazz, File storeFolder) throws IOException { 77 mClazz = Objects.requireNonNull(clazz); 78 mStoreFolder = ensureDirectoryExistsOrThrow(storeFolder); 79 } 80 ensureDirectoryExistsOrThrow(File directory)81 private static File ensureDirectoryExistsOrThrow(File directory) throws IOException { 82 if (directory.exists() && !directory.isDirectory()) { 83 throw new IOException("Store folder already exists, but isn't a directory."); 84 } 85 86 if (!directory.exists() && !directory.mkdir()) { 87 throw new IOException("Unable to create store folder."); 88 } 89 90 return directory; 91 } 92 93 /** 94 * Returns the chunk listing for the given package, or {@link Optional#empty()} if no listing 95 * exists. 96 */ loadProto(String packageName)97 public Optional<T> loadProto(String packageName) 98 throws IOException, IllegalAccessException, InstantiationException, 99 NoSuchMethodException, InvocationTargetException { 100 File file = getFileForPackage(packageName); 101 102 if (!file.exists()) { 103 Slog.d( 104 TAG, 105 "No chunk listing existed for " + packageName + ", returning empty listing."); 106 return Optional.empty(); 107 } 108 109 AtomicFile protoStore = new AtomicFile(file); 110 byte[] data = protoStore.readFully(); 111 112 Constructor<T> constructor = mClazz.getDeclaredConstructor(); 113 T proto = constructor.newInstance(); 114 MessageNano.mergeFrom(proto, data); 115 return Optional.of(proto); 116 } 117 118 /** Saves a proto to disk, associating it with the given package. */ saveProto(String packageName, T proto)119 public void saveProto(String packageName, T proto) throws IOException { 120 Objects.requireNonNull(proto); 121 File file = getFileForPackage(packageName); 122 123 try (FileOutputStream os = new FileOutputStream(file)) { 124 os.write(MessageNano.toByteArray(proto)); 125 } catch (IOException e) { 126 Slog.e( 127 TAG, 128 "Exception occurred when saving the listing for " 129 + packageName 130 + ", deleting saved listing.", 131 e); 132 133 // If a problem occurred when writing the listing then it might be corrupt, so delete 134 // it. 135 file.delete(); 136 137 throw e; 138 } 139 } 140 141 /** Deletes the proto for the given package, or does nothing if the package has no proto. */ deleteProto(String packageName)142 public void deleteProto(String packageName) { 143 File file = getFileForPackage(packageName); 144 file.delete(); 145 } 146 147 /** Deletes every proto of this type, for all package names. */ deleteAllProtos()148 public void deleteAllProtos() { 149 File[] files = mStoreFolder.listFiles(); 150 151 // We ensure that the storeFolder exists in the constructor, but check just in case it has 152 // mysteriously disappeared. 153 if (files == null) { 154 return; 155 } 156 157 for (File file : files) { 158 file.delete(); 159 } 160 } 161 getFileForPackage(String packageName)162 private File getFileForPackage(String packageName) { 163 checkPackageName(packageName); 164 return new File(mStoreFolder, packageName); 165 } 166 checkPackageName(String packageName)167 private static void checkPackageName(String packageName) { 168 if (TextUtils.isEmpty(packageName) || packageName.contains("/")) { 169 throw new IllegalArgumentException( 170 "Package name must not contain '/' or be empty: " + packageName); 171 } 172 } 173 } 174