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