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

Commit f30d3536 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Import ChunkListingMap"

parents 0daaaaf3 9ce01190
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;
    }
}