blob: 70ebbaa326b819f755a7295fd1ce3850965cf691 [file] [log] [blame]
/*
* 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 android.os.incremental;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.RemoteException;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.UUID;
/**
* Provides operations on an Incremental File System directory, using IncrementalServiceNative.
* Example usage:
*
* <blockquote><pre>
* IncrementalManager manager = (IncrementalManager) getSystemService(Context.INCREMENTAL_SERVICE);
* IncrementalStorage storage = manager.openStorage("/path/to/incremental/dir");
* storage.makeDirectory("subdir");
* </pre></blockquote>
*
* @hide
*/
public final class IncrementalStorage {
private static final String TAG = "IncrementalStorage";
private final int mId;
private final IIncrementalService mService;
public IncrementalStorage(@NonNull IIncrementalService is, int id) {
mService = is;
mId = id;
}
public int getId() {
return mId;
}
/**
* Temporarily bind-mounts the current storage directory to a target directory. The bind-mount
* will NOT be preserved between device reboots.
*
* @param targetPath Absolute path to the target directory.
*/
public void bind(@NonNull String targetPath) throws IOException {
bind("", targetPath);
}
/**
* Temporarily bind-mounts a subdir under the current storage directory to a target directory.
* The bind-mount will NOT be preserved between device reboots.
*
* @param sourcePath Source path as a relative path under current storage
* directory.
* @param targetPath Absolute path to the target directory.
*/
public void bind(@NonNull String sourcePath, @NonNull String targetPath)
throws IOException {
try {
int res = mService.makeBindMount(mId, sourcePath, targetPath,
IIncrementalService.BIND_TEMPORARY);
if (res < 0) {
throw new IOException("bind() failed with errno " + -res);
}
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
}
/**
* Permanently bind-mounts the current storage directory to a target directory. The bind-mount
* WILL be preserved between device reboots.
*
* @param targetPath Absolute path to the target directory.
*/
public void bindPermanent(@NonNull String targetPath) throws IOException {
bindPermanent("", targetPath);
}
/**
* Permanently bind-mounts a subdir under the current storage directory to a target directory.
* The bind-mount WILL be preserved between device reboots.
*
* @param sourcePath Relative path under the current storage directory.
* @param targetPath Absolute path to the target directory.
*/
public void bindPermanent(@NonNull String sourcePath, @NonNull String targetPath)
throws IOException {
try {
int res = mService.makeBindMount(mId, sourcePath, targetPath,
IIncrementalService.BIND_PERMANENT);
if (res < 0) {
throw new IOException("bind() permanent failed with errno " + -res);
}
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
}
/**
* Unbinds a bind mount.
*
* @param targetPath Absolute path to the target directory.
*/
public void unBind(@NonNull String targetPath) throws IOException {
try {
int res = mService.deleteBindMount(mId, targetPath);
if (res < 0) {
throw new IOException("unbind() failed with errno " + -res);
}
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
}
/**
* Creates a sub-directory under the current storage directory.
*
* @param path Relative path of the sub-directory, e.g., "subdir"
*/
public void makeDirectory(@NonNull String path) throws IOException {
try {
int res = mService.makeDirectory(mId, path);
if (res < 0) {
throw new IOException("makeDirectory() failed with errno " + -res);
}
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
}
/**
* Creates a sub-directory under the current storage directory. If its parent dirs do not exist,
* create the parent dirs as well.
*
* @param path Full path.
*/
public void makeDirectories(@NonNull String path) throws IOException {
try {
int res = mService.makeDirectories(mId, path);
if (res < 0) {
throw new IOException("makeDirectory() failed with errno " + -res);
}
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
}
/**
* Creates a file under the current storage directory.
*
* @param path Relative path of the new file.
* @param size Size of the new file in bytes.
* @param metadata Metadata bytes.
* @param v4signatureBytes Serialized V4SignatureProto.
*/
public void makeFile(@NonNull String path, long size, @Nullable UUID id,
@Nullable byte[] metadata, @Nullable byte[] v4signatureBytes)
throws IOException {
try {
if (id == null && metadata == null) {
throw new IOException("File ID and metadata cannot both be null");
}
validateV4Signature(v4signatureBytes);
final IncrementalNewFileParams params = new IncrementalNewFileParams();
params.size = size;
params.metadata = (metadata == null ? new byte[0] : metadata);
params.fileId = idToBytes(id);
params.signature = v4signatureBytes;
int res = mService.makeFile(mId, path, params);
if (res != 0) {
throw new IOException("makeFile() failed with errno " + -res);
}
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
}
/**
* Creates a file in Incremental storage. The content of the file is mapped from a range inside
* a source file in the same storage.
*
* @param destPath Target full path.
* @param sourcePath Source full path.
* @param rangeStart Starting offset (in bytes) in the source file.
* @param rangeEnd Ending offset (in bytes) in the source file.
*/
public void makeFileFromRange(@NonNull String destPath,
@NonNull String sourcePath, long rangeStart, long rangeEnd) throws IOException {
try {
int res = mService.makeFileFromRange(mId, destPath, sourcePath,
rangeStart, rangeEnd);
if (res < 0) {
throw new IOException("makeFileFromRange() failed, errno " + -res);
}
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
}
/**
* Creates a hard-link between two paths, which can be under different storages but in the same
* Incremental File System.
*
* @param sourcePath The absolute path of the source.
* @param destStorage The target storage of the link target.
* @param destPath The absolute path of the target.
*/
public void makeLink(@NonNull String sourcePath, IncrementalStorage destStorage,
@NonNull String destPath) throws IOException {
try {
int res = mService.makeLink(mId, sourcePath, destStorage.getId(),
destPath);
if (res < 0) {
throw new IOException("makeLink() failed with errno " + -res);
}
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
}
/**
* Deletes a hard-link under the current storage directory.
*
* @param path The absolute path of the target.
*/
public void unlink(@NonNull String path) throws IOException {
try {
int res = mService.unlink(mId, path);
if (res < 0) {
throw new IOException("unlink() failed with errno " + -res);
}
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
}
/**
* Rename an old file name to a new file name under the current storage directory.
*
* @param sourcepath Old file path as a full path to the storage directory.
* @param destpath New file path as a full path to the storage directory.
*/
public void moveFile(@NonNull String sourcepath,
@NonNull String destpath) throws IOException {
//TODO(zyy): implement using rename(2) when confirmed that IncFS supports it.
try {
int res = mService.makeLink(mId, sourcepath, mId, destpath);
if (res < 0) {
throw new IOException("moveFile() failed at makeLink(), errno " + -res);
}
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
try {
mService.unlink(mId, sourcepath);
} catch (RemoteException ignored) {
}
}
/**
* Move a directory, which is bind-mounted to a given storage, to a new location. The bind mount
* will be persistent between reboots.
*
* @param sourcePath The old path of the directory as an absolute path.
* @param destPath The new path of the directory as an absolute path, expected to already
* exist.
*/
public void moveDir(@NonNull String sourcePath, @NonNull String destPath) throws IOException {
if (!new File(destPath).exists()) {
throw new IOException("moveDir() requires that destination dir already exists.");
}
try {
int res = mService.makeBindMount(mId, sourcePath, destPath,
IIncrementalService.BIND_PERMANENT);
if (res < 0) {
throw new IOException("moveDir() failed at making bind mount, errno " + -res);
}
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
try {
mService.deleteBindMount(mId, sourcePath);
} catch (RemoteException ignored) {
}
}
/**
* Checks whether a file under the current storage directory is fully loaded.
*
* @param path The relative path of the file.
* @return True if the file is fully loaded.
*/
public boolean isFileFullyLoaded(@NonNull String path) {
return isFileRangeLoaded(path, 0, -1);
}
/**
* Checks whether a range in a file if loaded.
*
* @param path The relative path of the file.
* @param start The starting offset of the range.
* @param end The ending offset of the range.
* @return True if the file is fully loaded.
*/
public boolean isFileRangeLoaded(@NonNull String path, long start, long end) {
try {
return mService.isFileRangeLoaded(mId, path, start, end);
} catch (RemoteException e) {
e.rethrowFromSystemServer();
return false;
}
}
/**
* Returns the metadata object of an IncFs File.
*
* @param path The relative path of the file.
* @return Byte array that contains metadata bytes.
*/
@Nullable
public byte[] getFileMetadata(@NonNull String path) {
try {
return mService.getMetadataByPath(mId, path);
} catch (RemoteException e) {
e.rethrowFromSystemServer();
return null;
}
}
/**
* Returns the metadata object of an IncFs File.
*
* @param id The file id.
* @return Byte array that contains metadata bytes.
*/
@Nullable
public byte[] getFileMetadata(@NonNull UUID id) {
try {
final byte[] rawId = idToBytes(id);
return mService.getMetadataById(mId, rawId);
} catch (RemoteException e) {
e.rethrowFromSystemServer();
return null;
}
}
/**
* Informs the data loader service associated with the current storage to start data loader
*
* @return True if data loader is successfully started.
*/
public boolean startLoading() {
try {
return mService.startLoading(mId);
} catch (RemoteException e) {
e.rethrowFromSystemServer();
return false;
}
}
private static final int UUID_BYTE_SIZE = 16;
/**
* Converts UUID to a byte array usable for Incremental API calls
*
* @param id The id to convert
* @return Byte array that contains the same ID.
*/
@NonNull
public static byte[] idToBytes(@Nullable UUID id) {
if (id == null) {
return new byte[0];
}
final ByteBuffer buf = ByteBuffer.wrap(new byte[UUID_BYTE_SIZE]);
buf.putLong(id.getMostSignificantBits());
buf.putLong(id.getLeastSignificantBits());
return buf.array();
}
/**
* Converts UUID from a byte array usable for Incremental API calls
*
* @param bytes The id in byte array format, 16 bytes long
* @return UUID constructed from the byte array.
*/
@NonNull
public static UUID bytesToId(byte[] bytes) throws IllegalArgumentException {
if (bytes.length != UUID_BYTE_SIZE) {
throw new IllegalArgumentException("Expected array of size " + UUID_BYTE_SIZE
+ ", got " + bytes.length);
}
final ByteBuffer buf = ByteBuffer.wrap(bytes);
long msb = buf.getLong();
long lsb = buf.getLong();
return new UUID(msb, lsb);
}
private static final int INCFS_MAX_HASH_SIZE = 32; // SHA256
private static final int INCFS_MAX_ADD_DATA_SIZE = 128;
/**
* Deserialize and validate v4 signature bytes.
*/
private static void validateV4Signature(@Nullable byte[] v4signatureBytes)
throws IOException {
if (v4signatureBytes == null || v4signatureBytes.length == 0) {
return;
}
final V4Signature signature;
try {
signature = V4Signature.readFrom(v4signatureBytes);
} catch (IOException e) {
throw new IOException("Failed to read v4 signature:", e);
}
if (!signature.isVersionSupported()) {
throw new IOException("v4 signature version " + signature.version
+ " is not supported");
}
final V4Signature.HashingInfo hashingInfo = V4Signature.HashingInfo.fromByteArray(
signature.hashingInfo);
final V4Signature.SigningInfo signingInfo = V4Signature.SigningInfo.fromByteArray(
signature.signingInfo);
if (hashingInfo.hashAlgorithm != V4Signature.HASHING_ALGORITHM_SHA256) {
throw new IOException("Unsupported hashAlgorithm: " + hashingInfo.hashAlgorithm);
}
if (hashingInfo.log2BlockSize != V4Signature.LOG2_BLOCK_SIZE_4096_BYTES) {
throw new IOException("Unsupported log2BlockSize: " + hashingInfo.log2BlockSize);
}
if (hashingInfo.salt != null && hashingInfo.salt.length > 0) {
throw new IOException("Unsupported salt: " + hashingInfo.salt);
}
if (hashingInfo.rawRootHash.length != INCFS_MAX_HASH_SIZE) {
throw new IOException("rawRootHash has to be " + INCFS_MAX_HASH_SIZE + " bytes");
}
if (signingInfo.additionalData.length > INCFS_MAX_ADD_DATA_SIZE) {
throw new IOException(
"additionalData has to be at most " + INCFS_MAX_ADD_DATA_SIZE + " bytes");
}
}
/**
* Configure all the lib files inside Incremental Service, e.g., create lib dirs, create new lib
* files, extract original lib file data from zip and then write data to the lib files on the
* Incremental File System.
*
* @param apkFullPath Source APK to extract native libs from.
* @param libDirRelativePath Target dir to put lib files, e.g., "lib" or "lib/arm".
* @param abi Target ABI of the native lib files. Only extract native libs of this ABI.
* @return Success of not.
*/
public boolean configureNativeBinaries(String apkFullPath, String libDirRelativePath,
String abi) {
try {
return mService.configureNativeBinaries(mId, apkFullPath, libDirRelativePath, abi);
} catch (RemoteException e) {
e.rethrowFromSystemServer();
return false;
}
}
/**
* Waits for all native binary extraction operations to complete on the storage.
*
* @return Success of not.
*/
public boolean waitForNativeBinariesExtraction() {
try {
return mService.waitForNativeBinariesExtraction(mId);
} catch (RemoteException e) {
e.rethrowFromSystemServer();
return false;
}
}
}