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

Commit 9ce01190 authored by Al Sutton's avatar Al Sutton
Browse files

Import ChunkListingMap

Bug: 111386661
Test: make RunBackupEncryptionRoboTests
Change-Id: Ieb9f11c18cef02e94ec8b5d849ae1cd005069086
parent e18cb098
Loading
Loading
Loading
Loading
+94 −0
Original line number Diff line number Diff line
/*
 * 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 com.android.server.backup.encryption.chunk;

import android.annotation.Nullable;

import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;

import java.util.HashMap;
import java.util.Map;

/**
 * Chunk listing in a format optimized for quick look up of chunks via their hash keys. This is
 * useful when building an incremental backup. After a chunk has been produced, the algorithm can
 * quickly look up whether the chunk existed in the previous backup by checking this chunk listing.
 * It can then tell the server to use that chunk, through telling it the position and length of the
 * chunk in the previous backup's blob.
 */
public class ChunkListingMap {

    private final Map<ChunkHash, Entry> mChunksByHash;

    /** Construct a map from a {@link ChunksMetadataProto.ChunkListing} protobuf */
    public static ChunkListingMap fromProto(ChunksMetadataProto.ChunkListing chunkListingProto) {
        Map<ChunkHash, Entry> entries = new HashMap<>();

        long start = 0;

        for (ChunksMetadataProto.Chunk chunk : chunkListingProto.chunks) {
            entries.put(new ChunkHash(chunk.hash), new Entry(start, chunk.length));
            start += chunk.length;
        }

        return new ChunkListingMap(entries);
    }

    private ChunkListingMap(Map<ChunkHash, Entry> chunksByHash) {
        // This is only called from the {@link #fromProto} method, so we don't
        // need to take a copy.
        this.mChunksByHash = chunksByHash;
    }

    /** Returns {@code true} if there is a chunk with the given SHA-256 MAC key in the listing. */
    public boolean hasChunk(ChunkHash hash) {
        return mChunksByHash.containsKey(hash);
    }

    /**
     * Returns the entry for the chunk with the given hash.
     *
     * @param hash The SHA-256 MAC of the plaintext of the chunk.
     * @return The entry, containing position and length of the chunk in the backup blob, or null if
     *     it does not exist.
     */
    @Nullable
    public Entry getChunkEntry(ChunkHash hash) {
        return mChunksByHash.get(hash);
    }

    /** Information about a chunk entry in a backup blob - i.e., its position and length. */
    public static final class Entry {
        private final int mLength;
        private final long mStart;

        private Entry(long start, int length) {
            mLength = length;
            mStart = start;
        }

        /** Returns the length of the chunk in bytes. */
        public int getLength() {
            return mLength;
        }

        /** Returns the start position of the chunk in the backup blob, in bytes. */
        public long getStart() {
            return mStart;
        }
    }
}
+114 −0
Original line number Diff line number Diff line
/*
 * 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 com.android.server.backup.encryption.chunk;

import static com.google.common.truth.Truth.assertThat;

import android.platform.test.annotations.Presubmit;

import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;

import com.google.common.base.Charsets;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;

import java.util.Arrays;

@RunWith(RobolectricTestRunner.class)
@Presubmit
public class ChunkListingMapTest {
    private static final ChunkHash CHUNK_A_HASH = getHash("CHUNK_A");
    private static final ChunkHash CHUNK_B_HASH = getHash("CHUNK_B");
    private static final ChunkHash CHUNK_C_HASH = getHash("CHUNK_C");

    private static final int CHUNK_A_LENGTH = 256;
    private static final int CHUNK_B_LENGTH = 1024;
    private static final int CHUNK_C_LENGTH = 4055;

    private static final int CHUNK_A_START = 0;
    private static final int CHUNK_B_START = CHUNK_A_START + CHUNK_A_LENGTH;
    private static final int CHUNK_C_START = CHUNK_B_START + CHUNK_B_LENGTH;

    private ChunkListingMap mChunkListingMap;

    @Before
    public void setUp() {
        mChunkListingMap = createFromFixture();
    }

    @Test
    public void hasChunk_isTrueForExistingChunks() {
        assertThat(mChunkListingMap.hasChunk(CHUNK_A_HASH)).isTrue();
        assertThat(mChunkListingMap.hasChunk(CHUNK_B_HASH)).isTrue();
        assertThat(mChunkListingMap.hasChunk(CHUNK_C_HASH)).isTrue();
    }

    @Test
    public void hasChunk_isFalseForNonexistentChunks() {
        assertThat(mChunkListingMap.hasChunk(getHash("CHUNK_D"))).isFalse();
        assertThat(mChunkListingMap.hasChunk(getHash(""))).isFalse();
    }

    @Test
    public void getChunkListing_hasCorrectLengths() {
        assertThat(mChunkListingMap.getChunkEntry(CHUNK_A_HASH).getLength())
                .isEqualTo(CHUNK_A_LENGTH);
        assertThat(mChunkListingMap.getChunkEntry(CHUNK_B_HASH).getLength())
                .isEqualTo(CHUNK_B_LENGTH);
        assertThat(mChunkListingMap.getChunkEntry(CHUNK_C_HASH).getLength())
                .isEqualTo(CHUNK_C_LENGTH);
    }

    @Test
    public void getChunkListing_hasCorrectStarts() {
        assertThat(mChunkListingMap.getChunkEntry(CHUNK_A_HASH).getStart())
                .isEqualTo(CHUNK_A_START);
        assertThat(mChunkListingMap.getChunkEntry(CHUNK_B_HASH).getStart())
                .isEqualTo(CHUNK_B_START);
        assertThat(mChunkListingMap.getChunkEntry(CHUNK_C_HASH).getStart())
                .isEqualTo(CHUNK_C_START);
    }

    @Test
    public void getChunkListing_isNullForNonExistentChunks() {
        assertThat(mChunkListingMap.getChunkEntry(getHash("Hey"))).isNull();
    }

    private static ChunkListingMap createFromFixture() {
        ChunksMetadataProto.ChunkListing chunkListing = new ChunksMetadataProto.ChunkListing();
        chunkListing.chunks = new ChunksMetadataProto.Chunk[3];
        chunkListing.chunks[0] = newChunk(CHUNK_A_HASH.getHash(), CHUNK_A_LENGTH);
        chunkListing.chunks[1] = newChunk(CHUNK_B_HASH.getHash(), CHUNK_B_LENGTH);
        chunkListing.chunks[2] = newChunk(CHUNK_C_HASH.getHash(), CHUNK_C_LENGTH);
        return ChunkListingMap.fromProto(chunkListing);
    }

    private static ChunkHash getHash(String name) {
        return new ChunkHash(
                Arrays.copyOf(name.getBytes(Charsets.UTF_8), ChunkHash.HASH_LENGTH_BYTES));
    }

    public static ChunksMetadataProto.Chunk newChunk(byte[] hash, int length) {
        ChunksMetadataProto.Chunk newChunk = new ChunksMetadataProto.Chunk();
        newChunk.hash = Arrays.copyOf(hash, hash.length);
        newChunk.length = length;
        return newChunk;
    }
}