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 android.os;
  
  import android.content.Context;
  import android.os.storage.StorageManager;
  import android.system.ErrnoException;
  import android.system.Os;
  import android.util.Slog;
  
  import com.android.internal.annotations.VisibleForTesting;
  
  import libcore.io.IoUtils;
  import libcore.util.EmptyArray;
  
  import java.io.File;
  import java.io.FileDescriptor;
  import java.io.IOException;
  import java.io.InterruptedIOException;
  import java.util.Arrays;
  
  /**
   * Variant of {@link FileDescriptor} that allows its creator to specify regions
   * that should be redacted.
   *
   * @hide
   */
  public class RedactingFileDescriptor {
      private static final String TAG = "RedactingFileDescriptor";
      private static final boolean DEBUG = true;
  
      private volatile long[] mRedactRanges;
      private volatile long[] mFreeOffsets;
  
      private FileDescriptor mInner = null;
      private ParcelFileDescriptor mOuter = null;
  
      private RedactingFileDescriptor(
              Context context, File file, int mode, long[] redactRanges, long[] freeOffsets)
              throws IOException {
          mRedactRanges = checkRangesArgument(redactRanges);
          mFreeOffsets = freeOffsets;
  
          try {
              try {
                  mInner = Os.open(file.getAbsolutePath(),
                          FileUtils.translateModePfdToPosix(mode), 0);
                  mOuter = context.getSystemService(StorageManager.class)
                          .openProxyFileDescriptor(mode, mCallback);
              } catch (ErrnoException e) {
                  throw e.rethrowAsIOException();
              }
          } catch (IOException e) {
              IoUtils.closeQuietly(mInner);
              IoUtils.closeQuietly(mOuter);
              throw e;
          }
      }
  
      private static long[] checkRangesArgument(long[] ranges) {
          if (ranges.length % 2 != 0) {
              throw new IllegalArgumentException();
          }
          for (int i = 0; i < ranges.length - 1; i += 2) {
              if (ranges[i] > ranges[i + 1]) {
                  throw new IllegalArgumentException();
              }
          }
          return ranges;
      }
  
      /**
       * Open the given {@link File} and returns a {@link ParcelFileDescriptor}
       * that offers a redacted view of the underlying data. If a redacted region
       * is written to, the newly written data can be read back correctly instead
       * of continuing to be redacted.
       *
       * @param file The underlying file to open.
       * @param mode The {@link ParcelFileDescriptor} mode to open with.
       * @param redactRanges List of file ranges that should be redacted, stored
       *            as {@code [start1, end1, start2, end2, ...]}. Start values are
       *            inclusive and end values are exclusive.
       * @param freePositions List of file offsets at which the four byte value 'free' should be
       *            written instead of zeros within parts of the file covered by {@code redactRanges}.
       *            Non-redacted bytes will not be modified even if covered by a 'free'. This is
       *            useful for overwriting boxes in ISOBMFF files with padding data.
       */
      public static ParcelFileDescriptor open(Context context, File file, int mode,
              long[] redactRanges, long[] freePositions) throws IOException {
          return new RedactingFileDescriptor(context, file, mode, redactRanges, freePositions).mOuter;
      }
  
      /**
       * Update the given ranges argument to remove any references to the given
       * offset and length. This is typically used when a caller has written over
       * a previously redacted region.
       */
      @VisibleForTesting
      public static long[] removeRange(long[] ranges, long start, long end) {
          if (start == end) {
              return ranges;
          } else if (start > end) {
              throw new IllegalArgumentException();
          }
  
          long[] res = EmptyArray.LONG;
          for (int i = 0; i < ranges.length; i += 2) {
              if (start <= ranges[i] && end >= ranges[i + 1]) {
                  // Range entirely covered; remove it
              } else if (start >= ranges[i] && end <= ranges[i + 1]) {
                  // Range partially covered; punch a hole
                  res = Arrays.copyOf(res, res.length + 4);
                  res[res.length - 4] = ranges[i];
                  res[res.length - 3] = start;
                  res[res.length - 2] = end;
                  res[res.length - 1] = ranges[i + 1];
              } else {
                  // Range might covered; adjust edges if needed
                  res = Arrays.copyOf(res, res.length + 2);
                  if (end >= ranges[i] && end <= ranges[i + 1]) {
                      res[res.length - 2] = Math.max(ranges[i], end);
                  } else {
                      res[res.length - 2] = ranges[i];
                  }
                  if (start >= ranges[i] && start <= ranges[i + 1]) {
                      res[res.length - 1] = Math.min(ranges[i + 1], start);
                  } else {
                      res[res.length - 1] = ranges[i + 1];
                  }
              }
          }
          return res;
      }
  
      private final ProxyFileDescriptorCallback mCallback = new ProxyFileDescriptorCallback() {
          @Override
          public long onGetSize() throws ErrnoException {
              return Os.fstat(mInner).st_size;
          }
  
          @Override
          public int onRead(long offset, int size, byte[] data) throws ErrnoException {
              int n = 0;
              while (n < size) {
                  try {
                      final int res = Os.pread(mInner, data, n, size - n, offset + n);
                      if (res == 0) {
                          break;
                      } else {
                          n += res;
                      }
                  } catch (InterruptedIOException e) {
                      n += e.bytesTransferred;
                  }
              }
  
              // Redact any relevant ranges before returning
              final long[] ranges = mRedactRanges;
              for (int i = 0; i < ranges.length; i += 2) {
                  final long start = Math.max(offset, ranges[i]);
                  final long end = Math.min(offset + size, ranges[i + 1]);
                  for (long j = start; j < end; j++) {
                      data[(int) (j - offset)] = 0;
                  }
                  // Overwrite data at 'free' offsets within the redaction ranges.
                  for (long freeOffset : mFreeOffsets) {
                      final long freeEnd = freeOffset + 4;
                      final long redactFreeStart = Math.max(freeOffset, start);
                      final long redactFreeEnd = Math.min(freeEnd, end);
                      for (long j = redactFreeStart; j < redactFreeEnd; j++) {
                          data[(int) (j - offset)] = (byte) "free".charAt((int) (j - freeOffset));
                      }
                  }
              }
              return n;
          }
  
          @Override
          public int onWrite(long offset, int size, byte[] data) throws ErrnoException {
              int n = 0;
              while (n < size) {
                  try {
                      final int res = Os.pwrite(mInner, data, n, size - n, offset + n);
                      if (res == 0) {
                          break;
                      } else {
                          n += res;
                      }
                  } catch (InterruptedIOException e) {
                      n += e.bytesTransferred;
                  }
              }
  
              // Clear any relevant redaction ranges before returning, since the
              // writer should have access to see the data they just overwrote
              mRedactRanges = removeRange(mRedactRanges, offset, offset + n);
              return n;
          }
  
          @Override
          public void onFsync() throws ErrnoException {
              Os.fsync(mInner);
          }
  
          @Override
          public void onRelease() {
              if (DEBUG) Slog.v(TAG, "onRelease()");
              IoUtils.closeQuietly(mInner);
          }
      };
  }
  