File: RedactingFileDescriptor.java

package info (click to toggle)
android-platform-frameworks-base 1%3A10.0.0%2Br36-3
  • links: PTS, VCS
  • area: main
  • in suites: bullseye
  • size: 321,788 kB
  • sloc: java: 962,234; cpp: 274,314; xml: 242,770; python: 5,060; sh: 1,432; ansic: 494; makefile: 47; sed: 19
file content (225 lines) | stat: -rw-r--r-- 8,581 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
/*
 * 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);
        }
    };
}