blob: 7c015deae8f5a0eba876c772a976e35ebb0b5406 [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;
Jeff Sharkey35871f22016-01-29 17:13:29 -070041import java.io.FilenameFilter;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080042import java.io.IOException;
43import java.io.InputStream;
Jeff Sharkey4f5e8b32015-06-11 19:13:37 -070044import java.nio.charset.StandardCharsets;
Jeff Sharkeyd9526902013-03-14 14:11:57 -070045import java.util.Arrays;
46import java.util.Comparator;
Ben Kwa62539a22015-04-22 15:43:17 -070047import java.util.Objects;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080048import java.util.regex.Pattern;
Wink Saville6d25a992011-06-03 17:03:51 -070049import java.util.zip.CRC32;
50import java.util.zip.CheckedInputStream;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080051
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080052/**
53 * Tools for managing files. Not for public consumption.
54 * @hide
55 */
Wink Saville6d25a992011-06-03 17:03:51 -070056public class FileUtils {
Jeff Sharkeyd9526902013-03-14 14:11:57 -070057 private static final String TAG = "FileUtils";
58
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080059 public static final int S_IRWXU = 00700;
60 public static final int S_IRUSR = 00400;
61 public static final int S_IWUSR = 00200;
62 public static final int S_IXUSR = 00100;
63
64 public static final int S_IRWXG = 00070;
65 public static final int S_IRGRP = 00040;
66 public static final int S_IWGRP = 00020;
67 public static final int S_IXGRP = 00010;
68
69 public static final int S_IRWXO = 00007;
70 public static final int S_IROTH = 00004;
71 public static final int S_IWOTH = 00002;
72 public static final int S_IXOTH = 00001;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080073
Andreas Gampe0693fd82016-04-28 19:33:05 -070074 /** Regular expression for safe filenames: no spaces or metacharacters.
75 *
76 * Use a preload holder so that FileUtils can be compile-time initialized.
77 */
78 private static class NoImagePreloadHolder {
79 public static final Pattern SAFE_FILENAME_PATTERN = Pattern.compile("[\\w%+,./=_-]+");
80 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080081
Jeff Sharkey85ced632015-07-22 11:11:46 -070082 private static final File[] EMPTY = new File[0];
83
Jeff Sharkey184a0102013-07-10 16:19:52 -070084 /**
85 * Set owner and mode of of given {@link File}.
86 *
87 * @param mode to apply through {@code chmod}
88 * @param uid to apply through {@code chown}, or -1 to leave unchanged
89 * @param gid to apply through {@code chown}, or -1 to leave unchanged
90 * @return 0 on success, otherwise errno.
91 */
92 public static int setPermissions(File path, int mode, int uid, int gid) {
93 return setPermissions(path.getAbsolutePath(), mode, uid, gid);
94 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080095
Jeff Sharkey184a0102013-07-10 16:19:52 -070096 /**
97 * Set owner and mode of of given path.
98 *
99 * @param mode to apply through {@code chmod}
100 * @param uid to apply through {@code chown}, or -1 to leave unchanged
101 * @param gid to apply through {@code chown}, or -1 to leave unchanged
102 * @return 0 on success, otherwise errno.
103 */
104 public static int setPermissions(String path, int mode, int uid, int gid) {
105 try {
Elliott Hughes34385d32014-04-28 11:11:32 -0700106 Os.chmod(path, mode);
Jeff Sharkey184a0102013-07-10 16:19:52 -0700107 } catch (ErrnoException e) {
108 Slog.w(TAG, "Failed to chmod(" + path + "): " + e);
109 return e.errno;
110 }
111
112 if (uid >= 0 || gid >= 0) {
113 try {
Elliott Hughes34385d32014-04-28 11:11:32 -0700114 Os.chown(path, uid, gid);
Jeff Sharkey184a0102013-07-10 16:19:52 -0700115 } catch (ErrnoException e) {
116 Slog.w(TAG, "Failed to chown(" + path + "): " + e);
117 return e.errno;
118 }
119 }
120
121 return 0;
122 }
123
124 /**
125 * Set owner and mode of of given {@link FileDescriptor}.
126 *
127 * @param mode to apply through {@code chmod}
128 * @param uid to apply through {@code chown}, or -1 to leave unchanged
129 * @param gid to apply through {@code chown}, or -1 to leave unchanged
130 * @return 0 on success, otherwise errno.
131 */
132 public static int setPermissions(FileDescriptor fd, int mode, int uid, int gid) {
133 try {
Elliott Hughes34385d32014-04-28 11:11:32 -0700134 Os.fchmod(fd, mode);
Jeff Sharkey184a0102013-07-10 16:19:52 -0700135 } catch (ErrnoException e) {
136 Slog.w(TAG, "Failed to fchmod(): " + e);
137 return e.errno;
138 }
139
140 if (uid >= 0 || gid >= 0) {
141 try {
Elliott Hughes34385d32014-04-28 11:11:32 -0700142 Os.fchown(fd, uid, gid);
Jeff Sharkey184a0102013-07-10 16:19:52 -0700143 } catch (ErrnoException e) {
144 Slog.w(TAG, "Failed to fchown(): " + e);
145 return e.errno;
146 }
147 }
148
149 return 0;
150 }
151
Jeff Sharkey35871f22016-01-29 17:13:29 -0700152 public static void copyPermissions(File from, File to) throws IOException {
153 try {
154 final StructStat stat = Os.stat(from.getAbsolutePath());
155 Os.chmod(to.getAbsolutePath(), stat.st_mode);
156 Os.chown(to.getAbsolutePath(), stat.st_uid, stat.st_gid);
157 } catch (ErrnoException e) {
158 throw e.rethrowAsIOException();
159 }
160 }
161
Jeff Sharkey184a0102013-07-10 16:19:52 -0700162 /**
163 * Return owning UID of given path, otherwise -1.
164 */
165 public static int getUid(String path) {
166 try {
Elliott Hughes34385d32014-04-28 11:11:32 -0700167 return Os.stat(path).st_uid;
Jeff Sharkey184a0102013-07-10 16:19:52 -0700168 } catch (ErrnoException e) {
169 return -1;
170 }
171 }
Dianne Hackborn053f61d2013-06-26 18:07:43 -0700172
Dianne Hackborn8bdf5932010-10-15 12:54:40 -0700173 /**
174 * Perform an fsync on the given FileOutputStream. The stream at this
175 * point must be flushed but not yet closed.
176 */
177 public static boolean sync(FileOutputStream stream) {
178 try {
179 if (stream != null) {
180 stream.getFD().sync();
181 }
182 return true;
183 } catch (IOException e) {
184 }
185 return false;
186 }
187
Jeff Sharkey35871f22016-01-29 17:13:29 -0700188 @Deprecated
189 public static boolean copyFile(File srcFile, File destFile) {
190 try {
191 copyFileOrThrow(srcFile, destFile);
192 return true;
193 } catch (IOException e) {
194 return false;
195 }
196 }
197
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800198 // copy a file from srcFile to destFile, return true if succeed, return
199 // false if fail
Jeff Sharkey35871f22016-01-29 17:13:29 -0700200 public static void copyFileOrThrow(File srcFile, File destFile) throws IOException {
201 try (InputStream in = new FileInputStream(srcFile)) {
202 copyToFileOrThrow(in, destFile);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800203 }
Jeff Sharkey35871f22016-01-29 17:13:29 -0700204 }
205
206 @Deprecated
207 public static boolean copyToFile(InputStream inputStream, File destFile) {
208 try {
209 copyToFileOrThrow(inputStream, destFile);
210 return true;
211 } catch (IOException e) {
212 return false;
213 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800214 }
Guang Zhu90619812012-10-12 15:50:44 -0700215
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800216 /**
217 * Copy data from a source stream to destFile.
218 * Return true if succeed, return false if failed.
219 */
Jeff Sharkey35871f22016-01-29 17:13:29 -0700220 public static void copyToFileOrThrow(InputStream inputStream, File destFile)
221 throws IOException {
222 if (destFile.exists()) {
223 destFile.delete();
224 }
225 FileOutputStream out = new FileOutputStream(destFile);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800226 try {
Jeff Sharkey35871f22016-01-29 17:13:29 -0700227 byte[] buffer = new byte[4096];
228 int bytesRead;
229 while ((bytesRead = inputStream.read(buffer)) >= 0) {
230 out.write(buffer, 0, bytesRead);
Dianne Hackborn1afd1c92010-03-18 22:47:17 -0700231 }
Jeff Sharkey35871f22016-01-29 17:13:29 -0700232 } finally {
233 out.flush();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800234 try {
Jeff Sharkey35871f22016-01-29 17:13:29 -0700235 out.getFD().sync();
236 } catch (IOException e) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800237 }
Jeff Sharkey35871f22016-01-29 17:13:29 -0700238 out.close();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800239 }
240 }
241
242 /**
243 * Check if a filename is "safe" (no metacharacters or spaces).
244 * @param file The file to check
245 */
246 public static boolean isFilenameSafe(File file) {
247 // Note, we check whether it matches what's known to be safe,
248 // rather than what's known to be unsafe. Non-ASCII, control
249 // characters, etc. are all unsafe by default.
Andreas Gampe0693fd82016-04-28 19:33:05 -0700250 return NoImagePreloadHolder.SAFE_FILENAME_PATTERN.matcher(file.getPath()).matches();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800251 }
252
253 /**
254 * Read a text file into a String, optionally limiting the length.
255 * @param file to read (will not seek, so things like /proc files are OK)
256 * @param max length (positive for head, negative of tail, 0 for no limit)
257 * @param ellipsis to add of the file was truncated (can be null)
258 * @return the contents of the file, possibly truncated
259 * @throws IOException if something goes wrong reading the file
260 */
261 public static String readTextFile(File file, int max, String ellipsis) throws IOException {
262 InputStream input = new FileInputStream(file);
Guang Zhu90619812012-10-12 15:50:44 -0700263 // wrapping a BufferedInputStream around it because when reading /proc with unbuffered
264 // input stream, bytes read not equal to buffer size is not necessarily the correct
265 // indication for EOF; but it is true for BufferedInputStream due to its implementation.
266 BufferedInputStream bis = new BufferedInputStream(input);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800267 try {
Dan Egnor42471dd2010-01-07 17:25:22 -0800268 long size = file.length();
269 if (max > 0 || (size > 0 && max == 0)) { // "head" mode: read the first N bytes
270 if (size > 0 && (max == 0 || size < max)) max = (int) size;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800271 byte[] data = new byte[max + 1];
Guang Zhu90619812012-10-12 15:50:44 -0700272 int length = bis.read(data);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800273 if (length <= 0) return "";
274 if (length <= max) return new String(data, 0, length);
275 if (ellipsis == null) return new String(data, 0, max);
276 return new String(data, 0, max) + ellipsis;
Dan Egnor42471dd2010-01-07 17:25:22 -0800277 } else if (max < 0) { // "tail" mode: keep the last N
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800278 int len;
279 boolean rolled = false;
Jeff Sharkeyd9526902013-03-14 14:11:57 -0700280 byte[] last = null;
281 byte[] data = null;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800282 do {
283 if (last != null) rolled = true;
284 byte[] tmp = last; last = data; data = tmp;
285 if (data == null) data = new byte[-max];
Guang Zhu90619812012-10-12 15:50:44 -0700286 len = bis.read(data);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800287 } while (len == data.length);
288
289 if (last == null && len <= 0) return "";
290 if (last == null) return new String(data, 0, len);
291 if (len > 0) {
292 rolled = true;
293 System.arraycopy(last, len, last, 0, last.length - len);
294 System.arraycopy(data, 0, last, last.length - len, len);
295 }
296 if (ellipsis == null || !rolled) return new String(last);
297 return ellipsis + new String(last);
Dan Egnor42471dd2010-01-07 17:25:22 -0800298 } else { // "cat" mode: size unknown, read it all in streaming fashion
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800299 ByteArrayOutputStream contents = new ByteArrayOutputStream();
300 int len;
301 byte[] data = new byte[1024];
302 do {
Guang Zhu90619812012-10-12 15:50:44 -0700303 len = bis.read(data);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800304 if (len > 0) contents.write(data, 0, len);
305 } while (len == data.length);
306 return contents.toString();
307 }
308 } finally {
Guang Zhu90619812012-10-12 15:50:44 -0700309 bis.close();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800310 input.close();
311 }
312 }
Mike Lockwoodda8bb742011-05-28 13:24:04 -0400313
Jeff Sharkey2271ba32016-02-01 17:57:08 -0700314 public static void stringToFile(File file, String string) throws IOException {
315 stringToFile(file.getAbsolutePath(), string);
316 }
317
Narayan Kamath6d051fc2016-11-22 20:20:00 +0000318 /*
319 * Writes the bytes given in {@code content} to the file whose absolute path
320 * is {@code filename}.
321 */
322 public static void bytesToFile(String filename, byte[] content) throws IOException {
323 try (FileOutputStream fos = new FileOutputStream(filename)) {
324 fos.write(content);
325 }
326 }
327
Jeff Sharkey2271ba32016-02-01 17:57:08 -0700328 /**
Mike Lockwoodda8bb742011-05-28 13:24:04 -0400329 * Writes string to file. Basically same as "echo -n $string > $filename"
330 *
331 * @param filename
332 * @param string
333 * @throws IOException
334 */
335 public static void stringToFile(String filename, String string) throws IOException {
Jeff Sharkey032c08a2017-01-19 17:05:10 -0700336 bytesToFile(filename, string.getBytes(StandardCharsets.UTF_8));
Mike Lockwoodda8bb742011-05-28 13:24:04 -0400337 }
Wink Saville1b9a6a62011-06-04 07:31:35 -0700338
Wink Saville6d25a992011-06-03 17:03:51 -0700339 /**
340 * Computes the checksum of a file using the CRC32 checksum routine.
341 * The value of the checksum is returned.
342 *
343 * @param file the file to checksum, must not be null
344 * @return the checksum value or an exception is thrown.
345 */
346 public static long checksumCrc32(File file) throws FileNotFoundException, IOException {
347 CRC32 checkSummer = new CRC32();
348 CheckedInputStream cis = null;
349
350 try {
351 cis = new CheckedInputStream( new FileInputStream(file), checkSummer);
352 byte[] buf = new byte[128];
353 while(cis.read(buf) >= 0) {
354 // Just read for checksum to get calculated.
355 }
356 return checkSummer.getValue();
357 } finally {
358 if (cis != null) {
359 try {
360 cis.close();
361 } catch (IOException e) {
362 }
363 }
364 }
365 }
Jeff Sharkeyd9526902013-03-14 14:11:57 -0700366
367 /**
368 * Delete older files in a directory until only those matching the given
369 * constraints remain.
370 *
371 * @param minCount Always keep at least this many files.
372 * @param minAge Always keep files younger than this age.
Jeff Sharkeyebf8ad52014-01-30 15:01:22 -0800373 * @return if any files were deleted.
Jeff Sharkeyd9526902013-03-14 14:11:57 -0700374 */
Jeff Sharkeyebf8ad52014-01-30 15:01:22 -0800375 public static boolean deleteOlderFiles(File dir, int minCount, long minAge) {
Jeff Sharkeyd9526902013-03-14 14:11:57 -0700376 if (minCount < 0 || minAge < 0) {
377 throw new IllegalArgumentException("Constraints must be positive or 0");
378 }
379
380 final File[] files = dir.listFiles();
Jeff Sharkeyebf8ad52014-01-30 15:01:22 -0800381 if (files == null) return false;
Jeff Sharkeyd9526902013-03-14 14:11:57 -0700382
383 // Sort with newest files first
384 Arrays.sort(files, new Comparator<File>() {
385 @Override
386 public int compare(File lhs, File rhs) {
Ian Rogers660e6de2016-05-17 11:50:54 -0700387 return Long.compare(rhs.lastModified(), lhs.lastModified());
Jeff Sharkeyd9526902013-03-14 14:11:57 -0700388 }
389 });
390
391 // Keep at least minCount files
Jeff Sharkeyebf8ad52014-01-30 15:01:22 -0800392 boolean deleted = false;
Jeff Sharkeyd9526902013-03-14 14:11:57 -0700393 for (int i = minCount; i < files.length; i++) {
394 final File file = files[i];
395
396 // Keep files newer than minAge
397 final long age = System.currentTimeMillis() - file.lastModified();
398 if (age > minAge) {
Jeff Sharkeyebf8ad52014-01-30 15:01:22 -0800399 if (file.delete()) {
400 Log.d(TAG, "Deleted old file " + file);
401 deleted = true;
402 }
Jeff Sharkeyd9526902013-03-14 14:11:57 -0700403 }
404 }
Jeff Sharkeyebf8ad52014-01-30 15:01:22 -0800405 return deleted;
Jeff Sharkeyd9526902013-03-14 14:11:57 -0700406 }
Jeff Sharkey4ca728c2014-01-10 16:27:19 -0800407
408 /**
409 * Test if a file lives under the given directory, either as a direct child
410 * or a distant grandchild.
411 * <p>
412 * Both files <em>must</em> have been resolved using
413 * {@link File#getCanonicalFile()} to avoid symlink or path traversal
414 * attacks.
415 */
Jeff Sharkey48877892015-03-18 11:27:19 -0700416 public static boolean contains(File[] dirs, File file) {
417 for (File dir : dirs) {
418 if (contains(dir, file)) {
419 return true;
420 }
421 }
422 return false;
423 }
424
425 /**
426 * Test if a file lives under the given directory, either as a direct child
427 * or a distant grandchild.
428 * <p>
429 * Both files <em>must</em> have been resolved using
430 * {@link File#getCanonicalFile()} to avoid symlink or path traversal
431 * attacks.
432 */
Jeff Sharkey4ca728c2014-01-10 16:27:19 -0800433 public static boolean contains(File dir, File file) {
Jeff Sharkey50a05452015-04-29 11:24:52 -0700434 if (dir == null || file == null) return false;
Jeff Sharkeyd7460572014-07-06 20:44:55 -0700435
Jeff Sharkey21de56a2014-04-05 19:05:24 -0700436 String dirPath = dir.getAbsolutePath();
437 String filePath = file.getAbsolutePath();
Jeff Sharkey4ca728c2014-01-10 16:27:19 -0800438
439 if (dirPath.equals(filePath)) {
440 return true;
441 }
442
443 if (!dirPath.endsWith("/")) {
444 dirPath += "/";
445 }
446 return filePath.startsWith(dirPath);
447 }
Jeff Sharkey3a44f3f2014-04-28 17:36:31 -0700448
Jeff Sharkeyfcf1e552016-04-14 20:44:58 -0600449 public static boolean deleteContentsAndDir(File dir) {
450 if (deleteContents(dir)) {
451 return dir.delete();
452 } else {
453 return false;
454 }
455 }
456
Jeff Sharkey57dcf5b2014-06-18 17:46:05 -0700457 public static boolean deleteContents(File dir) {
Jeff Sharkey3a44f3f2014-04-28 17:36:31 -0700458 File[] files = dir.listFiles();
Jeff Sharkey57dcf5b2014-06-18 17:46:05 -0700459 boolean success = true;
Jeff Sharkey3a44f3f2014-04-28 17:36:31 -0700460 if (files != null) {
461 for (File file : files) {
462 if (file.isDirectory()) {
Jeff Sharkey57dcf5b2014-06-18 17:46:05 -0700463 success &= deleteContents(file);
Jeff Sharkey3a44f3f2014-04-28 17:36:31 -0700464 }
Jeff Sharkey73767b92014-07-04 20:18:13 -0700465 if (!file.delete()) {
466 Log.w(TAG, "Failed to delete " + file);
467 success = false;
468 }
Jeff Sharkey3a44f3f2014-04-28 17:36:31 -0700469 }
470 }
Jeff Sharkey57dcf5b2014-06-18 17:46:05 -0700471 return success;
Jeff Sharkey3a44f3f2014-04-28 17:36:31 -0700472 }
473
Jeff Sharkey0cce5352014-11-26 13:38:26 -0800474 private static boolean isValidExtFilenameChar(char c) {
475 switch (c) {
476 case '\0':
477 case '/':
478 return false;
479 default:
480 return true;
481 }
482 }
483
Jeff Sharkey3a44f3f2014-04-28 17:36:31 -0700484 /**
Jeff Sharkey0cce5352014-11-26 13:38:26 -0800485 * Check if given filename is valid for an ext4 filesystem.
Jeff Sharkey3a44f3f2014-04-28 17:36:31 -0700486 */
487 public static boolean isValidExtFilename(String name) {
Jeff Sharkey0cce5352014-11-26 13:38:26 -0800488 return (name != null) && name.equals(buildValidExtFilename(name));
489 }
490
491 /**
492 * Mutate the given filename to make it valid for an ext4 filesystem,
493 * replacing any invalid characters with "_".
494 */
495 public static String buildValidExtFilename(String name) {
Jeff Sharkey3a44f3f2014-04-28 17:36:31 -0700496 if (TextUtils.isEmpty(name) || ".".equals(name) || "..".equals(name)) {
Jeff Sharkey0cce5352014-11-26 13:38:26 -0800497 return "(invalid)";
Jeff Sharkey3a44f3f2014-04-28 17:36:31 -0700498 }
Jeff Sharkey0cce5352014-11-26 13:38:26 -0800499 final StringBuilder res = new StringBuilder(name.length());
Jeff Sharkey3a44f3f2014-04-28 17:36:31 -0700500 for (int i = 0; i < name.length(); i++) {
501 final char c = name.charAt(i);
Jeff Sharkey0cce5352014-11-26 13:38:26 -0800502 if (isValidExtFilenameChar(c)) {
503 res.append(c);
504 } else {
505 res.append('_');
Jeff Sharkey3a44f3f2014-04-28 17:36:31 -0700506 }
507 }
Jeff Sharkey4f5e8b32015-06-11 19:13:37 -0700508 trimFilename(res, 255);
Jeff Sharkey0cce5352014-11-26 13:38:26 -0800509 return res.toString();
510 }
511
512 private static boolean isValidFatFilenameChar(char c) {
513 if ((0x00 <= c && c <= 0x1f)) {
514 return false;
515 }
516 switch (c) {
517 case '"':
518 case '*':
519 case '/':
520 case ':':
521 case '<':
522 case '>':
523 case '?':
524 case '\\':
525 case '|':
526 case 0x7F:
527 return false;
528 default:
529 return true;
530 }
531 }
532
533 /**
534 * Check if given filename is valid for a FAT filesystem.
535 */
536 public static boolean isValidFatFilename(String name) {
537 return (name != null) && name.equals(buildValidFatFilename(name));
538 }
539
540 /**
541 * Mutate the given filename to make it valid for a FAT filesystem,
542 * replacing any invalid characters with "_".
543 */
544 public static String buildValidFatFilename(String name) {
545 if (TextUtils.isEmpty(name) || ".".equals(name) || "..".equals(name)) {
546 return "(invalid)";
547 }
548 final StringBuilder res = new StringBuilder(name.length());
549 for (int i = 0; i < name.length(); i++) {
550 final char c = name.charAt(i);
551 if (isValidFatFilenameChar(c)) {
552 res.append(c);
553 } else {
554 res.append('_');
555 }
556 }
Jeff Sharkey4f5e8b32015-06-11 19:13:37 -0700557 // Even though vfat allows 255 UCS-2 chars, we might eventually write to
558 // ext4 through a FUSE layer, so use that limit.
559 trimFilename(res, 255);
Jeff Sharkey0cce5352014-11-26 13:38:26 -0800560 return res.toString();
Jeff Sharkey3a44f3f2014-04-28 17:36:31 -0700561 }
Jeff Sharkey57dcf5b2014-06-18 17:46:05 -0700562
Jeff Sharkey4f5e8b32015-06-11 19:13:37 -0700563 @VisibleForTesting
564 public static String trimFilename(String str, int maxBytes) {
565 final StringBuilder res = new StringBuilder(str);
566 trimFilename(res, maxBytes);
567 return res.toString();
568 }
569
570 private static void trimFilename(StringBuilder res, int maxBytes) {
571 byte[] raw = res.toString().getBytes(StandardCharsets.UTF_8);
572 if (raw.length > maxBytes) {
573 maxBytes -= 3;
574 while (raw.length > maxBytes) {
575 res.deleteCharAt(res.length() / 2);
576 raw = res.toString().getBytes(StandardCharsets.UTF_8);
577 }
578 res.insert(res.length() / 2, "...");
579 }
580 }
581
Jeff Sharkey57dcf5b2014-06-18 17:46:05 -0700582 public static String rewriteAfterRename(File beforeDir, File afterDir, String path) {
Jeff Sharkeyd7460572014-07-06 20:44:55 -0700583 if (path == null) return null;
Jeff Sharkey57dcf5b2014-06-18 17:46:05 -0700584 final File result = rewriteAfterRename(beforeDir, afterDir, new File(path));
585 return (result != null) ? result.getAbsolutePath() : null;
586 }
587
Jeff Sharkeyd7460572014-07-06 20:44:55 -0700588 public static String[] rewriteAfterRename(File beforeDir, File afterDir, String[] paths) {
589 if (paths == null) return null;
590 final String[] result = new String[paths.length];
591 for (int i = 0; i < paths.length; i++) {
592 result[i] = rewriteAfterRename(beforeDir, afterDir, paths[i]);
593 }
594 return result;
595 }
596
Jeff Sharkey57dcf5b2014-06-18 17:46:05 -0700597 /**
598 * Given a path under the "before" directory, rewrite it to live under the
599 * "after" directory. For example, {@code /before/foo/bar.txt} would become
600 * {@code /after/foo/bar.txt}.
601 */
602 public static File rewriteAfterRename(File beforeDir, File afterDir, File file) {
Jeff Sharkey41be35d2015-05-13 12:38:16 -0700603 if (file == null || beforeDir == null || afterDir == null) return null;
Jeff Sharkey57dcf5b2014-06-18 17:46:05 -0700604 if (contains(beforeDir, file)) {
605 final String splice = file.getAbsolutePath().substring(
606 beforeDir.getAbsolutePath().length());
607 return new File(afterDir, splice);
608 }
609 return null;
610 }
Ben Kwa62539a22015-04-22 15:43:17 -0700611
Jeff Sharkeyaa444762016-09-20 18:54:46 -0600612 private static File buildUniqueFileWithExtension(File parent, String name, String ext)
613 throws FileNotFoundException {
614 File file = buildFile(parent, name, ext);
615
616 // If conflicting file, try adding counter suffix
617 int n = 0;
618 while (file.exists()) {
619 if (n++ >= 32) {
620 throw new FileNotFoundException("Failed to create unique file");
621 }
622 file = buildFile(parent, name + " (" + n + ")", ext);
623 }
624
625 return file;
626 }
627
Ben Kwa62539a22015-04-22 15:43:17 -0700628 /**
629 * Generates a unique file name under the given parent directory. If the display name doesn't
630 * have an extension that matches the requested MIME type, the default extension for that MIME
631 * type is appended. If a file already exists, the name is appended with a numerical value to
632 * make it unique.
633 *
634 * For example, the display name 'example' with 'text/plain' MIME might produce
635 * 'example.txt' or 'example (1).txt', etc.
636 *
637 * @throws FileNotFoundException
638 */
639 public static File buildUniqueFile(File parent, String mimeType, String displayName)
640 throws FileNotFoundException {
Daichi Hironofc7fb752016-03-15 19:19:31 +0900641 final String[] parts = splitFileName(mimeType, displayName);
Jeff Sharkeyaa444762016-09-20 18:54:46 -0600642 return buildUniqueFileWithExtension(parent, parts[0], parts[1]);
643 }
Daichi Hironofc7fb752016-03-15 19:19:31 +0900644
Jeff Sharkeyaa444762016-09-20 18:54:46 -0600645 /**
646 * Generates a unique file name under the given parent directory, keeping
647 * any extension intact.
648 */
649 public static File buildUniqueFile(File parent, String displayName)
650 throws FileNotFoundException {
651 final String name;
652 final String ext;
653
654 // Extract requested extension from display name
655 final int lastDot = displayName.lastIndexOf('.');
656 if (lastDot >= 0) {
657 name = displayName.substring(0, lastDot);
658 ext = displayName.substring(lastDot + 1);
659 } else {
660 name = displayName;
661 ext = null;
Daichi Hironofc7fb752016-03-15 19:19:31 +0900662 }
663
Jeff Sharkeyaa444762016-09-20 18:54:46 -0600664 return buildUniqueFileWithExtension(parent, name, ext);
Daichi Hironofc7fb752016-03-15 19:19:31 +0900665 }
666
667 /**
668 * Splits file name into base name and extension.
669 * If the display name doesn't have an extension that matches the requested MIME type, the
670 * extension is regarded as a part of filename and default extension for that MIME type is
671 * appended.
672 */
673 public static String[] splitFileName(String mimeType, String displayName) {
Ben Kwa62539a22015-04-22 15:43:17 -0700674 String name;
675 String ext;
676
677 if (Document.MIME_TYPE_DIR.equals(mimeType)) {
678 name = displayName;
679 ext = null;
680 } else {
681 String mimeTypeFromExt;
682
683 // Extract requested extension from display name
684 final int lastDot = displayName.lastIndexOf('.');
685 if (lastDot >= 0) {
686 name = displayName.substring(0, lastDot);
687 ext = displayName.substring(lastDot + 1);
688 mimeTypeFromExt = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
689 ext.toLowerCase());
690 } else {
691 name = displayName;
692 ext = null;
693 mimeTypeFromExt = null;
694 }
695
696 if (mimeTypeFromExt == null) {
697 mimeTypeFromExt = "application/octet-stream";
698 }
699
700 final String extFromMimeType = MimeTypeMap.getSingleton().getExtensionFromMimeType(
701 mimeType);
702 if (Objects.equals(mimeType, mimeTypeFromExt) || Objects.equals(ext, extFromMimeType)) {
703 // Extension maps back to requested MIME type; allow it
704 } else {
705 // No match; insist that create file matches requested MIME
706 name = displayName;
707 ext = extFromMimeType;
708 }
709 }
710
Daichi Hironofc7fb752016-03-15 19:19:31 +0900711 if (ext == null) {
712 ext = "";
Ben Kwa62539a22015-04-22 15:43:17 -0700713 }
714
Daichi Hironofc7fb752016-03-15 19:19:31 +0900715 return new String[] { name, ext };
Ben Kwa62539a22015-04-22 15:43:17 -0700716 }
717
718 private static File buildFile(File parent, String name, String ext) {
719 if (TextUtils.isEmpty(ext)) {
720 return new File(parent, name);
721 } else {
722 return new File(parent, name + "." + ext);
723 }
724 }
Jeff Sharkey85ced632015-07-22 11:11:46 -0700725
Jeff Sharkeyc4bab982016-02-01 10:16:01 -0700726 public static @NonNull String[] listOrEmpty(@Nullable File dir) {
727 if (dir == null) return EmptyArray.STRING;
728 final String[] res = dir.list();
729 if (res != null) {
730 return res;
731 } else {
732 return EmptyArray.STRING;
733 }
734 }
735
736 public static @NonNull File[] listFilesOrEmpty(@Nullable File dir) {
737 if (dir == null) return EMPTY;
738 final File[] res = dir.listFiles();
Jeff Sharkey85ced632015-07-22 11:11:46 -0700739 if (res != null) {
740 return res;
741 } else {
742 return EMPTY;
743 }
744 }
Jeff Sharkey15447792015-11-05 16:18:51 -0800745
Jeff Sharkeyc4bab982016-02-01 10:16:01 -0700746 public static @NonNull File[] listFilesOrEmpty(@Nullable File dir, FilenameFilter filter) {
747 if (dir == null) return EMPTY;
748 final File[] res = dir.listFiles(filter);
Jeff Sharkey35871f22016-01-29 17:13:29 -0700749 if (res != null) {
750 return res;
751 } else {
752 return EMPTY;
753 }
754 }
755
Jeff Sharkey15447792015-11-05 16:18:51 -0800756 public static @Nullable File newFileOrNull(@Nullable String path) {
757 return (path != null) ? new File(path) : null;
758 }
Narayan Kamath5c50e862016-11-24 13:22:40 +0000759
760 /**
761 * Creates a directory with name {@code name} under an existing directory {@code baseDir}.
762 * Returns a {@code File} object representing the directory on success, {@code null} on
763 * failure.
764 */
765 public static @Nullable File createDir(File baseDir, String name) {
766 final File dir = new File(baseDir, name);
767
768 if (dir.exists()) {
769 return dir.isDirectory() ? dir : null;
770 }
771
772 return dir.mkdir() ? dir : null;
773 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800774}