Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c8a9d421 authored by Christopher Tate's avatar Christopher Tate
Browse files

Implement key/value quota-overrun detection in LocalTransport

We now preflight the effect of a k/v backup delta set against the
current contents of the datastore, so we can detect and report a
quota overrun nondestructively.

Test: bit CtsBackupTestCases:.KeyValueQuotaTest
Change-Id: If69687b891271d7bbb46c3a66728b716dc51fc39
parent 808b0a01
Loading
Loading
Loading
Loading
+132 −49
Original line number Diff line number Diff line
@@ -30,6 +30,7 @@ import android.os.ParcelFileDescriptor;
import android.system.ErrnoException;
import android.system.Os;
import android.system.StructStat;
import android.util.ArrayMap;
import android.util.Log;

import com.android.org.bouncycastle.util.encoders.Base64;
@@ -44,7 +45,6 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import static android.system.OsConstants.SEEK_CUR;

/**
 * Backup transport for stashing stuff into a known location on disk, and
@@ -70,9 +70,8 @@ public class LocalTransport extends BackupTransport {
    // The currently-active restore set always has the same (nonzero!) token
    private static final long CURRENT_SET_TOKEN = 1;

    // Full backup size quota is set to reasonable value.
    // Size quotas at reasonable values, similar to the current cloud-storage limits
    private static final long FULL_BACKUP_SIZE_QUOTA = 25 * 1024 * 1024;

    private static final long KEY_VALUE_BACKUP_SIZE_QUOTA = 5 * 1024 * 1024;

    private Context mContext;
@@ -157,6 +156,17 @@ public class LocalTransport extends BackupTransport {
        return TRANSPORT_OK;
    }

    // Encapsulation of a single k/v element change
    private class KVOperation {
        final String key;     // Element filename, not the raw key, for efficiency
        final byte[] value;   // null when this is a deletion operation

        KVOperation(String k, byte[] v) {
            key = k;
            value = v;
        }
    }

    @Override
    public int performBackup(PackageInfo packageInfo, ParcelFileDescriptor data) {
        if (DEBUG) {
@@ -175,62 +185,135 @@ public class LocalTransport extends BackupTransport {

        // Each 'record' in the restore set is kept in its own file, named by
        // the record key.  Wind through the data file, extracting individual
        // record operations and building a set of all the updates to apply
        // record operations and building a list of all the updates to apply
        // in this update.
        BackupDataInput changeSet = new BackupDataInput(data.getFileDescriptor());
        final ArrayList<KVOperation> changeOps;
        try {
            int bufSize = 512;
            byte[] buf = new byte[bufSize];
            while (changeSet.readNextHeader()) {
                String key = changeSet.getKey();
                String base64Key = new String(Base64.encode(key.getBytes()));
                File entityFile = new File(packageDir, base64Key);

                int dataSize = changeSet.getDataSize();
            changeOps = parseBackupStream(data);
        } catch (IOException e) {
            // oops, something went wrong.  abort the operation and return error.
            Log.v(TAG, "Exception reading backup input", e);
            return TRANSPORT_ERROR;
        }

                if (DEBUG) Log.v(TAG, "Got change set key=" + key + " size=" + dataSize
                        + " key64=" + base64Key);
        // Okay, now we've parsed out the delta's individual operations.  We need to measure
        // the effect against what we already have in the datastore to detect quota overrun.
        // So, we first need to tally up the current in-datastore size per key.
        final ArrayMap<String, Integer> datastore = new ArrayMap<>();
        int totalSize = parseKeySizes(packageDir, datastore);

                if (dataSize >= 0) {
                    if (entityFile.exists()) {
                        entityFile.delete();
        // ... and now figure out the datastore size that will result from applying the
        // sequence of delta operations
        if (DEBUG) {
            if (changeOps.size() > 0) {
                Log.v(TAG, "Calculating delta size impact");
            } else {
                Log.v(TAG, "No operations in backup stream, so no size change");
            }
                    FileOutputStream entity = new FileOutputStream(entityFile);

                    if (dataSize > bufSize) {
                        bufSize = dataSize;
                        buf = new byte[bufSize];
        }
                    changeSet.readEntityData(buf, 0, dataSize);
        int updatedSize = totalSize;
        for (KVOperation op : changeOps) {
            // Deduct the size of the key we're about to replace, if any
            final Integer curSize = datastore.get(op.key);
            if (curSize != null) {
                updatedSize -= curSize.intValue();
                if (DEBUG && op.value == null) {
                    Log.v(TAG, "  delete " + op.key + ", updated total " + updatedSize);
                }
            }

            // And add back the size of the value we're about to store, if any
            if (op.value != null) {
                updatedSize += op.value.length;
                if (DEBUG) {
                        try {
                            long cur = Os.lseek(data.getFileDescriptor(), 0, SEEK_CUR);
                            Log.v(TAG, "  read entity data; new pos=" + cur);
                    Log.v(TAG, ((curSize == null) ? "  new " : "  replace ")
                            +  op.key + ", updated total " + updatedSize);
                }
                        catch (ErrnoException e) {
                            Log.w(TAG, "Unable to stat input file in performBackup() on "
                                    + packageInfo.packageName);
            }
        }

                    try {
                        entity.write(buf, 0, dataSize);
        // If our final size is over quota, report the failure
        if (updatedSize > KEY_VALUE_BACKUP_SIZE_QUOTA) {
            if (DEBUG) {
                Log.i(TAG, "New datastore size " + updatedSize
                        + " exceeds quota " + KEY_VALUE_BACKUP_SIZE_QUOTA);
            }
            return TRANSPORT_QUOTA_EXCEEDED;
        }

        // No problem with storage size, so go ahead and apply the delta operations
        // (in the order that the app provided them)
        for (KVOperation op : changeOps) {
            File element = new File(packageDir, op.key);

            // this is either a deletion or a rewrite-from-zero, so we can just remove
            // the existing file and proceed in either case.
            element.delete();

            // if this wasn't a deletion, put the new data in place
            if (op.value != null) {
                try (FileOutputStream out = new FileOutputStream(element)) {
                    out.write(op.value, 0, op.value.length);
                } catch (IOException e) {
                        Log.e(TAG, "Unable to update key file " + entityFile.getAbsolutePath());
                    Log.e(TAG, "Unable to update key file " + element);
                    return TRANSPORT_ERROR;
                    } finally {
                        entity.close();
                }
                } else {
                    entityFile.delete();
            }
        }
        return TRANSPORT_OK;
        } catch (IOException e) {
            // oops, something went wrong.  abort the operation and return error.
            Log.v(TAG, "Exception reading backup input:", e);
            return TRANSPORT_ERROR;
    }

    // Parses a backup stream into individual key/value operations
    private ArrayList<KVOperation> parseBackupStream(ParcelFileDescriptor data)
            throws IOException {
        ArrayList<KVOperation> changeOps = new ArrayList<>();
        BackupDataInput changeSet = new BackupDataInput(data.getFileDescriptor());
        while (changeSet.readNextHeader()) {
            String key = changeSet.getKey();
            String base64Key = new String(Base64.encode(key.getBytes()));
            int dataSize = changeSet.getDataSize();
            if (DEBUG) {
                Log.v(TAG, "  Delta operation key " + key + "   size " + dataSize
                        + "   key64 " + base64Key);
            }

            byte[] buf = (dataSize >= 0) ? new byte[dataSize] : null;
            if (dataSize >= 0) {
                changeSet.readEntityData(buf, 0, dataSize);
            }
            changeOps.add(new KVOperation(base64Key, buf));
        }
        return changeOps;
    }

    // Reads the given datastore directory, building a table of the value size of each
    // keyed element, and returning the summed total.
    private int parseKeySizes(File packageDir, ArrayMap<String, Integer> datastore) {
        int totalSize = 0;
        final String[] elements = packageDir.list();
        if (elements != null) {
            if (DEBUG) {
                Log.v(TAG, "Existing datastore contents:");
            }
            for (String file : elements) {
                File element = new File(packageDir, file);
                String key = file;  // filename
                int size = (int) element.length();
                totalSize += size;
                if (DEBUG) {
                    Log.v(TAG, "  key " + key + "   size " + size);
                }
                datastore.put(key, size);
            }
            if (DEBUG) {
                Log.v(TAG, "  TOTAL: " + totalSize);
            }
        } else {
            if (DEBUG) {
                Log.v(TAG, "No existing data for this package");
            }
        }
        return totalSize;
    }

    // Deletes the contents but not the given directory