blob: 760df45d2b69818c56437a2690b0625b8ddd7083 [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
2 * Copyright (C) 2006 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
17package android.os;
18
Jeff Sharkey85ced632015-07-22 11:11:46 -070019import android.annotation.NonNull;
Jeff Sharkey15447792015-11-05 16:18:51 -080020import android.annotation.Nullable;
Ben Kwa62539a22015-04-22 15:43:17 -070021import android.provider.DocumentsContract.Document;
Elliott Hughes34385d32014-04-28 11:11:32 -070022import android.system.ErrnoException;
23import android.system.Os;
Jeff Sharkey35871f22016-01-29 17:13:29 -070024import android.system.StructStat;
Jeff Sharkey0cce5352014-11-26 13:38:26 -080025import android.text.TextUtils;
Jeff Sharkeyd9526902013-03-14 14:11:57 -070026import android.util.Log;
Jeff Sharkey184a0102013-07-10 16:19:52 -070027import android.util.Slog;
Ben Kwa62539a22015-04-22 15:43:17 -070028import android.webkit.MimeTypeMap;
Jeff Sharkey184a0102013-07-10 16:19:52 -070029
Jeff Sharkey4f5e8b32015-06-11 19:13:37 -070030import com.android.internal.annotations.VisibleForTesting;
31
Jeff Sharkeyc4bab982016-02-01 10:16:01 -070032import libcore.util.EmptyArray;
33
Guang Zhu90619812012-10-12 15:50:44 -070034import java.io.BufferedInputStream;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080035import java.io.ByteArrayOutputStream;
36import java.io.File;
Jeff Sharkey184a0102013-07-10 16:19:52 -070037import java.io.FileDescriptor;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080038import java.io.FileInputStream;
Wink Saville6d25a992011-06-03 17:03:51 -070039import java.io.FileNotFoundException;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080040import java.io.FileOutputStream;
Mike Lockwoodda8bb742011-05-28 13:24:04 -040041import java.io.FileWriter;
Jeff Sharkey35871f22016-01-29 17:13:29 -070042import java.io.FilenameFilter;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080043import java.io.IOException;
44import java.io.InputStream;
Jeff Sharkey4f5e8b32015-06-11 19:13:37 -070045import java.nio.charset.StandardCharsets;
Jeff Sharkeyd9526902013-03-14 14:11:57 -070046import java.util.Arrays;
47import java.util.Comparator;
Ben Kwa62539a22015-04-22 15:43:17 -070048import java.util.Objects;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080049import java.util.regex.Pattern;
Wink Saville6d25a992011-06-03 17:03:51 -070050import java.util.zip.CRC32;
51import java.util.zip.CheckedInputStream;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080052
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080053/**
54 * Tools for managing files. Not for public consumption.
55 * @hide
56 */
Wink Saville6d25a992011-06-03 17:03:51 -070057public class FileUtils {
Jeff Sharkeyd9526902013-03-14 14:11:57 -070058 private static final String TAG = "FileUtils";
59
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080060 public static final int S_IRWXU = 00700;
61 public static final int S_IRUSR = 00400;
62 public static final int S_IWUSR = 00200;
63 public static final int S_IXUSR = 00100;
64
65 public static final int S_IRWXG = 00070;
66 public static final int S_IRGRP = 00040;
67 public static final int S_IWGRP = 00020;
68 public static final int S_IXGRP = 00010;
69
70 public static final int S_IRWXO = 00007;
71 public static final int S_IROTH = 00004;
72 public static final int S_IWOTH = 00002;
73 public static final int S_IXOTH = 00001;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080074
Andreas Gampe0693fd82016-04-28 19:33:05 -070075 /** Regular expression for safe filenames: no spaces or metacharacters.
76 *
77 * Use a preload holder so that FileUtils can be compile-time initialized.
78 */
79 private static class NoImagePreloadHolder {
80 public static final Pattern SAFE_FILENAME_PATTERN = Pattern.compile("[\\w%+,./=_-]+");
81 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080082
Jeff Sharkey85ced632015-07-22 11:11:46 -070083 private static final File[] EMPTY = new File[0];
84
Jeff Sharkey184a0102013-07-10 16:19:52 -070085 /**
86 * Set owner and mode of of given {@link File}.
87 *
88 * @param mode to apply through {@code chmod}
89 * @param uid to apply through {@code chown}, or -1 to leave unchanged
90 * @param gid to apply through {@code chown}, or -1 to leave unchanged
91 * @return 0 on success, otherwise errno.
92 */
93 public static int setPermissions(File path, int mode, int uid, int gid) {
94 return setPermissions(path.getAbsolutePath(), mode, uid, gid);
95 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080096
Jeff Sharkey184a0102013-07-10 16:19:52 -070097 /**
98 * Set owner and mode of of given path.
99 *
100 * @param mode to apply through {@code chmod}
101 * @param uid to apply through {@code chown}, or -1 to leave unchanged
102 * @param gid to apply through {@code chown}, or -1 to leave unchanged
103 * @return 0 on success, otherwise errno.
104 */
105 public static int setPermissions(String path, int mode, int uid, int gid) {
106 try {
Elliott Hughes34385d32014-04-28 11:11:32 -0700107 Os.chmod(path, mode);
Jeff Sharkey184a0102013-07-10 16:19:52 -0700108 } catch (ErrnoException e) {
109 Slog.w(TAG, "Failed to chmod(" + path + "): " + e);
110 return e.errno;
111 }
112
113 if (uid >= 0 || gid >= 0) {
114 try {
Elliott Hughes34385d32014-04-28 11:11:32 -0700115 Os.chown(path, uid, gid);
Jeff Sharkey184a0102013-07-10 16:19:52 -0700116 } catch (ErrnoException e) {
117 Slog.w(TAG, "Failed to chown(" + path + "): " + e);
118 return e.errno;
119 }
120 }
121
122 return 0;
123 }
124
125 /**
126 * Set owner and mode of of given {@link FileDescriptor}.
127 *
128 * @param mode to apply through {@code chmod}
129 * @param uid to apply through {@code chown}, or -1 to leave unchanged
130 * @param gid to apply through {@code chown}, or -1 to leave unchanged
131 * @return 0 on success, otherwise errno.
132 */
133 public static int setPermissions(FileDescriptor fd, int mode, int uid, int gid) {
134 try {
Elliott Hughes34385d32014-04-28 11:11:32 -0700135 Os.fchmod(fd, mode);
Jeff Sharkey184a0102013-07-10 16:19:52 -0700136 } catch (ErrnoException e) {
137 Slog.w(TAG, "Failed to fchmod(): " + e);
138 return e.errno;
139 }
140
141 if (uid >= 0 || gid >= 0) {
142 try {
Elliott Hughes34385d32014-04-28 11:11:32 -0700143 Os.fchown(fd, uid, gid);
Jeff Sharkey184a0102013-07-10 16:19:52 -0700144 } catch (ErrnoException e) {
145 Slog.w(TAG, "Failed to fchown(): " + e);
146 return e.errno;
147 }
148 }
149
150 return 0;
151 }
152
Jeff Sharkey35871f22016-01-29 17:13:29 -0700153 public static void copyPermissions(File from, File to) throws IOException {
154 try {
155 final StructStat stat = Os.stat(from.getAbsolutePath());
156 Os.chmod(to.getAbsolutePath(), stat.st_mode);
157 Os.chown(to.getAbsolutePath(), stat.st_uid, stat.st_gid);
158 } catch (ErrnoException e) {
159 throw e.rethrowAsIOException();
160 }
161 }
162
Jeff Sharkey184a0102013-07-10 16:19:52 -0700163 /**
164 * Return owning UID of given path, otherwise -1.
165 */
166 public static int getUid(String path) {
167 try {
Elliott Hughes34385d32014-04-28 11:11:32 -0700168 return Os.stat(path).st_uid;
Jeff Sharkey184a0102013-07-10 16:19:52 -0700169 } catch (ErrnoException e) {
170 return -1;
171 }
172 }
Dianne Hackborn053f61d2013-06-26 18:07:43 -0700173
Dianne Hackborn8bdf5932010-10-15 12:54:40 -0700174 /**
175 * Perform an fsync on the given FileOutputStream. The stream at this
176 * point must be flushed but not yet closed.
177 */
178 public static boolean sync(FileOutputStream stream) {
179 try {
180 if (stream != null) {
181 stream.getFD().sync();
182 }
183 return true;
184 } catch (IOException e) {
185 }
186 return false;
187 }
188
Jeff Sharkey35871f22016-01-29 17:13:29 -0700189 @Deprecated
190 public static boolean copyFile(File srcFile, File destFile) {
191 try {
192 copyFileOrThrow(srcFile, destFile);
193 return true;
194 } catch (IOException e) {
195 return false;
196 }
197 }
198
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800199 // copy a file from srcFile to destFile, return true if succeed, return
200 // false if fail
Jeff Sharkey35871f22016-01-29 17:13:29 -0700201 public static void copyFileOrThrow(File srcFile, File destFile) throws IOException {
202 try (InputStream in = new FileInputStream(srcFile)) {
203 copyToFileOrThrow(in, destFile);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800204 }
Jeff Sharkey35871f22016-01-29 17:13:29 -0700205 }
206
207 @Deprecated
208 public static boolean copyToFile(InputStream inputStream, File destFile) {
209 try {
210 copyToFileOrThrow(inputStream, destFile);
211 return true;
212 } catch (IOException e) {
213 return false;
214 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800215 }
Guang Zhu90619812012-10-12 15:50:44 -0700216
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800217 /**
218 * Copy data from a source stream to destFile.
219 * Return true if succeed, return false if failed.
220 */
Jeff Sharkey35871f22016-01-29 17:13:29 -0700221 public static void copyToFileOrThrow(InputStream inputStream, File destFile)
222 throws IOException {
223 if (destFile.exists()) {
224 destFile.delete();
225 }
226 FileOutputStream out = new FileOutputStream(destFile);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800227 try {
Jeff Sharkey35871f22016-01-29 17:13:29 -0700228 byte[] buffer = new byte[4096];
229 int bytesRead;
230 while ((bytesRead = inputStream.read(buffer)) >= 0) {
231 out.write(buffer, 0, bytesRead);
Dianne Hackborn1afd1c92010-03-18 22:47:17 -0700232 }
Jeff Sharkey35871f22016-01-29 17:13:29 -0700233 } finally {
234 out.flush();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800235 try {
Jeff Sharkey35871f22016-01-29 17:13:29 -0700236 out.getFD().sync();
237 } catch (IOException e) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800238 }
Jeff Sharkey35871f22016-01-29 17:13:29 -0700239 out.close();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800240 }
241 }
242
243 /**
244 * Check if a filename is "safe" (no metacharacters or spaces).
245 * @param file The file to check
246 */
247 public static boolean isFilenameSafe(File file) {
248 // Note, we check whether it matches what's known to be safe,
249 // rather than what's known to be unsafe. Non-ASCII, control
250 // characters, etc. are all unsafe by default.
Andreas Gampe0693fd82016-04-28 19:33:05 -0700251 return NoImagePreloadHolder.SAFE_FILENAME_PATTERN.matcher(file.getPath()).matches();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800252 }
253
254 /**
255 * Read a text file into a String, optionally limiting the length.
256 * @param file to read (will not seek, so things like /proc files are OK)
257 * @param max length (positive for head, negative of tail, 0 for no limit)
258 * @param ellipsis to add of the file was truncated (can be null)
259 * @return the contents of the file, possibly truncated
260 * @throws IOException if something goes wrong reading the file
261 */
262 public static String readTextFile(File file, int max, String ellipsis) throws IOException {
263 InputStream input = new FileInputStream(file);
Guang Zhu90619812012-10-12 15:50:44 -0700264 // wrapping a BufferedInputStream around it because when reading /proc with unbuffered
265 // input stream, bytes read not equal to buffer size is not necessarily the correct
266 // indication for EOF; but it is true for BufferedInputStream due to its implementation.
267 BufferedInputStream bis = new BufferedInputStream(input);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800268 try {
Dan Egnor42471dd2010-01-07 17:25:22 -0800269 long size = file.length();
270 if (max > 0 || (size > 0 && max == 0)) { // "head" mode: read the first N bytes
271 if (size > 0 && (max == 0 || size < max)) max = (int) size;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800272 byte[] data = new byte[max + 1];
Guang Zhu90619812012-10-12 15:50:44 -0700273 int length = bis.read(data);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800274 if (length <= 0) return "";
275 if (length <= max) return new String(data, 0, length);
276 if (ellipsis == null) return new String(data, 0, max);
277 return new String(data, 0, max) + ellipsis;
Dan Egnor42471dd2010-01-07 17:25:22 -0800278 } else if (max < 0) { // "tail" mode: keep the last N
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800279 int len;
280 boolean rolled = false;
Jeff Sharkeyd9526902013-03-14 14:11:57 -0700281 byte[] last = null;
282 byte[] data = null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800283 do {
284 if (last != null) rolled = true;
285 byte[] tmp = last; last = data; data = tmp;
286 if (data == null) data = new byte[-max];
Guang Zhu90619812012-10-12 15:50:44 -0700287 len = bis.read(data);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800288 } while (len == data.length);
289
290 if (last == null && len <= 0) return "";
291 if (last == null) return new String(data, 0, len);
292 if (len > 0) {
293 rolled = true;
294 System.arraycopy(last, len, last, 0, last.length - len);
295 System.arraycopy(data, 0, last, last.length - len, len);
296 }
297 if (ellipsis == null || !rolled) return new String(last);
298 return ellipsis + new String(last);
Dan Egnor42471dd2010-01-07 17:25:22 -0800299 } else { // "cat" mode: size unknown, read it all in streaming fashion
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800300 ByteArrayOutputStream contents = new ByteArrayOutputStream();
301 int len;
302 byte[] data = new byte[1024];
303 do {
Guang Zhu90619812012-10-12 15:50:44 -0700304 len = bis.read(data);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800305 if (len > 0) contents.write(data, 0, len);
306 } while (len == data.length);
307 return contents.toString();
308 }
309 } finally {
Guang Zhu90619812012-10-12 15:50:44 -0700310 bis.close();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800311 input.close();
312 }
313 }
Mike Lockwoodda8bb742011-05-28 13:24:04 -0400314
Jeff Sharkey2271ba32016-02-01 17:57:08 -0700315 public static void stringToFile(File file, String string) throws IOException {
316 stringToFile(file.getAbsolutePath(), string);
317 }
318
Narayan Kamath6d051fc2016-11-22 20:20:00 +0000319 /*
320 * Writes the bytes given in {@code content} to the file whose absolute path
321 * is {@code filename}.
322 */
323 public static void bytesToFile(String filename, byte[] content) throws IOException {
324 try (FileOutputStream fos = new FileOutputStream(filename)) {
325 fos.write(content);
326 }
327 }
328
Jeff Sharkey2271ba32016-02-01 17:57:08 -0700329 /**
Mike Lockwoodda8bb742011-05-28 13:24:04 -0400330 * Writes string to file. Basically same as "echo -n $string > $filename"
331 *
332 * @param filename
333 * @param string
334 * @throws IOException
335 */
336 public static void stringToFile(String filename, String string) throws IOException {
337 FileWriter out = new FileWriter(filename);
338 try {
339 out.write(string);
340 } finally {
341 out.close();
342 }
343 }
Wink Saville1b9a6a62011-06-04 07:31:35 -0700344
Wink Saville6d25a992011-06-03 17:03:51 -0700345 /**
346 * Computes the checksum of a file using the CRC32 checksum routine.
347 * The value of the checksum is returned.
348 *
349 * @param file the file to checksum, must not be null
350 * @return the checksum value or an exception is thrown.
351 */
352 public static long checksumCrc32(File file) throws FileNotFoundException, IOException {
353 CRC32 checkSummer = new CRC32();
354 CheckedInputStream cis = null;
355
356 try {
357 cis = new CheckedInputStream( new FileInputStream(file), checkSummer);
358 byte[] buf = new byte[128];
359 while(cis.read(buf) >= 0) {
360 // Just read for checksum to get calculated.
361 }
362 return checkSummer.getValue();
363 } finally {
364 if (cis != null) {
365 try {
366 cis.close();
367 } catch (IOException e) {
368 }
369 }
370 }
371 }
Jeff Sharkeyd9526902013-03-14 14:11:57 -0700372
373 /**
374 * Delete older files in a directory until only those matching the given
375 * constraints remain.
376 *
377 * @param minCount Always keep at least this many files.
378 * @param minAge Always keep files younger than this age.
Jeff Sharkeyebf8ad52014-01-30 15:01:22 -0800379 * @return if any files were deleted.
Jeff Sharkeyd9526902013-03-14 14:11:57 -0700380 */
Jeff Sharkeyebf8ad52014-01-30 15:01:22 -0800381 public static boolean deleteOlderFiles(File dir, int minCount, long minAge) {
Jeff Sharkeyd9526902013-03-14 14:11:57 -0700382 if (minCount < 0 || minAge < 0) {
383 throw new IllegalArgumentException("Constraints must be positive or 0");
384 }
385
386 final File[] files = dir.listFiles();
Jeff Sharkeyebf8ad52014-01-30 15:01:22 -0800387 if (files == null) return false;
Jeff Sharkeyd9526902013-03-14 14:11:57 -0700388
389 // Sort with newest files first
390 Arrays.sort(files, new Comparator<File>() {
391 @Override
392 public int compare(File lhs, File rhs) {
Ian Rogers660e6de2016-05-17 11:50:54 -0700393 return Long.compare(rhs.lastModified(), lhs.lastModified());
Jeff Sharkeyd9526902013-03-14 14:11:57 -0700394 }
395 });
396
397 // Keep at least minCount files
Jeff Sharkeyebf8ad52014-01-30 15:01:22 -0800398 boolean deleted = false;
Jeff Sharkeyd9526902013-03-14 14:11:57 -0700399 for (int i = minCount; i < files.length; i++) {
400 final File file = files[i];
401
402 // Keep files newer than minAge
403 final long age = System.currentTimeMillis() - file.lastModified();
404 if (age > minAge) {
Jeff Sharkeyebf8ad52014-01-30 15:01:22 -0800405 if (file.delete()) {
406 Log.d(TAG, "Deleted old file " + file);
407 deleted = true;
408 }
Jeff Sharkeyd9526902013-03-14 14:11:57 -0700409 }
410 }
Jeff Sharkeyebf8ad52014-01-30 15:01:22 -0800411 return deleted;
Jeff Sharkeyd9526902013-03-14 14:11:57 -0700412 }
Jeff Sharkey4ca728c2014-01-10 16:27:19 -0800413
414 /**
415 * Test if a file lives under the given directory, either as a direct child
416 * or a distant grandchild.
417 * <p>
418 * Both files <em>must</em> have been resolved using
419 * {@link File#getCanonicalFile()} to avoid symlink or path traversal
420 * attacks.
421 */
Jeff Sharkey48877892015-03-18 11:27:19 -0700422 public static boolean contains(File[] dirs, File file) {
423 for (File dir : dirs) {
424 if (contains(dir, file)) {
425 return true;
426 }
427 }
428 return false;
429 }
430
431 /**
432 * Test if a file lives under the given directory, either as a direct child
433 * or a distant grandchild.
434 * <p>
435 * Both files <em>must</em> have been resolved using
436 * {@link File#getCanonicalFile()} to avoid symlink or path traversal
437 * attacks.
438 */
Jeff Sharkey4ca728c2014-01-10 16:27:19 -0800439 public static boolean contains(File dir, File file) {
Jeff Sharkey50a05452015-04-29 11:24:52 -0700440 if (dir == null || file == null) return false;
Jeff Sharkeyd7460572014-07-06 20:44:55 -0700441
Jeff Sharkey21de56a2014-04-05 19:05:24 -0700442 String dirPath = dir.getAbsolutePath();
443 String filePath = file.getAbsolutePath();
Jeff Sharkey4ca728c2014-01-10 16:27:19 -0800444
445 if (dirPath.equals(filePath)) {
446 return true;
447 }
448
449 if (!dirPath.endsWith("/")) {
450 dirPath += "/";
451 }
452 return filePath.startsWith(dirPath);
453 }
Jeff Sharkey3a44f3f2014-04-28 17:36:31 -0700454
Jeff Sharkeyfcf1e552016-04-14 20:44:58 -0600455 public static boolean deleteContentsAndDir(File dir) {
456 if (deleteContents(dir)) {
457 return dir.delete();
458 } else {
459 return false;
460 }
461 }
462
Jeff Sharkey57dcf5b2014-06-18 17:46:05 -0700463 public static boolean deleteContents(File dir) {
Jeff Sharkey3a44f3f2014-04-28 17:36:31 -0700464 File[] files = dir.listFiles();
Jeff Sharkey57dcf5b2014-06-18 17:46:05 -0700465 boolean success = true;
Jeff Sharkey3a44f3f2014-04-28 17:36:31 -0700466 if (files != null) {
467 for (File file : files) {
468 if (file.isDirectory()) {
Jeff Sharkey57dcf5b2014-06-18 17:46:05 -0700469 success &= deleteContents(file);
Jeff Sharkey3a44f3f2014-04-28 17:36:31 -0700470 }
Jeff Sharkey73767b92014-07-04 20:18:13 -0700471 if (!file.delete()) {
472 Log.w(TAG, "Failed to delete " + file);
473 success = false;
474 }
Jeff Sharkey3a44f3f2014-04-28 17:36:31 -0700475 }
476 }
Jeff Sharkey57dcf5b2014-06-18 17:46:05 -0700477 return success;
Jeff Sharkey3a44f3f2014-04-28 17:36:31 -0700478 }
479
Jeff Sharkey0cce5352014-11-26 13:38:26 -0800480 private static boolean isValidExtFilenameChar(char c) {
481 switch (c) {
482 case '\0':
483 case '/':
484 return false;
485 default:
486 return true;
487 }
488 }
489
Jeff Sharkey3a44f3f2014-04-28 17:36:31 -0700490 /**
Jeff Sharkey0cce5352014-11-26 13:38:26 -0800491 * Check if given filename is valid for an ext4 filesystem.
Jeff Sharkey3a44f3f2014-04-28 17:36:31 -0700492 */
493 public static boolean isValidExtFilename(String name) {
Jeff Sharkey0cce5352014-11-26 13:38:26 -0800494 return (name != null) && name.equals(buildValidExtFilename(name));
495 }
496
497 /**
498 * Mutate the given filename to make it valid for an ext4 filesystem,
499 * replacing any invalid characters with "_".
500 */
501 public static String buildValidExtFilename(String name) {
Jeff Sharkey3a44f3f2014-04-28 17:36:31 -0700502 if (TextUtils.isEmpty(name) || ".".equals(name) || "..".equals(name)) {
Jeff Sharkey0cce5352014-11-26 13:38:26 -0800503 return "(invalid)";
Jeff Sharkey3a44f3f2014-04-28 17:36:31 -0700504 }
Jeff Sharkey0cce5352014-11-26 13:38:26 -0800505 final StringBuilder res = new StringBuilder(name.length());
Jeff Sharkey3a44f3f2014-04-28 17:36:31 -0700506 for (int i = 0; i < name.length(); i++) {
507 final char c = name.charAt(i);
Jeff Sharkey0cce5352014-11-26 13:38:26 -0800508 if (isValidExtFilenameChar(c)) {
509 res.append(c);
510 } else {
511 res.append('_');
Jeff Sharkey3a44f3f2014-04-28 17:36:31 -0700512 }
513 }
Jeff Sharkey4f5e8b32015-06-11 19:13:37 -0700514 trimFilename(res, 255);
Jeff Sharkey0cce5352014-11-26 13:38:26 -0800515 return res.toString();
516 }
517
518 private static boolean isValidFatFilenameChar(char c) {
519 if ((0x00 <= c && c <= 0x1f)) {
520 return false;
521 }
522 switch (c) {
523 case '"':
524 case '*':
525 case '/':
526 case ':':
527 case '<':
528 case '>':
529 case '?':
530 case '\\':
531 case '|':
532 case 0x7F:
533 return false;
534 default:
535 return true;
536 }
537 }
538
539 /**
540 * Check if given filename is valid for a FAT filesystem.
541 */
542 public static boolean isValidFatFilename(String name) {
543 return (name != null) && name.equals(buildValidFatFilename(name));
544 }
545
546 /**
547 * Mutate the given filename to make it valid for a FAT filesystem,
548 * replacing any invalid characters with "_".
549 */
550 public static String buildValidFatFilename(String name) {
551 if (TextUtils.isEmpty(name) || ".".equals(name) || "..".equals(name)) {
552 return "(invalid)";
553 }
554 final StringBuilder res = new StringBuilder(name.length());
555 for (int i = 0; i < name.length(); i++) {
556 final char c = name.charAt(i);
557 if (isValidFatFilenameChar(c)) {
558 res.append(c);
559 } else {
560 res.append('_');
561 }
562 }
Jeff Sharkey4f5e8b32015-06-11 19:13:37 -0700563 // Even though vfat allows 255 UCS-2 chars, we might eventually write to
564 // ext4 through a FUSE layer, so use that limit.
565 trimFilename(res, 255);
Jeff Sharkey0cce5352014-11-26 13:38:26 -0800566 return res.toString();
Jeff Sharkey3a44f3f2014-04-28 17:36:31 -0700567 }
Jeff Sharkey57dcf5b2014-06-18 17:46:05 -0700568
Jeff Sharkey4f5e8b32015-06-11 19:13:37 -0700569 @VisibleForTesting
570 public static String trimFilename(String str, int maxBytes) {
571 final StringBuilder res = new StringBuilder(str);
572 trimFilename(res, maxBytes);
573 return res.toString();
574 }
575
576 private static void trimFilename(StringBuilder res, int maxBytes) {
577 byte[] raw = res.toString().getBytes(StandardCharsets.UTF_8);
578 if (raw.length > maxBytes) {
579 maxBytes -= 3;
580 while (raw.length > maxBytes) {
581 res.deleteCharAt(res.length() / 2);
582 raw = res.toString().getBytes(StandardCharsets.UTF_8);
583 }
584 res.insert(res.length() / 2, "...");
585 }
586 }
587
Jeff Sharkey57dcf5b2014-06-18 17:46:05 -0700588 public static String rewriteAfterRename(File beforeDir, File afterDir, String path) {
Jeff Sharkeyd7460572014-07-06 20:44:55 -0700589 if (path == null) return null;
Jeff Sharkey57dcf5b2014-06-18 17:46:05 -0700590 final File result = rewriteAfterRename(beforeDir, afterDir, new File(path));
591 return (result != null) ? result.getAbsolutePath() : null;
592 }
593
Jeff Sharkeyd7460572014-07-06 20:44:55 -0700594 public static String[] rewriteAfterRename(File beforeDir, File afterDir, String[] paths) {
595 if (paths == null) return null;
596 final String[] result = new String[paths.length];
597 for (int i = 0; i < paths.length; i++) {
598 result[i] = rewriteAfterRename(beforeDir, afterDir, paths[i]);
599 }
600 return result;
601 }
602
Jeff Sharkey57dcf5b2014-06-18 17:46:05 -0700603 /**
604 * Given a path under the "before" directory, rewrite it to live under the
605 * "after" directory. For example, {@code /before/foo/bar.txt} would become
606 * {@code /after/foo/bar.txt}.
607 */
608 public static File rewriteAfterRename(File beforeDir, File afterDir, File file) {
Jeff Sharkey41be35d2015-05-13 12:38:16 -0700609 if (file == null || beforeDir == null || afterDir == null) return null;
Jeff Sharkey57dcf5b2014-06-18 17:46:05 -0700610 if (contains(beforeDir, file)) {
611 final String splice = file.getAbsolutePath().substring(
612 beforeDir.getAbsolutePath().length());
613 return new File(afterDir, splice);
614 }
615 return null;
616 }
Ben Kwa62539a22015-04-22 15:43:17 -0700617
Jeff Sharkeyaa444762016-09-20 18:54:46 -0600618 private static File buildUniqueFileWithExtension(File parent, String name, String ext)
619 throws FileNotFoundException {
620 File file = buildFile(parent, name, ext);
621
622 // If conflicting file, try adding counter suffix
623 int n = 0;
624 while (file.exists()) {
625 if (n++ >= 32) {
626 throw new FileNotFoundException("Failed to create unique file");
627 }
628 file = buildFile(parent, name + " (" + n + ")", ext);
629 }
630
631 return file;
632 }
633
Ben Kwa62539a22015-04-22 15:43:17 -0700634 /**
635 * Generates a unique file name under the given parent directory. If the display name doesn't
636 * have an extension that matches the requested MIME type, the default extension for that MIME
637 * type is appended. If a file already exists, the name is appended with a numerical value to
638 * make it unique.
639 *
640 * For example, the display name 'example' with 'text/plain' MIME might produce
641 * 'example.txt' or 'example (1).txt', etc.
642 *
643 * @throws FileNotFoundException
644 */
645 public static File buildUniqueFile(File parent, String mimeType, String displayName)
646 throws FileNotFoundException {
Daichi Hironofc7fb752016-03-15 19:19:31 +0900647 final String[] parts = splitFileName(mimeType, displayName);
Jeff Sharkeyaa444762016-09-20 18:54:46 -0600648 return buildUniqueFileWithExtension(parent, parts[0], parts[1]);
649 }
Daichi Hironofc7fb752016-03-15 19:19:31 +0900650
Jeff Sharkeyaa444762016-09-20 18:54:46 -0600651 /**
652 * Generates a unique file name under the given parent directory, keeping
653 * any extension intact.
654 */
655 public static File buildUniqueFile(File parent, String displayName)
656 throws FileNotFoundException {
657 final String name;
658 final String ext;
659
660 // Extract requested extension from display name
661 final int lastDot = displayName.lastIndexOf('.');
662 if (lastDot >= 0) {
663 name = displayName.substring(0, lastDot);
664 ext = displayName.substring(lastDot + 1);
665 } else {
666 name = displayName;
667 ext = null;
Daichi Hironofc7fb752016-03-15 19:19:31 +0900668 }
669
Jeff Sharkeyaa444762016-09-20 18:54:46 -0600670 return buildUniqueFileWithExtension(parent, name, ext);
Daichi Hironofc7fb752016-03-15 19:19:31 +0900671 }
672
673 /**
674 * Splits file name into base name and extension.
675 * If the display name doesn't have an extension that matches the requested MIME type, the
676 * extension is regarded as a part of filename and default extension for that MIME type is
677 * appended.
678 */
679 public static String[] splitFileName(String mimeType, String displayName) {
Ben Kwa62539a22015-04-22 15:43:17 -0700680 String name;
681 String ext;
682
683 if (Document.MIME_TYPE_DIR.equals(mimeType)) {
684 name = displayName;
685 ext = null;
686 } else {
687 String mimeTypeFromExt;
688
689 // Extract requested extension from display name
690 final int lastDot = displayName.lastIndexOf('.');
691 if (lastDot >= 0) {
692 name = displayName.substring(0, lastDot);
693 ext = displayName.substring(lastDot + 1);
694 mimeTypeFromExt = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
695 ext.toLowerCase());
696 } else {
697 name = displayName;
698 ext = null;
699 mimeTypeFromExt = null;
700 }
701
702 if (mimeTypeFromExt == null) {
703 mimeTypeFromExt = "application/octet-stream";
704 }
705
706 final String extFromMimeType = MimeTypeMap.getSingleton().getExtensionFromMimeType(
707 mimeType);
708 if (Objects.equals(mimeType, mimeTypeFromExt) || Objects.equals(ext, extFromMimeType)) {
709 // Extension maps back to requested MIME type; allow it
710 } else {
711 // No match; insist that create file matches requested MIME
712 name = displayName;
713 ext = extFromMimeType;
714 }
715 }
716
Daichi Hironofc7fb752016-03-15 19:19:31 +0900717 if (ext == null) {
718 ext = "";
Ben Kwa62539a22015-04-22 15:43:17 -0700719 }
720
Daichi Hironofc7fb752016-03-15 19:19:31 +0900721 return new String[] { name, ext };
Ben Kwa62539a22015-04-22 15:43:17 -0700722 }
723
724 private static File buildFile(File parent, String name, String ext) {
725 if (TextUtils.isEmpty(ext)) {
726 return new File(parent, name);
727 } else {
728 return new File(parent, name + "." + ext);
729 }
730 }
Jeff Sharkey85ced632015-07-22 11:11:46 -0700731
Jeff Sharkeyc4bab982016-02-01 10:16:01 -0700732 public static @NonNull String[] listOrEmpty(@Nullable File dir) {
733 if (dir == null) return EmptyArray.STRING;
734 final String[] res = dir.list();
735 if (res != null) {
736 return res;
737 } else {
738 return EmptyArray.STRING;
739 }
740 }
741
742 public static @NonNull File[] listFilesOrEmpty(@Nullable File dir) {
743 if (dir == null) return EMPTY;
744 final File[] res = dir.listFiles();
Jeff Sharkey85ced632015-07-22 11:11:46 -0700745 if (res != null) {
746 return res;
747 } else {
748 return EMPTY;
749 }
750 }
Jeff Sharkey15447792015-11-05 16:18:51 -0800751
Jeff Sharkeyc4bab982016-02-01 10:16:01 -0700752 public static @NonNull File[] listFilesOrEmpty(@Nullable File dir, FilenameFilter filter) {
753 if (dir == null) return EMPTY;
754 final File[] res = dir.listFiles(filter);
Jeff Sharkey35871f22016-01-29 17:13:29 -0700755 if (res != null) {
756 return res;
757 } else {
758 return EMPTY;
759 }
760 }
761
Jeff Sharkey15447792015-11-05 16:18:51 -0800762 public static @Nullable File newFileOrNull(@Nullable String path) {
763 return (path != null) ? new File(path) : null;
764 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800765}