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