226 lines
8.4 KiB
Java
226 lines
8.4 KiB
Java
/*
|
|
* 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);
|
|
}
|
|
};
|
|
}
|