content: /*
   * 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);
          }
      }
  }
  