blob: a1ed2146b38e5717ead63c489dd00120bc60d381 [file] [log] [blame]
/*
* 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);
}
};
}