• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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