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

Commit 02cf411d authored by Bram Bonné's avatar Bram Bonné
Browse files

Ports DiffScriptBackupWriter from gmscore to AOSP.

A few additional changes (apart from style and usual dependencies) were
needed:
- Additional dependencies (not part of Backup & Restore code) were
ported over:
	- ByteRange
	- DiffScriptWriter
	- OutputStreamWrapper
	- SingleStreamDiffScriptWriter

- DiffScripBackupWriter.ENCRYPTION_DIFF_SCRIPT_MAX_CHUNK_SIZE_BYTES is
now a constant rather than a flag.
- Additional tests were added for SingleStreamDiffScriptWriter.

Bug: 111386661
Test: atest RunBackupFrameworksServicesRoboTests
Change-Id: Ia3234bb8d665211e6fa91d6a92d190171b0d2dc1
parent 01c1c07c
Loading
Loading
Loading
Loading
+80 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2018 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.chunking;

import com.android.internal.util.Preconditions;

/** Representation of a range of bytes to be downloaded. */
final class ByteRange {
    private final long mStart;
    private final long mEnd;

    /** Creates a range of bytes which includes {@code mStart} and {@code mEnd}. */
    ByteRange(long start, long end) {
        Preconditions.checkArgument(start >= 0);
        Preconditions.checkArgument(end >= start);
        mStart = start;
        mEnd = end;
    }

    /** Returns the start of the {@code ByteRange}. The start is included in the range. */
    long getStart() {
        return mStart;
    }

    /** Returns the end of the {@code ByteRange}. The end is included in the range. */
    long getEnd() {
        return mEnd;
    }

    /** Returns the number of bytes included in the {@code ByteRange}. */
    int getLength() {
        return (int) (mEnd - mStart + 1);
    }

    /** Creates a new {@link ByteRange} from {@code mStart} to {@code mEnd + length}. */
    ByteRange extend(long length) {
        Preconditions.checkArgument(length > 0);
        return new ByteRange(mStart, mEnd + length);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof ByteRange)) {
            return false;
        }

        ByteRange byteRange = (ByteRange) o;
        return (mEnd == byteRange.mEnd && mStart == byteRange.mStart);
    }

    @Override
    public int hashCode() {
        int result = 17;
        result = 31 * result + (int) (mStart ^ (mStart >>> 32));
        result = 31 * result + (int) (mEnd ^ (mEnd >>> 32));
        return result;
    }

    @Override
    public String toString() {
        return String.format("ByteRange{mStart=%d, mEnd=%d}", mStart, mEnd);
    }
}
+75 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2018 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.chunking;

import com.android.internal.annotations.VisibleForTesting;

import java.io.IOException;
import java.io.OutputStream;

/** Writes backup data to a diff script, using a {@link SingleStreamDiffScriptWriter}. */
public class DiffScriptBackupWriter implements BackupWriter {
    /**
     * The maximum size of a chunk in the diff script. The diff script writer {@code mWriter} will
     * buffer this many bytes in memory.
     */
    private static final int ENCRYPTION_DIFF_SCRIPT_MAX_CHUNK_SIZE_BYTES = 1024 * 1024;

    private final SingleStreamDiffScriptWriter mWriter;
    private long mBytesWritten;

    /**
     * Constructs a new writer which writes the diff script to the given output stream, using the
     * maximum new chunk size {@code ENCRYPTION_DIFF_SCRIPT_MAX_CHUNK_SIZE_BYTES}.
     */
    public static DiffScriptBackupWriter newInstance(OutputStream outputStream) {
        SingleStreamDiffScriptWriter writer =
                new SingleStreamDiffScriptWriter(
                        outputStream, ENCRYPTION_DIFF_SCRIPT_MAX_CHUNK_SIZE_BYTES);
        return new DiffScriptBackupWriter(writer);
    }

    @VisibleForTesting
    DiffScriptBackupWriter(SingleStreamDiffScriptWriter writer) {
        mWriter = writer;
    }

    @Override
    public void writeBytes(byte[] bytes) throws IOException {
        for (byte b : bytes) {
            mWriter.writeByte(b);
        }

        mBytesWritten += bytes.length;
    }

    @Override
    public void writeChunk(long start, int length) throws IOException {
        mWriter.writeChunk(start, length);
        mBytesWritten += length;
    }

    @Override
    public long getBytesWritten() {
        return mBytesWritten;
    }

    @Override
    public void flush() throws IOException {
        mWriter.flush();
    }
}
+36 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2018 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.chunking;

import java.io.IOException;
import java.io.OutputStream;

/** Writer that formats a Diff Script and writes it to an output source. */
interface DiffScriptWriter {
    /** Adds a new byte to the diff script. */
    void writeByte(byte b) throws IOException;

    /** Adds a known chunk to the diff script. */
    void writeChunk(long chunkStart, int chunkLength) throws IOException;

    /** Indicates that no more bytes or chunks will be added to the diff script. */
    void flush() throws IOException;

    interface Factory {
        DiffScriptWriter create(OutputStream outputStream);
    }
}
+25 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2018 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.chunking;

import java.io.OutputStream;

/** An interface that wraps one {@link OutputStream} with another for filtration purposes. */
public interface OutputStreamWrapper {
    /** Wraps a given {@link OutputStream}. */
    OutputStream wrap(OutputStream outputStream);
}
+130 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2018 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.chunking;

import android.annotation.Nullable;

import com.android.internal.util.Preconditions;

import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.Locale;

/**
 * A {@link DiffScriptWriter} that writes an entire diff script to a single {@link OutputStream}.
 */
public class SingleStreamDiffScriptWriter implements DiffScriptWriter {
    static final byte LINE_SEPARATOR = 0xA;
    private static final Charset UTF_8 = Charset.forName("UTF-8");

    private final int mMaxNewByteChunkSize;
    private final OutputStream mOutputStream;
    private final byte[] mByteBuffer;
    private int mBufferSize = 0;
    // Each chunk could be written immediately to the output stream. However,
    // it is possible that chunks may overlap. We therefore cache the most recent
    // reusable chunk and try to merge it with future chunks.
    private ByteRange mReusableChunk;

    public SingleStreamDiffScriptWriter(OutputStream outputStream, int maxNewByteChunkSize) {
        mOutputStream = outputStream;
        mMaxNewByteChunkSize = maxNewByteChunkSize;
        mByteBuffer = new byte[maxNewByteChunkSize];
    }

    @Override
    public void writeByte(byte b) throws IOException {
        if (mReusableChunk != null) {
            writeReusableChunk();
        }
        mByteBuffer[mBufferSize++] = b;
        if (mBufferSize == mMaxNewByteChunkSize) {
            writeByteBuffer();
        }
    }

    @Override
    public void writeChunk(long chunkStart, int chunkLength) throws IOException {
        Preconditions.checkArgument(chunkStart >= 0);
        Preconditions.checkArgument(chunkLength > 0);
        if (mBufferSize != 0) {
            writeByteBuffer();
        }

        if (mReusableChunk != null && mReusableChunk.getEnd() + 1 == chunkStart) {
            // The new chunk overlaps the old, so combine them into a single byte range.
            mReusableChunk = mReusableChunk.extend(chunkLength);
        } else {
            writeReusableChunk();
            mReusableChunk = new ByteRange(chunkStart, chunkStart + chunkLength - 1);
        }
    }

    @Override
    public void flush() throws IOException {
        Preconditions.checkState(!(mBufferSize != 0 && mReusableChunk != null));
        if (mBufferSize != 0) {
            writeByteBuffer();
        }
        if (mReusableChunk != null) {
            writeReusableChunk();
        }
        mOutputStream.flush();
    }

    private void writeByteBuffer() throws IOException {
        mOutputStream.write(Integer.toString(mBufferSize).getBytes(UTF_8));
        mOutputStream.write(LINE_SEPARATOR);
        mOutputStream.write(mByteBuffer, 0, mBufferSize);
        mOutputStream.write(LINE_SEPARATOR);
        mBufferSize = 0;
    }

    private void writeReusableChunk() throws IOException {
        if (mReusableChunk != null) {
            mOutputStream.write(
                    String.format(
                                    Locale.US,
                                    "%d-%d",
                                    mReusableChunk.getStart(),
                                    mReusableChunk.getEnd())
                            .getBytes(UTF_8));
            mOutputStream.write(LINE_SEPARATOR);
            mReusableChunk = null;
        }
    }

    /** A factory that creates {@link SingleStreamDiffScriptWriter}s. */
    public static class Factory implements DiffScriptWriter.Factory {
        private final int mMaxNewByteChunkSize;
        private final OutputStreamWrapper mOutputStreamWrapper;

        public Factory(int maxNewByteChunkSize, @Nullable OutputStreamWrapper outputStreamWrapper) {
            mMaxNewByteChunkSize = maxNewByteChunkSize;
            mOutputStreamWrapper = outputStreamWrapper;
        }

        @Override
        public SingleStreamDiffScriptWriter create(OutputStream outputStream) {
            if (mOutputStreamWrapper != null) {
                outputStream = mOutputStreamWrapper.wrap(outputStream);
            }
            return new SingleStreamDiffScriptWriter(outputStream, mMaxNewByteChunkSize);
        }
    }
}
Loading