1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.providers.media.util;
18 
19 import android.content.Context;
20 import android.os.Handler;
21 import android.os.Looper;
22 import android.os.ParcelFileDescriptor;
23 import android.os.ProxyFileDescriptorCallback;
24 import android.os.storage.StorageManager;
25 import android.system.ErrnoException;
26 import android.system.Os;
27 
28 import androidx.annotation.VisibleForTesting;
29 
30 import java.io.File;
31 import java.io.FileDescriptor;
32 import java.io.IOException;
33 import java.io.InterruptedIOException;
34 import java.util.Arrays;
35 
36 /**
37  * Variant of {@link FileDescriptor} that allows its creator to specify regions
38  * that should be redacted.
39  *
40  * @deprecated will no longer be needed once FUSE is enabled
41  */
42 @Deprecated
43 public class RedactingFileDescriptor {
44     private static final String TAG = "RedactingFileDescriptor";
45     private static final boolean DEBUG = true;
46 
47     private volatile long[] mRedactRanges;
48     private volatile long[] mFreeOffsets;
49 
50     private ParcelFileDescriptor mInner = null;
51     private ParcelFileDescriptor mOuter = null;
52 
53     private FileDescriptor mInnerFd = null;
54 
RedactingFileDescriptor( Context context, File file, int mode, long[] redactRanges, long[] freeOffsets)55     private RedactingFileDescriptor(
56             Context context, File file, int mode, long[] redactRanges, long[] freeOffsets)
57             throws IOException {
58         mRedactRanges = checkRangesArgument(redactRanges);
59         mFreeOffsets = freeOffsets;
60 
61         try {
62             mInner = FileUtils.openSafely(file, mode);
63             mInnerFd = mInner.getFileDescriptor();
64             mOuter = context.getSystemService(StorageManager.class)
65                     .openProxyFileDescriptor(mode, mCallback,
66                             new Handler(Looper.getMainLooper()));
67         } catch (IOException e) {
68             FileUtils.closeQuietly(mInner);
69             FileUtils.closeQuietly(mOuter);
70             throw e;
71         }
72     }
73 
74     @VisibleForTesting
checkRangesArgument(long[] ranges)75     public static long[] checkRangesArgument(long[] ranges) {
76         if (ranges.length % 2 != 0) {
77             throw new IllegalArgumentException();
78         }
79         for (int i = 0; i < ranges.length - 1; i += 2) {
80             if (ranges[i] > ranges[i + 1]) {
81                 throw new IllegalArgumentException();
82             }
83         }
84         return ranges;
85     }
86 
87     /**
88      * Open the given {@link File} and returns a {@link ParcelFileDescriptor}
89      * that offers a redacted view of the underlying data. If a redacted region
90      * is written to, the newly written data can be read back correctly instead
91      * of continuing to be redacted.
92      *
93      * @param file The underlying file to open.
94      * @param mode The {@link ParcelFileDescriptor} mode to open with.
95      * @param redactRanges List of file ranges that should be redacted, stored
96      *            as {@code [start1, end1, start2, end2, ...]}. Start values are
97      *            inclusive and end values are exclusive.
98      * @param freePositions List of file offsets at which the four byte value 'free' should be
99      *            written instead of zeros within parts of the file covered by {@code redactRanges}.
100      *            Non-redacted bytes will not be modified even if covered by a 'free'. This is
101      *            useful for overwriting boxes in ISOBMFF files with padding data.
102      */
open(Context context, File file, int mode, long[] redactRanges, long[] freePositions)103     public static ParcelFileDescriptor open(Context context, File file, int mode,
104             long[] redactRanges, long[] freePositions) throws IOException {
105         return new RedactingFileDescriptor(context, file, mode, redactRanges, freePositions).mOuter;
106     }
107 
108     /**
109      * Update the given ranges argument to remove any references to the given
110      * offset and length. This is typically used when a caller has written over
111      * a previously redacted region.
112      */
removeRange(long[] ranges, long start, long end)113     public static long[] removeRange(long[] ranges, long start, long end) {
114         if (start == end) {
115             return ranges;
116         } else if (start > end) {
117             throw new IllegalArgumentException();
118         }
119 
120         long[] res = new long[0];
121         for (int i = 0; i < ranges.length; i += 2) {
122             if (start <= ranges[i] && end >= ranges[i + 1]) {
123                 // Range entirely covered; remove it
124             } else if (start >= ranges[i] && end <= ranges[i + 1]) {
125                 // Range partially covered; punch a hole
126                 res = Arrays.copyOf(res, res.length + 4);
127                 res[res.length - 4] = ranges[i];
128                 res[res.length - 3] = start;
129                 res[res.length - 2] = end;
130                 res[res.length - 1] = ranges[i + 1];
131             } else {
132                 // Range might covered; adjust edges if needed
133                 res = Arrays.copyOf(res, res.length + 2);
134                 if (end >= ranges[i] && end <= ranges[i + 1]) {
135                     res[res.length - 2] = Math.max(ranges[i], end);
136                 } else {
137                     res[res.length - 2] = ranges[i];
138                 }
139                 if (start >= ranges[i] && start <= ranges[i + 1]) {
140                     res[res.length - 1] = Math.min(ranges[i + 1], start);
141                 } else {
142                     res[res.length - 1] = ranges[i + 1];
143                 }
144             }
145         }
146         return res;
147     }
148 
149     private final ProxyFileDescriptorCallback mCallback = new ProxyFileDescriptorCallback() {
150         @Override
151         public long onGetSize() throws ErrnoException {
152             return Os.fstat(mInnerFd).st_size;
153         }
154 
155         @Override
156         public int onRead(long offset, int size, byte[] data) throws ErrnoException {
157             int n = 0;
158             while (n < size) {
159                 try {
160                     final int res = Os.pread(mInnerFd, data, n, size - n, offset + n);
161                     if (res == 0) {
162                         break;
163                     } else {
164                         n += res;
165                     }
166                 } catch (InterruptedIOException e) {
167                     n += e.bytesTransferred;
168                 }
169             }
170 
171             // Redact any relevant ranges before returning
172             final long[] ranges = mRedactRanges;
173             for (int i = 0; i < ranges.length; i += 2) {
174                 final long start = Math.max(offset, ranges[i]);
175                 final long end = Math.min(offset + size, ranges[i + 1]);
176                 for (long j = start; j < end; j++) {
177                     data[(int) (j - offset)] = 0;
178                 }
179                 // Overwrite data at 'free' offsets within the redaction ranges.
180                 for (long freeOffset : mFreeOffsets) {
181                     final long freeEnd = freeOffset + 4;
182                     final long redactFreeStart = Math.max(freeOffset, start);
183                     final long redactFreeEnd = Math.min(freeEnd, end);
184                     for (long j = redactFreeStart; j < redactFreeEnd; j++) {
185                         data[(int) (j - offset)] = (byte) "free".charAt((int) (j - freeOffset));
186                     }
187                 }
188             }
189             return n;
190         }
191 
192         @Override
193         public int onWrite(long offset, int size, byte[] data) throws ErrnoException {
194             int n = 0;
195             while (n < size) {
196                 try {
197                     final int res = Os.pwrite(mInnerFd, data, n, size - n, offset + n);
198                     if (res == 0) {
199                         break;
200                     } else {
201                         n += res;
202                     }
203                 } catch (InterruptedIOException e) {
204                     n += e.bytesTransferred;
205                 }
206             }
207 
208             // Clear any relevant redaction ranges before returning, since the
209             // writer should have access to see the data they just overwrote
210             mRedactRanges = removeRange(mRedactRanges, offset, offset + n);
211             return n;
212         }
213 
214         @Override
215         public void onFsync() throws ErrnoException {
216             Os.fsync(mInnerFd);
217         }
218 
219         @Override
220         public void onRelease() {
221             FileUtils.closeQuietly(mInner);
222         }
223     };
224 }
225