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 android.os.incremental;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.content.pm.DataLoaderParams;
22 import android.content.pm.IDataLoaderStatusListener;
23 import android.os.PersistableBundle;
24 import android.os.RemoteException;
25 
26 import java.io.File;
27 import java.io.IOException;
28 import java.nio.ByteBuffer;
29 import java.util.Objects;
30 import java.util.UUID;
31 
32 /**
33  * Provides operations on an Incremental File System directory, using IncrementalServiceNative.
34  * Example usage:
35  *
36  * <blockquote><pre>
37  * IncrementalManager manager = (IncrementalManager) getSystemService(Context.INCREMENTAL_SERVICE);
38  * IncrementalStorage storage = manager.openStorage("/path/to/incremental/dir");
39  * storage.makeDirectory("subdir");
40  * </pre></blockquote>
41  *
42  * @hide
43  */
44 public final class IncrementalStorage {
45     private static final String TAG = "IncrementalStorage";
46     private final int mId;
47     private final IIncrementalService mService;
48 
49 
IncrementalStorage(@onNull IIncrementalService is, int id)50     public IncrementalStorage(@NonNull IIncrementalService is, int id) {
51         mService = is;
52         mId = id;
53     }
54 
getId()55     public int getId() {
56         return mId;
57     }
58 
59     /**
60      * Temporarily bind-mounts the current storage directory to a target directory. The bind-mount
61      * will NOT be preserved between device reboots.
62      *
63      * @param targetPath Absolute path to the target directory.
64      */
bind(@onNull String targetPath)65     public void bind(@NonNull String targetPath) throws IOException {
66         bind("", targetPath);
67     }
68 
69     /**
70      * Temporarily bind-mounts a subdir under the current storage directory to a target directory.
71      * The bind-mount will NOT be preserved between device reboots.
72      *
73      * @param sourcePath Source path as a relative path under current storage
74      *                   directory.
75      * @param targetPath Absolute path to the target directory.
76      */
bind(@onNull String sourcePath, @NonNull String targetPath)77     public void bind(@NonNull String sourcePath, @NonNull String targetPath)
78             throws IOException {
79         try {
80             int res = mService.makeBindMount(mId, sourcePath, targetPath,
81                     IIncrementalService.BIND_TEMPORARY);
82             if (res < 0) {
83                 throw new IOException("bind() failed with errno " + -res);
84             }
85         } catch (RemoteException e) {
86             e.rethrowFromSystemServer();
87         }
88     }
89 
90 
91     /**
92      * Permanently bind-mounts the current storage directory to a target directory. The bind-mount
93      * WILL be preserved between device reboots.
94      *
95      * @param targetPath Absolute path to the target directory.
96      */
bindPermanent(@onNull String targetPath)97     public void bindPermanent(@NonNull String targetPath) throws IOException {
98         bindPermanent("", targetPath);
99     }
100 
101     /**
102      * Permanently bind-mounts a subdir under the current storage directory to a target directory.
103      * The bind-mount WILL be preserved between device reboots.
104      *
105      * @param sourcePath Relative path under the current storage directory.
106      * @param targetPath Absolute path to the target directory.
107      */
bindPermanent(@onNull String sourcePath, @NonNull String targetPath)108     public void bindPermanent(@NonNull String sourcePath, @NonNull String targetPath)
109             throws IOException {
110         try {
111             int res = mService.makeBindMount(mId, sourcePath, targetPath,
112                     IIncrementalService.BIND_PERMANENT);
113             if (res < 0) {
114                 throw new IOException("bind() permanent failed with errno " + -res);
115             }
116         } catch (RemoteException e) {
117             e.rethrowFromSystemServer();
118         }
119     }
120 
121     /**
122      * Unbinds a bind mount.
123      *
124      * @param targetPath Absolute path to the target directory.
125      */
unBind(@onNull String targetPath)126     public void unBind(@NonNull String targetPath) throws IOException {
127         try {
128             int res = mService.deleteBindMount(mId, targetPath);
129             if (res < 0) {
130                 throw new IOException("unbind() failed with errno " + -res);
131             }
132         } catch (RemoteException e) {
133             e.rethrowFromSystemServer();
134         }
135     }
136 
137     /**
138      * Creates a sub-directory under the current storage directory.
139      *
140      * @param path Relative path of the sub-directory, e.g., "subdir"
141      */
makeDirectory(@onNull String path)142     public void makeDirectory(@NonNull String path) throws IOException {
143         try {
144             int res = mService.makeDirectory(mId, path);
145             if (res < 0) {
146                 throw new IOException("makeDirectory() failed with errno " + -res);
147             }
148         } catch (RemoteException e) {
149             e.rethrowFromSystemServer();
150         }
151     }
152 
153     /**
154      * Creates a sub-directory under the current storage directory. If its parent dirs do not exist,
155      * create the parent dirs as well.
156      *
157      * @param path Full path.
158      */
makeDirectories(@onNull String path)159     public void makeDirectories(@NonNull String path) throws IOException {
160         try {
161             int res = mService.makeDirectories(mId, path);
162             if (res < 0) {
163                 throw new IOException("makeDirectory() failed with errno " + -res);
164             }
165         } catch (RemoteException e) {
166             e.rethrowFromSystemServer();
167         }
168     }
169 
170     /**
171      * Creates a file under the current storage directory.
172      *
173      * @param path             Relative path of the new file.
174      * @param size             Size of the new file in bytes.
175      * @param metadata         Metadata bytes.
176      * @param v4signatureBytes Serialized V4SignatureProto.
177      * @param content          Optionally set file content.
178      */
makeFile(@onNull String path, long size, @Nullable UUID id, @Nullable byte[] metadata, @Nullable byte[] v4signatureBytes, @Nullable byte[] content)179     public void makeFile(@NonNull String path, long size, @Nullable UUID id,
180             @Nullable byte[] metadata, @Nullable byte[] v4signatureBytes, @Nullable byte[] content)
181             throws IOException {
182         try {
183             if (id == null && metadata == null) {
184                 throw new IOException("File ID and metadata cannot both be null");
185             }
186             validateV4Signature(v4signatureBytes);
187             final IncrementalNewFileParams params = new IncrementalNewFileParams();
188             params.size = size;
189             params.metadata = (metadata == null ? new byte[0] : metadata);
190             params.fileId = idToBytes(id);
191             params.signature = v4signatureBytes;
192             int res = mService.makeFile(mId, path, params, content);
193             if (res != 0) {
194                 throw new IOException("makeFile() failed with errno " + -res);
195             }
196         } catch (RemoteException e) {
197             e.rethrowFromSystemServer();
198         }
199     }
200 
201 
202     /**
203      * Creates a file in Incremental storage. The content of the file is mapped from a range inside
204      * a source file in the same storage.
205      *
206      * @param destPath           Target full path.
207      * @param sourcePath         Source full path.
208      * @param rangeStart         Starting offset (in bytes) in the source file.
209      * @param rangeEnd           Ending offset (in bytes) in the source file.
210      */
makeFileFromRange(@onNull String destPath, @NonNull String sourcePath, long rangeStart, long rangeEnd)211     public void makeFileFromRange(@NonNull String destPath,
212             @NonNull String sourcePath, long rangeStart, long rangeEnd) throws IOException {
213         try {
214             int res = mService.makeFileFromRange(mId, destPath, sourcePath,
215                     rangeStart, rangeEnd);
216             if (res < 0) {
217                 throw new IOException("makeFileFromRange() failed, errno " + -res);
218             }
219         } catch (RemoteException e) {
220             e.rethrowFromSystemServer();
221         }
222     }
223 
224     /**
225      * Creates a hard-link between two paths, which can be under different storages but in the same
226      * Incremental File System.
227      *
228      * @param sourcePath    The absolute path of the source.
229      * @param destStorage   The target storage of the link target.
230      * @param destPath      The absolute path of the target.
231      */
makeLink(@onNull String sourcePath, IncrementalStorage destStorage, @NonNull String destPath)232     public void makeLink(@NonNull String sourcePath, IncrementalStorage destStorage,
233             @NonNull String destPath) throws IOException {
234         try {
235             int res = mService.makeLink(mId, sourcePath, destStorage.getId(),
236                     destPath);
237             if (res < 0) {
238                 throw new IOException("makeLink() failed with errno " + -res);
239             }
240         } catch (RemoteException e) {
241             e.rethrowFromSystemServer();
242         }
243     }
244 
245     /**
246      * Deletes a hard-link under the current storage directory.
247      *
248      * @param path The absolute path of the target.
249      */
unlink(@onNull String path)250     public void unlink(@NonNull String path) throws IOException {
251         try {
252             int res = mService.unlink(mId, path);
253             if (res < 0) {
254                 throw new IOException("unlink() failed with errno " + -res);
255             }
256         } catch (RemoteException e) {
257             e.rethrowFromSystemServer();
258         }
259     }
260 
261     /**
262      * Rename an old file name to a new file name under the current storage directory.
263      *
264      * @param sourcepath Old file path as a full path to the storage directory.
265      * @param destpath   New file path as a full path to the storage directory.
266      */
moveFile(@onNull String sourcepath, @NonNull String destpath)267     public void moveFile(@NonNull String sourcepath,
268             @NonNull String destpath) throws IOException {
269         //TODO(zyy): implement using rename(2) when confirmed that IncFS supports it.
270         try {
271             int res = mService.makeLink(mId, sourcepath, mId, destpath);
272             if (res < 0) {
273                 throw new IOException("moveFile() failed at makeLink(), errno " + -res);
274             }
275         } catch (RemoteException e) {
276             e.rethrowFromSystemServer();
277         }
278         try {
279             mService.unlink(mId, sourcepath);
280         } catch (RemoteException ignored) {
281         }
282     }
283 
284     /**
285      * Move a directory, which is bind-mounted to a given storage, to a new location. The bind mount
286      * will be persistent between reboots.
287      *
288      * @param sourcePath The old path of the directory as an absolute path.
289      * @param destPath   The new path of the directory as an absolute path, expected to already
290      *                   exist.
291      */
moveDir(@onNull String sourcePath, @NonNull String destPath)292     public void moveDir(@NonNull String sourcePath, @NonNull String destPath) throws IOException {
293         if (!new File(destPath).exists()) {
294             throw new IOException("moveDir() requires that destination dir already exists.");
295         }
296         try {
297             int res = mService.makeBindMount(mId, sourcePath, destPath,
298                     IIncrementalService.BIND_PERMANENT);
299             if (res < 0) {
300                 throw new IOException("moveDir() failed at making bind mount, errno " + -res);
301             }
302         } catch (RemoteException e) {
303             e.rethrowFromSystemServer();
304         }
305         try {
306             mService.deleteBindMount(mId, sourcePath);
307         } catch (RemoteException ignored) {
308         }
309     }
310 
311     /**
312      * Checks whether a file under the current storage directory is fully loaded.
313      *
314      * @param path The relative path of the file.
315      * @return True if the file is fully loaded.
316      */
isFileFullyLoaded(@onNull String path)317     public boolean isFileFullyLoaded(@NonNull String path) throws IOException {
318         try {
319             int res = mService.isFileFullyLoaded(mId, path);
320             if (res < 0) {
321                 throw new IOException("isFileFullyLoaded() failed, errno " + -res);
322             }
323             return res == 0;
324         } catch (RemoteException e) {
325             e.rethrowFromSystemServer();
326             return false;
327         }
328     }
329 
330 
331     /**
332      * Checks if all files in the storage are fully loaded.
333      */
isFullyLoaded()334     public boolean isFullyLoaded() throws IOException {
335         try {
336             final int res = mService.isFullyLoaded(mId);
337             if (res < 0) {
338                 throw new IOException(
339                         "isFullyLoaded() failed at querying loading progress, errno " + -res);
340             }
341             return res == 0;
342         } catch (RemoteException e) {
343             e.rethrowFromSystemServer();
344             return false;
345         }
346     }
347 
348     /**
349      * Returns the loading progress of a storage
350      *
351      * @return progress value between [0, 1].
352      */
getLoadingProgress()353     public float getLoadingProgress() throws IOException {
354         try {
355             final float res = mService.getLoadingProgress(mId);
356             if (res < 0) {
357                 throw new IOException(
358                         "getLoadingProgress() failed at querying loading progress, errno " + -res);
359             }
360             return res;
361         } catch (RemoteException e) {
362             e.rethrowFromSystemServer();
363             return 0;
364         }
365     }
366 
367     /**
368      * Returns the metadata object of an IncFs File.
369      *
370      * @param path The relative path of the file.
371      * @return Byte array that contains metadata bytes.
372      */
373     @Nullable
getFileMetadata(@onNull String path)374     public byte[] getFileMetadata(@NonNull String path) {
375         try {
376             return mService.getMetadataByPath(mId, path);
377         } catch (RemoteException e) {
378             e.rethrowFromSystemServer();
379             return null;
380         }
381     }
382 
383     /**
384      * Returns the metadata object of an IncFs File.
385      *
386      * @param id The file id.
387      * @return Byte array that contains metadata bytes.
388      */
389     @Nullable
getFileMetadata(@onNull UUID id)390     public byte[] getFileMetadata(@NonNull UUID id) {
391         try {
392             final byte[] rawId = idToBytes(id);
393             return mService.getMetadataById(mId, rawId);
394         } catch (RemoteException e) {
395             e.rethrowFromSystemServer();
396             return null;
397         }
398     }
399 
400     /**
401      * Initializes and starts the DataLoader.
402      * This makes sure all install-time parameters are applied.
403      * Does not affect persistent DataLoader params.
404      * @return True if start request was successfully queued.
405      */
startLoading( @onNull DataLoaderParams dataLoaderParams, @Nullable IDataLoaderStatusListener statusListener, @Nullable StorageHealthCheckParams healthCheckParams, @Nullable IStorageHealthListener healthListener, @NonNull PerUidReadTimeouts[] perUidReadTimeouts)406     public boolean startLoading(
407             @NonNull DataLoaderParams dataLoaderParams,
408             @Nullable IDataLoaderStatusListener statusListener,
409             @Nullable StorageHealthCheckParams healthCheckParams,
410             @Nullable IStorageHealthListener healthListener,
411             @NonNull PerUidReadTimeouts[] perUidReadTimeouts) {
412         Objects.requireNonNull(perUidReadTimeouts);
413         try {
414             return mService.startLoading(mId, dataLoaderParams.getData(), statusListener,
415                     healthCheckParams, healthListener, perUidReadTimeouts);
416         } catch (RemoteException e) {
417             e.rethrowFromSystemServer();
418             return false;
419         }
420     }
421 
422     /**
423      * Marks the completion of installation.
424      */
onInstallationComplete()425     public void onInstallationComplete() {
426         try {
427             mService.onInstallationComplete(mId);
428         } catch (RemoteException e) {
429             e.rethrowFromSystemServer();
430         }
431     }
432 
433 
434     private static final int UUID_BYTE_SIZE = 16;
435 
436     /**
437      * Converts UUID to a byte array usable for Incremental API calls
438      *
439      * @param id The id to convert
440      * @return Byte array that contains the same ID.
441      */
442     @NonNull
idToBytes(@ullable UUID id)443     public static byte[] idToBytes(@Nullable UUID id) {
444         if (id == null) {
445             return new byte[0];
446         }
447         final ByteBuffer buf = ByteBuffer.wrap(new byte[UUID_BYTE_SIZE]);
448         buf.putLong(id.getMostSignificantBits());
449         buf.putLong(id.getLeastSignificantBits());
450         return buf.array();
451     }
452 
453     /**
454      * Converts UUID from a byte array usable for Incremental API calls
455      *
456      * @param bytes The id in byte array format, 16 bytes long
457      * @return UUID constructed from the byte array.
458      */
459     @NonNull
bytesToId(byte[] bytes)460     public static UUID bytesToId(byte[] bytes) throws IllegalArgumentException {
461         if (bytes.length != UUID_BYTE_SIZE) {
462             throw new IllegalArgumentException("Expected array of size " + UUID_BYTE_SIZE
463                                                + ", got " + bytes.length);
464         }
465         final ByteBuffer buf = ByteBuffer.wrap(bytes);
466         long msb = buf.getLong();
467         long lsb = buf.getLong();
468         return new UUID(msb, lsb);
469     }
470 
471     private static final int INCFS_MAX_HASH_SIZE = 32; // SHA256
472     private static final int INCFS_MAX_ADD_DATA_SIZE = 128;
473 
474     /**
475      * Permanently disable readlogs collection.
476      */
disallowReadLogs()477     public void disallowReadLogs() {
478         try {
479             mService.disallowReadLogs(mId);
480         } catch (RemoteException e) {
481             e.rethrowFromSystemServer();
482         }
483     }
484 
485     /**
486      * Deserialize and validate v4 signature bytes.
487      */
validateV4Signature(@ullable byte[] v4signatureBytes)488     private static void validateV4Signature(@Nullable byte[] v4signatureBytes)
489             throws IOException {
490         if (v4signatureBytes == null || v4signatureBytes.length == 0) {
491             return;
492         }
493 
494         final V4Signature signature;
495         try {
496             signature = V4Signature.readFrom(v4signatureBytes);
497         } catch (IOException e) {
498             throw new IOException("Failed to read v4 signature:", e);
499         }
500 
501         if (!signature.isVersionSupported()) {
502             throw new IOException("v4 signature version " + signature.version
503                     + " is not supported");
504         }
505 
506         final V4Signature.HashingInfo hashingInfo = V4Signature.HashingInfo.fromByteArray(
507                 signature.hashingInfo);
508         final V4Signature.SigningInfo signingInfo = V4Signature.SigningInfo.fromByteArray(
509                 signature.signingInfo);
510 
511         if (hashingInfo.hashAlgorithm != V4Signature.HASHING_ALGORITHM_SHA256) {
512             throw new IOException("Unsupported hashAlgorithm: " + hashingInfo.hashAlgorithm);
513         }
514         if (hashingInfo.log2BlockSize != V4Signature.LOG2_BLOCK_SIZE_4096_BYTES) {
515             throw new IOException("Unsupported log2BlockSize: " + hashingInfo.log2BlockSize);
516         }
517         if (hashingInfo.salt != null && hashingInfo.salt.length > 0) {
518             throw new IOException("Unsupported salt: " + hashingInfo.salt);
519         }
520         if (hashingInfo.rawRootHash.length != INCFS_MAX_HASH_SIZE) {
521             throw new IOException("rawRootHash has to be " + INCFS_MAX_HASH_SIZE + " bytes");
522         }
523         if (signingInfo.additionalData.length > INCFS_MAX_ADD_DATA_SIZE) {
524             throw new IOException(
525                     "additionalData has to be at most " + INCFS_MAX_ADD_DATA_SIZE + " bytes");
526         }
527     }
528 
529     /**
530      * Configure all the lib files inside Incremental Service, e.g., create lib dirs, create new lib
531      * files, extract original lib file data from zip and then write data to the lib files on the
532      * Incremental File System.
533      *
534      * @param apkFullPath Source APK to extract native libs from.
535      * @param libDirRelativePath Target dir to put lib files, e.g., "lib" or "lib/arm".
536      * @param abi Target ABI of the native lib files. Only extract native libs of this ABI.
537      * @param extractNativeLibs If true, extract native libraries; otherwise just setup directories
538      *                          without extracting.
539      * @return Success of not.
540      */
configureNativeBinaries(String apkFullPath, String libDirRelativePath, String abi, boolean extractNativeLibs)541     public boolean configureNativeBinaries(String apkFullPath, String libDirRelativePath,
542             String abi, boolean extractNativeLibs) {
543         try {
544             return mService.configureNativeBinaries(mId, apkFullPath, libDirRelativePath, abi,
545                     extractNativeLibs);
546         } catch (RemoteException e) {
547             e.rethrowFromSystemServer();
548             return false;
549         }
550     }
551 
552     /**
553      * Waits for all native binary extraction operations to complete on the storage.
554      *
555      * @return Success of not.
556      */
waitForNativeBinariesExtraction()557     public boolean waitForNativeBinariesExtraction() {
558         try {
559             return mService.waitForNativeBinariesExtraction(mId);
560         } catch (RemoteException e) {
561             e.rethrowFromSystemServer();
562             return false;
563         }
564     }
565 
566     /**
567      * Register to listen to loading progress of all the files on this storage.
568      * @param listener To report progress from Incremental Service to the caller.
569      */
registerLoadingProgressListener(IStorageLoadingProgressListener listener)570     public boolean registerLoadingProgressListener(IStorageLoadingProgressListener listener) {
571         try {
572             return mService.registerLoadingProgressListener(mId, listener);
573         } catch (RemoteException e) {
574             e.rethrowFromSystemServer();
575             return false;
576         }
577     }
578 
579     /**
580      * Unregister to stop listening to storage loading progress.
581      */
unregisterLoadingProgressListener()582     public boolean unregisterLoadingProgressListener() {
583         try {
584             return mService.unregisterLoadingProgressListener(mId);
585         } catch (RemoteException e) {
586             e.rethrowFromSystemServer();
587             return false;
588         }
589     }
590 
591     /**
592      * Returns the metrics of the current storage.
593      * {@see IIncrementalService} for metrics keys.
594      */
getMetrics()595     public PersistableBundle getMetrics() {
596         try {
597             return mService.getMetrics(mId);
598         } catch (RemoteException e) {
599             e.rethrowFromSystemServer();
600             return null;
601         }
602     }
603 }
604