1 /* 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 17 package android.os; 18 19 import static android.system.OsConstants.SPLICE_F_MORE; 20 import static android.system.OsConstants.SPLICE_F_MOVE; 21 import static android.system.OsConstants.S_ISFIFO; 22 import static android.system.OsConstants.S_ISREG; 23 24 import android.annotation.NonNull; 25 import android.annotation.Nullable; 26 import android.provider.DocumentsContract.Document; 27 import android.system.ErrnoException; 28 import android.system.Os; 29 import android.system.StructStat; 30 import android.text.TextUtils; 31 import android.util.Log; 32 import android.util.Slog; 33 import android.webkit.MimeTypeMap; 34 35 import com.android.internal.annotations.VisibleForTesting; 36 import com.android.internal.util.SizedInputStream; 37 38 import libcore.io.IoUtils; 39 import libcore.util.EmptyArray; 40 41 import java.io.BufferedInputStream; 42 import java.io.ByteArrayOutputStream; 43 import java.io.File; 44 import java.io.FileDescriptor; 45 import java.io.FileInputStream; 46 import java.io.FileNotFoundException; 47 import java.io.FileOutputStream; 48 import java.io.FilenameFilter; 49 import java.io.IOException; 50 import java.io.InputStream; 51 import java.io.OutputStream; 52 import java.nio.charset.StandardCharsets; 53 import java.util.Arrays; 54 import java.util.Comparator; 55 import java.util.Objects; 56 import java.util.concurrent.TimeUnit; 57 import java.util.regex.Pattern; 58 import java.util.zip.CRC32; 59 import java.util.zip.CheckedInputStream; 60 61 /** 62 * Tools for managing files. Not for public consumption. 63 * @hide 64 */ 65 public class FileUtils { 66 private static final String TAG = "FileUtils"; 67 68 public static final int S_IRWXU = 00700; 69 public static final int S_IRUSR = 00400; 70 public static final int S_IWUSR = 00200; 71 public static final int S_IXUSR = 00100; 72 73 public static final int S_IRWXG = 00070; 74 public static final int S_IRGRP = 00040; 75 public static final int S_IWGRP = 00020; 76 public static final int S_IXGRP = 00010; 77 78 public static final int S_IRWXO = 00007; 79 public static final int S_IROTH = 00004; 80 public static final int S_IWOTH = 00002; 81 public static final int S_IXOTH = 00001; 82 83 /** Regular expression for safe filenames: no spaces or metacharacters. 84 * 85 * Use a preload holder so that FileUtils can be compile-time initialized. 86 */ 87 private static class NoImagePreloadHolder { 88 public static final Pattern SAFE_FILENAME_PATTERN = Pattern.compile("[\\w%+,./=_-]+"); 89 } 90 91 private static final File[] EMPTY = new File[0]; 92 93 private static final boolean ENABLE_COPY_OPTIMIZATIONS = true; 94 95 private static final long COPY_CHECKPOINT_BYTES = 524288; 96 97 public interface ProgressListener { onProgress(long progress)98 public void onProgress(long progress); 99 } 100 101 /** 102 * Set owner and mode of of given {@link File}. 103 * 104 * @param mode to apply through {@code chmod} 105 * @param uid to apply through {@code chown}, or -1 to leave unchanged 106 * @param gid to apply through {@code chown}, or -1 to leave unchanged 107 * @return 0 on success, otherwise errno. 108 */ setPermissions(File path, int mode, int uid, int gid)109 public static int setPermissions(File path, int mode, int uid, int gid) { 110 return setPermissions(path.getAbsolutePath(), mode, uid, gid); 111 } 112 113 /** 114 * Set owner and mode of of given path. 115 * 116 * @param mode to apply through {@code chmod} 117 * @param uid to apply through {@code chown}, or -1 to leave unchanged 118 * @param gid to apply through {@code chown}, or -1 to leave unchanged 119 * @return 0 on success, otherwise errno. 120 */ setPermissions(String path, int mode, int uid, int gid)121 public static int setPermissions(String path, int mode, int uid, int gid) { 122 try { 123 Os.chmod(path, mode); 124 } catch (ErrnoException e) { 125 Slog.w(TAG, "Failed to chmod(" + path + "): " + e); 126 return e.errno; 127 } 128 129 if (uid >= 0 || gid >= 0) { 130 try { 131 Os.chown(path, uid, gid); 132 } catch (ErrnoException e) { 133 Slog.w(TAG, "Failed to chown(" + path + "): " + e); 134 return e.errno; 135 } 136 } 137 138 return 0; 139 } 140 141 /** 142 * Set owner and mode of of given {@link FileDescriptor}. 143 * 144 * @param mode to apply through {@code chmod} 145 * @param uid to apply through {@code chown}, or -1 to leave unchanged 146 * @param gid to apply through {@code chown}, or -1 to leave unchanged 147 * @return 0 on success, otherwise errno. 148 */ setPermissions(FileDescriptor fd, int mode, int uid, int gid)149 public static int setPermissions(FileDescriptor fd, int mode, int uid, int gid) { 150 try { 151 Os.fchmod(fd, mode); 152 } catch (ErrnoException e) { 153 Slog.w(TAG, "Failed to fchmod(): " + e); 154 return e.errno; 155 } 156 157 if (uid >= 0 || gid >= 0) { 158 try { 159 Os.fchown(fd, uid, gid); 160 } catch (ErrnoException e) { 161 Slog.w(TAG, "Failed to fchown(): " + e); 162 return e.errno; 163 } 164 } 165 166 return 0; 167 } 168 copyPermissions(File from, File to)169 public static void copyPermissions(File from, File to) throws IOException { 170 try { 171 final StructStat stat = Os.stat(from.getAbsolutePath()); 172 Os.chmod(to.getAbsolutePath(), stat.st_mode); 173 Os.chown(to.getAbsolutePath(), stat.st_uid, stat.st_gid); 174 } catch (ErrnoException e) { 175 throw e.rethrowAsIOException(); 176 } 177 } 178 179 /** 180 * Return owning UID of given path, otherwise -1. 181 */ getUid(String path)182 public static int getUid(String path) { 183 try { 184 return Os.stat(path).st_uid; 185 } catch (ErrnoException e) { 186 return -1; 187 } 188 } 189 190 /** 191 * Perform an fsync on the given FileOutputStream. The stream at this 192 * point must be flushed but not yet closed. 193 */ sync(FileOutputStream stream)194 public static boolean sync(FileOutputStream stream) { 195 try { 196 if (stream != null) { 197 stream.getFD().sync(); 198 } 199 return true; 200 } catch (IOException e) { 201 } 202 return false; 203 } 204 205 /** 206 * @deprecated use {@link #copy(File, File)} instead. 207 */ 208 @Deprecated copyFile(File srcFile, File destFile)209 public static boolean copyFile(File srcFile, File destFile) { 210 try { 211 copyFileOrThrow(srcFile, destFile); 212 return true; 213 } catch (IOException e) { 214 return false; 215 } 216 } 217 218 /** 219 * @deprecated use {@link #copy(File, File)} instead. 220 */ 221 @Deprecated copyFileOrThrow(File srcFile, File destFile)222 public static void copyFileOrThrow(File srcFile, File destFile) throws IOException { 223 try (InputStream in = new FileInputStream(srcFile)) { 224 copyToFileOrThrow(in, destFile); 225 } 226 } 227 228 /** 229 * @deprecated use {@link #copy(InputStream, OutputStream)} instead. 230 */ 231 @Deprecated copyToFile(InputStream inputStream, File destFile)232 public static boolean copyToFile(InputStream inputStream, File destFile) { 233 try { 234 copyToFileOrThrow(inputStream, destFile); 235 return true; 236 } catch (IOException e) { 237 return false; 238 } 239 } 240 241 /** 242 * @deprecated use {@link #copy(InputStream, OutputStream)} instead. 243 */ 244 @Deprecated copyToFileOrThrow(InputStream in, File destFile)245 public static void copyToFileOrThrow(InputStream in, File destFile) throws IOException { 246 if (destFile.exists()) { 247 destFile.delete(); 248 } 249 try (FileOutputStream out = new FileOutputStream(destFile)) { 250 copy(in, out); 251 try { 252 Os.fsync(out.getFD()); 253 } catch (ErrnoException e) { 254 throw e.rethrowAsIOException(); 255 } 256 } 257 } 258 259 /** 260 * Copy the contents of one file to another, replacing any existing content. 261 * <p> 262 * Attempts to use several optimization strategies to copy the data in the 263 * kernel before falling back to a userspace copy as a last resort. 264 * 265 * @return number of bytes copied. 266 */ copy(@onNull File from, @NonNull File to)267 public static long copy(@NonNull File from, @NonNull File to) throws IOException { 268 return copy(from, to, null, null); 269 } 270 271 /** 272 * Copy the contents of one file to another, replacing any existing content. 273 * <p> 274 * Attempts to use several optimization strategies to copy the data in the 275 * kernel before falling back to a userspace copy as a last resort. 276 * 277 * @param listener to be periodically notified as the copy progresses. 278 * @param signal to signal if the copy should be cancelled early. 279 * @return number of bytes copied. 280 */ copy(@onNull File from, @NonNull File to, @Nullable ProgressListener listener, @Nullable CancellationSignal signal)281 public static long copy(@NonNull File from, @NonNull File to, 282 @Nullable ProgressListener listener, @Nullable CancellationSignal signal) 283 throws IOException { 284 try (FileInputStream in = new FileInputStream(from); 285 FileOutputStream out = new FileOutputStream(to)) { 286 return copy(in, out, listener, signal); 287 } 288 } 289 290 /** 291 * Copy the contents of one stream to another. 292 * <p> 293 * Attempts to use several optimization strategies to copy the data in the 294 * kernel before falling back to a userspace copy as a last resort. 295 * 296 * @return number of bytes copied. 297 */ copy(@onNull InputStream in, @NonNull OutputStream out)298 public static long copy(@NonNull InputStream in, @NonNull OutputStream out) throws IOException { 299 return copy(in, out, null, null); 300 } 301 302 /** 303 * Copy the contents of one stream to another. 304 * <p> 305 * Attempts to use several optimization strategies to copy the data in the 306 * kernel before falling back to a userspace copy as a last resort. 307 * 308 * @param listener to be periodically notified as the copy progresses. 309 * @param signal to signal if the copy should be cancelled early. 310 * @return number of bytes copied. 311 */ copy(@onNull InputStream in, @NonNull OutputStream out, @Nullable ProgressListener listener, @Nullable CancellationSignal signal)312 public static long copy(@NonNull InputStream in, @NonNull OutputStream out, 313 @Nullable ProgressListener listener, @Nullable CancellationSignal signal) 314 throws IOException { 315 if (ENABLE_COPY_OPTIMIZATIONS) { 316 if (in instanceof FileInputStream && out instanceof FileOutputStream) { 317 return copy(((FileInputStream) in).getFD(), ((FileOutputStream) out).getFD(), 318 listener, signal); 319 } 320 } 321 322 // Worse case fallback to userspace 323 return copyInternalUserspace(in, out, listener, signal); 324 } 325 326 /** 327 * Copy the contents of one FD to another. 328 * <p> 329 * Attempts to use several optimization strategies to copy the data in the 330 * kernel before falling back to a userspace copy as a last resort. 331 * 332 * @return number of bytes copied. 333 */ copy(@onNull FileDescriptor in, @NonNull FileDescriptor out)334 public static long copy(@NonNull FileDescriptor in, @NonNull FileDescriptor out) 335 throws IOException { 336 return copy(in, out, null, null); 337 } 338 339 /** 340 * Copy the contents of one FD to another. 341 * <p> 342 * Attempts to use several optimization strategies to copy the data in the 343 * kernel before falling back to a userspace copy as a last resort. 344 * 345 * @param listener to be periodically notified as the copy progresses. 346 * @param signal to signal if the copy should be cancelled early. 347 * @return number of bytes copied. 348 */ copy(@onNull FileDescriptor in, @NonNull FileDescriptor out, @Nullable ProgressListener listener, @Nullable CancellationSignal signal)349 public static long copy(@NonNull FileDescriptor in, @NonNull FileDescriptor out, 350 @Nullable ProgressListener listener, @Nullable CancellationSignal signal) 351 throws IOException { 352 return copy(in, out, listener, signal, Long.MAX_VALUE); 353 } 354 355 /** 356 * Copy the contents of one FD to another. 357 * <p> 358 * Attempts to use several optimization strategies to copy the data in the 359 * kernel before falling back to a userspace copy as a last resort. 360 * 361 * @param listener to be periodically notified as the copy progresses. 362 * @param signal to signal if the copy should be cancelled early. 363 * @param count the number of bytes to copy. 364 * @return number of bytes copied. 365 */ copy(@onNull FileDescriptor in, @NonNull FileDescriptor out, @Nullable ProgressListener listener, @Nullable CancellationSignal signal, long count)366 public static long copy(@NonNull FileDescriptor in, @NonNull FileDescriptor out, 367 @Nullable ProgressListener listener, @Nullable CancellationSignal signal, long count) 368 throws IOException { 369 if (ENABLE_COPY_OPTIMIZATIONS) { 370 try { 371 final StructStat st_in = Os.fstat(in); 372 final StructStat st_out = Os.fstat(out); 373 if (S_ISREG(st_in.st_mode) && S_ISREG(st_out.st_mode)) { 374 return copyInternalSendfile(in, out, listener, signal, count); 375 } else if (S_ISFIFO(st_in.st_mode) || S_ISFIFO(st_out.st_mode)) { 376 return copyInternalSplice(in, out, listener, signal, count); 377 } 378 } catch (ErrnoException e) { 379 throw e.rethrowAsIOException(); 380 } 381 } 382 383 // Worse case fallback to userspace 384 return copyInternalUserspace(in, out, listener, signal, count); 385 } 386 387 /** 388 * Requires one of input or output to be a pipe. 389 */ 390 @VisibleForTesting copyInternalSplice(FileDescriptor in, FileDescriptor out, ProgressListener listener, CancellationSignal signal, long count)391 public static long copyInternalSplice(FileDescriptor in, FileDescriptor out, 392 ProgressListener listener, CancellationSignal signal, long count) 393 throws ErrnoException { 394 long progress = 0; 395 long checkpoint = 0; 396 397 long t; 398 while ((t = Os.splice(in, null, out, null, Math.min(count, COPY_CHECKPOINT_BYTES), 399 SPLICE_F_MOVE | SPLICE_F_MORE)) != 0) { 400 progress += t; 401 checkpoint += t; 402 count -= t; 403 404 if (checkpoint >= COPY_CHECKPOINT_BYTES) { 405 if (signal != null) { 406 signal.throwIfCanceled(); 407 } 408 if (listener != null) { 409 listener.onProgress(progress); 410 } 411 checkpoint = 0; 412 } 413 } 414 if (listener != null) { 415 listener.onProgress(progress); 416 } 417 return progress; 418 } 419 420 /** 421 * Requires both input and output to be a regular file. 422 */ 423 @VisibleForTesting copyInternalSendfile(FileDescriptor in, FileDescriptor out, ProgressListener listener, CancellationSignal signal, long count)424 public static long copyInternalSendfile(FileDescriptor in, FileDescriptor out, 425 ProgressListener listener, CancellationSignal signal, long count) 426 throws ErrnoException { 427 long progress = 0; 428 long checkpoint = 0; 429 430 long t; 431 while ((t = Os.sendfile(out, in, null, Math.min(count, COPY_CHECKPOINT_BYTES))) != 0) { 432 progress += t; 433 checkpoint += t; 434 count -= t; 435 436 if (checkpoint >= COPY_CHECKPOINT_BYTES) { 437 if (signal != null) { 438 signal.throwIfCanceled(); 439 } 440 if (listener != null) { 441 listener.onProgress(progress); 442 } 443 checkpoint = 0; 444 } 445 } 446 if (listener != null) { 447 listener.onProgress(progress); 448 } 449 return progress; 450 } 451 452 @VisibleForTesting copyInternalUserspace(FileDescriptor in, FileDescriptor out, ProgressListener listener, CancellationSignal signal, long count)453 public static long copyInternalUserspace(FileDescriptor in, FileDescriptor out, 454 ProgressListener listener, CancellationSignal signal, long count) throws IOException { 455 if (count != Long.MAX_VALUE) { 456 return copyInternalUserspace(new SizedInputStream(new FileInputStream(in), count), 457 new FileOutputStream(out), listener, signal); 458 } else { 459 return copyInternalUserspace(new FileInputStream(in), 460 new FileOutputStream(out), listener, signal); 461 } 462 } 463 464 @VisibleForTesting copyInternalUserspace(InputStream in, OutputStream out, ProgressListener listener, CancellationSignal signal)465 public static long copyInternalUserspace(InputStream in, OutputStream out, 466 ProgressListener listener, CancellationSignal signal) throws IOException { 467 long progress = 0; 468 long checkpoint = 0; 469 byte[] buffer = new byte[8192]; 470 471 int t; 472 while ((t = in.read(buffer)) != -1) { 473 out.write(buffer, 0, t); 474 475 progress += t; 476 checkpoint += t; 477 478 if (checkpoint >= COPY_CHECKPOINT_BYTES) { 479 if (signal != null) { 480 signal.throwIfCanceled(); 481 } 482 if (listener != null) { 483 listener.onProgress(progress); 484 } 485 checkpoint = 0; 486 } 487 } 488 if (listener != null) { 489 listener.onProgress(progress); 490 } 491 return progress; 492 } 493 494 /** 495 * Check if a filename is "safe" (no metacharacters or spaces). 496 * @param file The file to check 497 */ isFilenameSafe(File file)498 public static boolean isFilenameSafe(File file) { 499 // Note, we check whether it matches what's known to be safe, 500 // rather than what's known to be unsafe. Non-ASCII, control 501 // characters, etc. are all unsafe by default. 502 return NoImagePreloadHolder.SAFE_FILENAME_PATTERN.matcher(file.getPath()).matches(); 503 } 504 505 /** 506 * Read a text file into a String, optionally limiting the length. 507 * @param file to read (will not seek, so things like /proc files are OK) 508 * @param max length (positive for head, negative of tail, 0 for no limit) 509 * @param ellipsis to add of the file was truncated (can be null) 510 * @return the contents of the file, possibly truncated 511 * @throws IOException if something goes wrong reading the file 512 */ readTextFile(File file, int max, String ellipsis)513 public static String readTextFile(File file, int max, String ellipsis) throws IOException { 514 InputStream input = new FileInputStream(file); 515 // wrapping a BufferedInputStream around it because when reading /proc with unbuffered 516 // input stream, bytes read not equal to buffer size is not necessarily the correct 517 // indication for EOF; but it is true for BufferedInputStream due to its implementation. 518 BufferedInputStream bis = new BufferedInputStream(input); 519 try { 520 long size = file.length(); 521 if (max > 0 || (size > 0 && max == 0)) { // "head" mode: read the first N bytes 522 if (size > 0 && (max == 0 || size < max)) max = (int) size; 523 byte[] data = new byte[max + 1]; 524 int length = bis.read(data); 525 if (length <= 0) return ""; 526 if (length <= max) return new String(data, 0, length); 527 if (ellipsis == null) return new String(data, 0, max); 528 return new String(data, 0, max) + ellipsis; 529 } else if (max < 0) { // "tail" mode: keep the last N 530 int len; 531 boolean rolled = false; 532 byte[] last = null; 533 byte[] data = null; 534 do { 535 if (last != null) rolled = true; 536 byte[] tmp = last; last = data; data = tmp; 537 if (data == null) data = new byte[-max]; 538 len = bis.read(data); 539 } while (len == data.length); 540 541 if (last == null && len <= 0) return ""; 542 if (last == null) return new String(data, 0, len); 543 if (len > 0) { 544 rolled = true; 545 System.arraycopy(last, len, last, 0, last.length - len); 546 System.arraycopy(data, 0, last, last.length - len, len); 547 } 548 if (ellipsis == null || !rolled) return new String(last); 549 return ellipsis + new String(last); 550 } else { // "cat" mode: size unknown, read it all in streaming fashion 551 ByteArrayOutputStream contents = new ByteArrayOutputStream(); 552 int len; 553 byte[] data = new byte[1024]; 554 do { 555 len = bis.read(data); 556 if (len > 0) contents.write(data, 0, len); 557 } while (len == data.length); 558 return contents.toString(); 559 } 560 } finally { 561 bis.close(); 562 input.close(); 563 } 564 } 565 stringToFile(File file, String string)566 public static void stringToFile(File file, String string) throws IOException { 567 stringToFile(file.getAbsolutePath(), string); 568 } 569 570 /* 571 * Writes the bytes given in {@code content} to the file whose absolute path 572 * is {@code filename}. 573 */ bytesToFile(String filename, byte[] content)574 public static void bytesToFile(String filename, byte[] content) throws IOException { 575 if (filename.startsWith("/proc/")) { 576 final int oldMask = StrictMode.allowThreadDiskWritesMask(); 577 try (FileOutputStream fos = new FileOutputStream(filename)) { 578 fos.write(content); 579 } finally { 580 StrictMode.setThreadPolicyMask(oldMask); 581 } 582 } else { 583 try (FileOutputStream fos = new FileOutputStream(filename)) { 584 fos.write(content); 585 } 586 } 587 } 588 589 /** 590 * Writes string to file. Basically same as "echo -n $string > $filename" 591 * 592 * @param filename 593 * @param string 594 * @throws IOException 595 */ stringToFile(String filename, String string)596 public static void stringToFile(String filename, String string) throws IOException { 597 bytesToFile(filename, string.getBytes(StandardCharsets.UTF_8)); 598 } 599 600 /** 601 * Computes the checksum of a file using the CRC32 checksum routine. 602 * The value of the checksum is returned. 603 * 604 * @param file the file to checksum, must not be null 605 * @return the checksum value or an exception is thrown. 606 */ checksumCrc32(File file)607 public static long checksumCrc32(File file) throws FileNotFoundException, IOException { 608 CRC32 checkSummer = new CRC32(); 609 CheckedInputStream cis = null; 610 611 try { 612 cis = new CheckedInputStream( new FileInputStream(file), checkSummer); 613 byte[] buf = new byte[128]; 614 while(cis.read(buf) >= 0) { 615 // Just read for checksum to get calculated. 616 } 617 return checkSummer.getValue(); 618 } finally { 619 if (cis != null) { 620 try { 621 cis.close(); 622 } catch (IOException e) { 623 } 624 } 625 } 626 } 627 628 /** 629 * Delete older files in a directory until only those matching the given 630 * constraints remain. 631 * 632 * @param minCount Always keep at least this many files. 633 * @param minAgeMs Always keep files younger than this age, in milliseconds. 634 * @return if any files were deleted. 635 */ deleteOlderFiles(File dir, int minCount, long minAgeMs)636 public static boolean deleteOlderFiles(File dir, int minCount, long minAgeMs) { 637 if (minCount < 0 || minAgeMs < 0) { 638 throw new IllegalArgumentException("Constraints must be positive or 0"); 639 } 640 641 final File[] files = dir.listFiles(); 642 if (files == null) return false; 643 644 // Sort with newest files first 645 Arrays.sort(files, new Comparator<File>() { 646 @Override 647 public int compare(File lhs, File rhs) { 648 return Long.compare(rhs.lastModified(), lhs.lastModified()); 649 } 650 }); 651 652 // Keep at least minCount files 653 boolean deleted = false; 654 for (int i = minCount; i < files.length; i++) { 655 final File file = files[i]; 656 657 // Keep files newer than minAgeMs 658 final long age = System.currentTimeMillis() - file.lastModified(); 659 if (age > minAgeMs) { 660 if (file.delete()) { 661 Log.d(TAG, "Deleted old file " + file); 662 deleted = true; 663 } 664 } 665 } 666 return deleted; 667 } 668 669 /** 670 * Test if a file lives under the given directory, either as a direct child 671 * or a distant grandchild. 672 * <p> 673 * Both files <em>must</em> have been resolved using 674 * {@link File#getCanonicalFile()} to avoid symlink or path traversal 675 * attacks. 676 */ contains(File[] dirs, File file)677 public static boolean contains(File[] dirs, File file) { 678 for (File dir : dirs) { 679 if (contains(dir, file)) { 680 return true; 681 } 682 } 683 return false; 684 } 685 686 /** 687 * Test if a file lives under the given directory, either as a direct child 688 * or a distant grandchild. 689 * <p> 690 * Both files <em>must</em> have been resolved using 691 * {@link File#getCanonicalFile()} to avoid symlink or path traversal 692 * attacks. 693 */ contains(File dir, File file)694 public static boolean contains(File dir, File file) { 695 if (dir == null || file == null) return false; 696 return contains(dir.getAbsolutePath(), file.getAbsolutePath()); 697 } 698 contains(String dirPath, String filePath)699 public static boolean contains(String dirPath, String filePath) { 700 if (dirPath.equals(filePath)) { 701 return true; 702 } 703 if (!dirPath.endsWith("/")) { 704 dirPath += "/"; 705 } 706 return filePath.startsWith(dirPath); 707 } 708 deleteContentsAndDir(File dir)709 public static boolean deleteContentsAndDir(File dir) { 710 if (deleteContents(dir)) { 711 return dir.delete(); 712 } else { 713 return false; 714 } 715 } 716 deleteContents(File dir)717 public static boolean deleteContents(File dir) { 718 File[] files = dir.listFiles(); 719 boolean success = true; 720 if (files != null) { 721 for (File file : files) { 722 if (file.isDirectory()) { 723 success &= deleteContents(file); 724 } 725 if (!file.delete()) { 726 Log.w(TAG, "Failed to delete " + file); 727 success = false; 728 } 729 } 730 } 731 return success; 732 } 733 isValidExtFilenameChar(char c)734 private static boolean isValidExtFilenameChar(char c) { 735 switch (c) { 736 case '\0': 737 case '/': 738 return false; 739 default: 740 return true; 741 } 742 } 743 744 /** 745 * Check if given filename is valid for an ext4 filesystem. 746 */ isValidExtFilename(String name)747 public static boolean isValidExtFilename(String name) { 748 return (name != null) && name.equals(buildValidExtFilename(name)); 749 } 750 751 /** 752 * Mutate the given filename to make it valid for an ext4 filesystem, 753 * replacing any invalid characters with "_". 754 */ buildValidExtFilename(String name)755 public static String buildValidExtFilename(String name) { 756 if (TextUtils.isEmpty(name) || ".".equals(name) || "..".equals(name)) { 757 return "(invalid)"; 758 } 759 final StringBuilder res = new StringBuilder(name.length()); 760 for (int i = 0; i < name.length(); i++) { 761 final char c = name.charAt(i); 762 if (isValidExtFilenameChar(c)) { 763 res.append(c); 764 } else { 765 res.append('_'); 766 } 767 } 768 trimFilename(res, 255); 769 return res.toString(); 770 } 771 isValidFatFilenameChar(char c)772 private static boolean isValidFatFilenameChar(char c) { 773 if ((0x00 <= c && c <= 0x1f)) { 774 return false; 775 } 776 switch (c) { 777 case '"': 778 case '*': 779 case '/': 780 case ':': 781 case '<': 782 case '>': 783 case '?': 784 case '\\': 785 case '|': 786 case 0x7F: 787 return false; 788 default: 789 return true; 790 } 791 } 792 793 /** 794 * Check if given filename is valid for a FAT filesystem. 795 */ isValidFatFilename(String name)796 public static boolean isValidFatFilename(String name) { 797 return (name != null) && name.equals(buildValidFatFilename(name)); 798 } 799 800 /** 801 * Mutate the given filename to make it valid for a FAT filesystem, 802 * replacing any invalid characters with "_". 803 */ buildValidFatFilename(String name)804 public static String buildValidFatFilename(String name) { 805 if (TextUtils.isEmpty(name) || ".".equals(name) || "..".equals(name)) { 806 return "(invalid)"; 807 } 808 final StringBuilder res = new StringBuilder(name.length()); 809 for (int i = 0; i < name.length(); i++) { 810 final char c = name.charAt(i); 811 if (isValidFatFilenameChar(c)) { 812 res.append(c); 813 } else { 814 res.append('_'); 815 } 816 } 817 // Even though vfat allows 255 UCS-2 chars, we might eventually write to 818 // ext4 through a FUSE layer, so use that limit. 819 trimFilename(res, 255); 820 return res.toString(); 821 } 822 823 @VisibleForTesting trimFilename(String str, int maxBytes)824 public static String trimFilename(String str, int maxBytes) { 825 final StringBuilder res = new StringBuilder(str); 826 trimFilename(res, maxBytes); 827 return res.toString(); 828 } 829 trimFilename(StringBuilder res, int maxBytes)830 private static void trimFilename(StringBuilder res, int maxBytes) { 831 byte[] raw = res.toString().getBytes(StandardCharsets.UTF_8); 832 if (raw.length > maxBytes) { 833 maxBytes -= 3; 834 while (raw.length > maxBytes) { 835 res.deleteCharAt(res.length() / 2); 836 raw = res.toString().getBytes(StandardCharsets.UTF_8); 837 } 838 res.insert(res.length() / 2, "..."); 839 } 840 } 841 rewriteAfterRename(File beforeDir, File afterDir, String path)842 public static String rewriteAfterRename(File beforeDir, File afterDir, String path) { 843 if (path == null) return null; 844 final File result = rewriteAfterRename(beforeDir, afterDir, new File(path)); 845 return (result != null) ? result.getAbsolutePath() : null; 846 } 847 rewriteAfterRename(File beforeDir, File afterDir, String[] paths)848 public static String[] rewriteAfterRename(File beforeDir, File afterDir, String[] paths) { 849 if (paths == null) return null; 850 final String[] result = new String[paths.length]; 851 for (int i = 0; i < paths.length; i++) { 852 result[i] = rewriteAfterRename(beforeDir, afterDir, paths[i]); 853 } 854 return result; 855 } 856 857 /** 858 * Given a path under the "before" directory, rewrite it to live under the 859 * "after" directory. For example, {@code /before/foo/bar.txt} would become 860 * {@code /after/foo/bar.txt}. 861 */ rewriteAfterRename(File beforeDir, File afterDir, File file)862 public static File rewriteAfterRename(File beforeDir, File afterDir, File file) { 863 if (file == null || beforeDir == null || afterDir == null) return null; 864 if (contains(beforeDir, file)) { 865 final String splice = file.getAbsolutePath().substring( 866 beforeDir.getAbsolutePath().length()); 867 return new File(afterDir, splice); 868 } 869 return null; 870 } 871 buildUniqueFileWithExtension(File parent, String name, String ext)872 private static File buildUniqueFileWithExtension(File parent, String name, String ext) 873 throws FileNotFoundException { 874 File file = buildFile(parent, name, ext); 875 876 // If conflicting file, try adding counter suffix 877 int n = 0; 878 while (file.exists()) { 879 if (n++ >= 32) { 880 throw new FileNotFoundException("Failed to create unique file"); 881 } 882 file = buildFile(parent, name + " (" + n + ")", ext); 883 } 884 885 return file; 886 } 887 888 /** 889 * Generates a unique file name under the given parent directory. If the display name doesn't 890 * have an extension that matches the requested MIME type, the default extension for that MIME 891 * type is appended. If a file already exists, the name is appended with a numerical value to 892 * make it unique. 893 * 894 * For example, the display name 'example' with 'text/plain' MIME might produce 895 * 'example.txt' or 'example (1).txt', etc. 896 * 897 * @throws FileNotFoundException 898 */ buildUniqueFile(File parent, String mimeType, String displayName)899 public static File buildUniqueFile(File parent, String mimeType, String displayName) 900 throws FileNotFoundException { 901 final String[] parts = splitFileName(mimeType, displayName); 902 return buildUniqueFileWithExtension(parent, parts[0], parts[1]); 903 } 904 905 /** 906 * Generates a unique file name under the given parent directory, keeping 907 * any extension intact. 908 */ buildUniqueFile(File parent, String displayName)909 public static File buildUniqueFile(File parent, String displayName) 910 throws FileNotFoundException { 911 final String name; 912 final String ext; 913 914 // Extract requested extension from display name 915 final int lastDot = displayName.lastIndexOf('.'); 916 if (lastDot >= 0) { 917 name = displayName.substring(0, lastDot); 918 ext = displayName.substring(lastDot + 1); 919 } else { 920 name = displayName; 921 ext = null; 922 } 923 924 return buildUniqueFileWithExtension(parent, name, ext); 925 } 926 927 /** 928 * Splits file name into base name and extension. 929 * If the display name doesn't have an extension that matches the requested MIME type, the 930 * extension is regarded as a part of filename and default extension for that MIME type is 931 * appended. 932 */ splitFileName(String mimeType, String displayName)933 public static String[] splitFileName(String mimeType, String displayName) { 934 String name; 935 String ext; 936 937 if (Document.MIME_TYPE_DIR.equals(mimeType)) { 938 name = displayName; 939 ext = null; 940 } else { 941 String mimeTypeFromExt; 942 943 // Extract requested extension from display name 944 final int lastDot = displayName.lastIndexOf('.'); 945 if (lastDot >= 0) { 946 name = displayName.substring(0, lastDot); 947 ext = displayName.substring(lastDot + 1); 948 mimeTypeFromExt = MimeTypeMap.getSingleton().getMimeTypeFromExtension( 949 ext.toLowerCase()); 950 } else { 951 name = displayName; 952 ext = null; 953 mimeTypeFromExt = null; 954 } 955 956 if (mimeTypeFromExt == null) { 957 mimeTypeFromExt = "application/octet-stream"; 958 } 959 960 final String extFromMimeType = MimeTypeMap.getSingleton().getExtensionFromMimeType( 961 mimeType); 962 if (Objects.equals(mimeType, mimeTypeFromExt) || Objects.equals(ext, extFromMimeType)) { 963 // Extension maps back to requested MIME type; allow it 964 } else { 965 // No match; insist that create file matches requested MIME 966 name = displayName; 967 ext = extFromMimeType; 968 } 969 } 970 971 if (ext == null) { 972 ext = ""; 973 } 974 975 return new String[] { name, ext }; 976 } 977 buildFile(File parent, String name, String ext)978 private static File buildFile(File parent, String name, String ext) { 979 if (TextUtils.isEmpty(ext)) { 980 return new File(parent, name); 981 } else { 982 return new File(parent, name + "." + ext); 983 } 984 } 985 listOrEmpty(@ullable File dir)986 public static @NonNull String[] listOrEmpty(@Nullable File dir) { 987 if (dir == null) return EmptyArray.STRING; 988 final String[] res = dir.list(); 989 if (res != null) { 990 return res; 991 } else { 992 return EmptyArray.STRING; 993 } 994 } 995 listFilesOrEmpty(@ullable File dir)996 public static @NonNull File[] listFilesOrEmpty(@Nullable File dir) { 997 if (dir == null) return EMPTY; 998 final File[] res = dir.listFiles(); 999 if (res != null) { 1000 return res; 1001 } else { 1002 return EMPTY; 1003 } 1004 } 1005 listFilesOrEmpty(@ullable File dir, FilenameFilter filter)1006 public static @NonNull File[] listFilesOrEmpty(@Nullable File dir, FilenameFilter filter) { 1007 if (dir == null) return EMPTY; 1008 final File[] res = dir.listFiles(filter); 1009 if (res != null) { 1010 return res; 1011 } else { 1012 return EMPTY; 1013 } 1014 } 1015 newFileOrNull(@ullable String path)1016 public static @Nullable File newFileOrNull(@Nullable String path) { 1017 return (path != null) ? new File(path) : null; 1018 } 1019 1020 /** 1021 * Creates a directory with name {@code name} under an existing directory {@code baseDir}. 1022 * Returns a {@code File} object representing the directory on success, {@code null} on 1023 * failure. 1024 */ createDir(File baseDir, String name)1025 public static @Nullable File createDir(File baseDir, String name) { 1026 final File dir = new File(baseDir, name); 1027 1028 if (dir.exists()) { 1029 return dir.isDirectory() ? dir : null; 1030 } 1031 1032 return dir.mkdir() ? dir : null; 1033 } 1034 1035 /** 1036 * Round the given size of a storage device to a nice round power-of-two 1037 * value, such as 256MB or 32GB. This avoids showing weird values like 1038 * "29.5GB" in UI. 1039 */ roundStorageSize(long size)1040 public static long roundStorageSize(long size) { 1041 long val = 1; 1042 long pow = 1; 1043 while ((val * pow) < size) { 1044 val <<= 1; 1045 if (val > 512) { 1046 val = 1; 1047 pow *= 1000; 1048 } 1049 } 1050 return val * pow; 1051 } 1052 1053 @VisibleForTesting 1054 public static class MemoryPipe extends Thread implements AutoCloseable { 1055 private final FileDescriptor[] pipe; 1056 private final byte[] data; 1057 private final boolean sink; 1058 MemoryPipe(byte[] data, boolean sink)1059 private MemoryPipe(byte[] data, boolean sink) throws IOException { 1060 try { 1061 this.pipe = Os.pipe(); 1062 } catch (ErrnoException e) { 1063 throw e.rethrowAsIOException(); 1064 } 1065 this.data = data; 1066 this.sink = sink; 1067 } 1068 startInternal()1069 private MemoryPipe startInternal() { 1070 super.start(); 1071 return this; 1072 } 1073 createSource(byte[] data)1074 public static MemoryPipe createSource(byte[] data) throws IOException { 1075 return new MemoryPipe(data, false).startInternal(); 1076 } 1077 createSink(byte[] data)1078 public static MemoryPipe createSink(byte[] data) throws IOException { 1079 return new MemoryPipe(data, true).startInternal(); 1080 } 1081 getFD()1082 public FileDescriptor getFD() { 1083 return sink ? pipe[1] : pipe[0]; 1084 } 1085 getInternalFD()1086 public FileDescriptor getInternalFD() { 1087 return sink ? pipe[0] : pipe[1]; 1088 } 1089 1090 @Override run()1091 public void run() { 1092 final FileDescriptor fd = getInternalFD(); 1093 try { 1094 int i = 0; 1095 while (i < data.length) { 1096 if (sink) { 1097 i += Os.read(fd, data, i, data.length - i); 1098 } else { 1099 i += Os.write(fd, data, i, data.length - i); 1100 } 1101 } 1102 } catch (IOException | ErrnoException e) { 1103 // Ignored 1104 } finally { 1105 if (sink) { 1106 SystemClock.sleep(TimeUnit.SECONDS.toMillis(1)); 1107 } 1108 IoUtils.closeQuietly(fd); 1109 } 1110 } 1111 1112 @Override close()1113 public void close() throws Exception { 1114 IoUtils.closeQuietly(getFD()); 1115 } 1116 } 1117 } 1118