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 android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.provider.DocumentsContract.Document; 22 import android.system.ErrnoException; 23 import android.system.Os; 24 import android.system.StructStat; 25 import android.text.TextUtils; 26 import android.util.Log; 27 import android.util.Slog; 28 import android.webkit.MimeTypeMap; 29 30 import com.android.internal.annotations.VisibleForTesting; 31 32 import libcore.util.EmptyArray; 33 34 import java.io.BufferedInputStream; 35 import java.io.ByteArrayOutputStream; 36 import java.io.File; 37 import java.io.FileDescriptor; 38 import java.io.FileInputStream; 39 import java.io.FileNotFoundException; 40 import java.io.FileOutputStream; 41 import java.io.FileWriter; 42 import java.io.FilenameFilter; 43 import java.io.IOException; 44 import java.io.InputStream; 45 import java.nio.charset.StandardCharsets; 46 import java.util.Arrays; 47 import java.util.Comparator; 48 import java.util.Objects; 49 import java.util.regex.Pattern; 50 import java.util.zip.CRC32; 51 import java.util.zip.CheckedInputStream; 52 53 /** 54 * Tools for managing files. Not for public consumption. 55 * @hide 56 */ 57 public class FileUtils { 58 private static final String TAG = "FileUtils"; 59 60 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; 74 75 /** 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 } 82 83 private static final File[] EMPTY = new File[0]; 84 85 /** 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 } 96 97 /** 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 { 107 Os.chmod(path, mode); 108 } 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 { 115 Os.chown(path, uid, gid); 116 } 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 { 135 Os.fchmod(fd, mode); 136 } 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 { 143 Os.fchown(fd, uid, gid); 144 } catch (ErrnoException e) { 145 Slog.w(TAG, "Failed to fchown(): " + e); 146 return e.errno; 147 } 148 } 149 150 return 0; 151 } 152 153 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 163 /** 164 * Return owning UID of given path, otherwise -1. 165 */ 166 public static int getUid(String path) { 167 try { 168 return Os.stat(path).st_uid; 169 } catch (ErrnoException e) { 170 return -1; 171 } 172 } 173 174 /** 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 189 @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 199 // copy a file from srcFile to destFile, return true if succeed, return 200 // false if fail 201 public static void copyFileOrThrow(File srcFile, File destFile) throws IOException { 202 try (InputStream in = new FileInputStream(srcFile)) { 203 copyToFileOrThrow(in, destFile); 204 } 205 } 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 } 215 } 216 217 /** 218 * Copy data from a source stream to destFile. 219 * Return true if succeed, return false if failed. 220 */ 221 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); 227 try { 228 byte[] buffer = new byte[4096]; 229 int bytesRead; 230 while ((bytesRead = inputStream.read(buffer)) >= 0) { 231 out.write(buffer, 0, bytesRead); 232 } 233 } finally { 234 out.flush(); 235 try { 236 out.getFD().sync(); 237 } catch (IOException e) { 238 } 239 out.close(); 240 } 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. 251 return NoImagePreloadHolder.SAFE_FILENAME_PATTERN.matcher(file.getPath()).matches(); 252 } 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); 264 // 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); 268 try { 269 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; 272 byte[] data = new byte[max + 1]; 273 int length = bis.read(data); 274 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; 278 } else if (max < 0) { // "tail" mode: keep the last N 279 int len; 280 boolean rolled = false; 281 byte[] last = null; 282 byte[] data = null; 283 do { 284 if (last != null) rolled = true; 285 byte[] tmp = last; last = data; data = tmp; 286 if (data == null) data = new byte[-max]; 287 len = bis.read(data); 288 } 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); 299 } else { // "cat" mode: size unknown, read it all in streaming fashion 300 ByteArrayOutputStream contents = new ByteArrayOutputStream(); 301 int len; 302 byte[] data = new byte[1024]; 303 do { 304 len = bis.read(data); 305 if (len > 0) contents.write(data, 0, len); 306 } while (len == data.length); 307 return contents.toString(); 308 } 309 } finally { 310 bis.close(); 311 input.close(); 312 } 313 } 314 315 public static void stringToFile(File file, String string) throws IOException { 316 stringToFile(file.getAbsolutePath(), string); 317 } 318 319 /** 320 * Writes string to file. Basically same as "echo -n $string > $filename" 321 * 322 * @param filename 323 * @param string 324 * @throws IOException 325 */ 326 public static void stringToFile(String filename, String string) throws IOException { 327 FileWriter out = new FileWriter(filename); 328 try { 329 out.write(string); 330 } finally { 331 out.close(); 332 } 333 } 334 335 /** 336 * Computes the checksum of a file using the CRC32 checksum routine. 337 * The value of the checksum is returned. 338 * 339 * @param file the file to checksum, must not be null 340 * @return the checksum value or an exception is thrown. 341 */ 342 public static long checksumCrc32(File file) throws FileNotFoundException, IOException { 343 CRC32 checkSummer = new CRC32(); 344 CheckedInputStream cis = null; 345 346 try { 347 cis = new CheckedInputStream( new FileInputStream(file), checkSummer); 348 byte[] buf = new byte[128]; 349 while(cis.read(buf) >= 0) { 350 // Just read for checksum to get calculated. 351 } 352 return checkSummer.getValue(); 353 } finally { 354 if (cis != null) { 355 try { 356 cis.close(); 357 } catch (IOException e) { 358 } 359 } 360 } 361 } 362 363 /** 364 * Delete older files in a directory until only those matching the given 365 * constraints remain. 366 * 367 * @param minCount Always keep at least this many files. 368 * @param minAge Always keep files younger than this age. 369 * @return if any files were deleted. 370 */ 371 public static boolean deleteOlderFiles(File dir, int minCount, long minAge) { 372 if (minCount < 0 || minAge < 0) { 373 throw new IllegalArgumentException("Constraints must be positive or 0"); 374 } 375 376 final File[] files = dir.listFiles(); 377 if (files == null) return false; 378 379 // Sort with newest files first 380 Arrays.sort(files, new Comparator<File>() { 381 @Override 382 public int compare(File lhs, File rhs) { 383 return (int) (rhs.lastModified() - lhs.lastModified()); 384 } 385 }); 386 387 // Keep at least minCount files 388 boolean deleted = false; 389 for (int i = minCount; i < files.length; i++) { 390 final File file = files[i]; 391 392 // Keep files newer than minAge 393 final long age = System.currentTimeMillis() - file.lastModified(); 394 if (age > minAge) { 395 if (file.delete()) { 396 Log.d(TAG, "Deleted old file " + file); 397 deleted = true; 398 } 399 } 400 } 401 return deleted; 402 } 403 404 /** 405 * Test if a file lives under the given directory, either as a direct child 406 * or a distant grandchild. 407 * <p> 408 * Both files <em>must</em> have been resolved using 409 * {@link File#getCanonicalFile()} to avoid symlink or path traversal 410 * attacks. 411 */ 412 public static boolean contains(File[] dirs, File file) { 413 for (File dir : dirs) { 414 if (contains(dir, file)) { 415 return true; 416 } 417 } 418 return false; 419 } 420 421 /** 422 * Test if a file lives under the given directory, either as a direct child 423 * or a distant grandchild. 424 * <p> 425 * Both files <em>must</em> have been resolved using 426 * {@link File#getCanonicalFile()} to avoid symlink or path traversal 427 * attacks. 428 */ 429 public static boolean contains(File dir, File file) { 430 if (dir == null || file == null) return false; 431 432 String dirPath = dir.getAbsolutePath(); 433 String filePath = file.getAbsolutePath(); 434 435 if (dirPath.equals(filePath)) { 436 return true; 437 } 438 439 if (!dirPath.endsWith("/")) { 440 dirPath += "/"; 441 } 442 return filePath.startsWith(dirPath); 443 } 444 445 public static boolean deleteContentsAndDir(File dir) { 446 if (deleteContents(dir)) { 447 return dir.delete(); 448 } else { 449 return false; 450 } 451 } 452 453 public static boolean deleteContents(File dir) { 454 File[] files = dir.listFiles(); 455 boolean success = true; 456 if (files != null) { 457 for (File file : files) { 458 if (file.isDirectory()) { 459 success &= deleteContents(file); 460 } 461 if (!file.delete()) { 462 Log.w(TAG, "Failed to delete " + file); 463 success = false; 464 } 465 } 466 } 467 return success; 468 } 469 470 private static boolean isValidExtFilenameChar(char c) { 471 switch (c) { 472 case '\0': 473 case '/': 474 return false; 475 default: 476 return true; 477 } 478 } 479 480 /** 481 * Check if given filename is valid for an ext4 filesystem. 482 */ 483 public static boolean isValidExtFilename(String name) { 484 return (name != null) && name.equals(buildValidExtFilename(name)); 485 } 486 487 /** 488 * Mutate the given filename to make it valid for an ext4 filesystem, 489 * replacing any invalid characters with "_". 490 */ 491 public static String buildValidExtFilename(String name) { 492 if (TextUtils.isEmpty(name) || ".".equals(name) || "..".equals(name)) { 493 return "(invalid)"; 494 } 495 final StringBuilder res = new StringBuilder(name.length()); 496 for (int i = 0; i < name.length(); i++) { 497 final char c = name.charAt(i); 498 if (isValidExtFilenameChar(c)) { 499 res.append(c); 500 } else { 501 res.append('_'); 502 } 503 } 504 trimFilename(res, 255); 505 return res.toString(); 506 } 507 508 private static boolean isValidFatFilenameChar(char c) { 509 if ((0x00 <= c && c <= 0x1f)) { 510 return false; 511 } 512 switch (c) { 513 case '"': 514 case '*': 515 case '/': 516 case ':': 517 case '<': 518 case '>': 519 case '?': 520 case '\\': 521 case '|': 522 case 0x7F: 523 return false; 524 default: 525 return true; 526 } 527 } 528 529 /** 530 * Check if given filename is valid for a FAT filesystem. 531 */ 532 public static boolean isValidFatFilename(String name) { 533 return (name != null) && name.equals(buildValidFatFilename(name)); 534 } 535 536 /** 537 * Mutate the given filename to make it valid for a FAT filesystem, 538 * replacing any invalid characters with "_". 539 */ 540 public static String buildValidFatFilename(String name) { 541 if (TextUtils.isEmpty(name) || ".".equals(name) || "..".equals(name)) { 542 return "(invalid)"; 543 } 544 final StringBuilder res = new StringBuilder(name.length()); 545 for (int i = 0; i < name.length(); i++) { 546 final char c = name.charAt(i); 547 if (isValidFatFilenameChar(c)) { 548 res.append(c); 549 } else { 550 res.append('_'); 551 } 552 } 553 // Even though vfat allows 255 UCS-2 chars, we might eventually write to 554 // ext4 through a FUSE layer, so use that limit. 555 trimFilename(res, 255); 556 return res.toString(); 557 } 558 559 @VisibleForTesting 560 public static String trimFilename(String str, int maxBytes) { 561 final StringBuilder res = new StringBuilder(str); 562 trimFilename(res, maxBytes); 563 return res.toString(); 564 } 565 566 private static void trimFilename(StringBuilder res, int maxBytes) { 567 byte[] raw = res.toString().getBytes(StandardCharsets.UTF_8); 568 if (raw.length > maxBytes) { 569 maxBytes -= 3; 570 while (raw.length > maxBytes) { 571 res.deleteCharAt(res.length() / 2); 572 raw = res.toString().getBytes(StandardCharsets.UTF_8); 573 } 574 res.insert(res.length() / 2, "..."); 575 } 576 } 577 578 public static String rewriteAfterRename(File beforeDir, File afterDir, String path) { 579 if (path == null) return null; 580 final File result = rewriteAfterRename(beforeDir, afterDir, new File(path)); 581 return (result != null) ? result.getAbsolutePath() : null; 582 } 583 584 public static String[] rewriteAfterRename(File beforeDir, File afterDir, String[] paths) { 585 if (paths == null) return null; 586 final String[] result = new String[paths.length]; 587 for (int i = 0; i < paths.length; i++) { 588 result[i] = rewriteAfterRename(beforeDir, afterDir, paths[i]); 589 } 590 return result; 591 } 592 593 /** 594 * Given a path under the "before" directory, rewrite it to live under the 595 * "after" directory. For example, {@code /before/foo/bar.txt} would become 596 * {@code /after/foo/bar.txt}. 597 */ 598 public static File rewriteAfterRename(File beforeDir, File afterDir, File file) { 599 if (file == null || beforeDir == null || afterDir == null) return null; 600 if (contains(beforeDir, file)) { 601 final String splice = file.getAbsolutePath().substring( 602 beforeDir.getAbsolutePath().length()); 603 return new File(afterDir, splice); 604 } 605 return null; 606 } 607 608 /** 609 * Generates a unique file name under the given parent directory. If the display name doesn't 610 * have an extension that matches the requested MIME type, the default extension for that MIME 611 * type is appended. If a file already exists, the name is appended with a numerical value to 612 * make it unique. 613 * 614 * For example, the display name 'example' with 'text/plain' MIME might produce 615 * 'example.txt' or 'example (1).txt', etc. 616 * 617 * @throws FileNotFoundException 618 */ 619 public static File buildUniqueFile(File parent, String mimeType, String displayName) 620 throws FileNotFoundException { 621 final String[] parts = splitFileName(mimeType, displayName); 622 final String name = parts[0]; 623 final String ext = parts[1]; 624 File file = buildFile(parent, name, ext); 625 626 // If conflicting file, try adding counter suffix 627 int n = 0; 628 while (file.exists()) { 629 if (n++ >= 32) { 630 throw new FileNotFoundException("Failed to create unique file"); 631 } 632 file = buildFile(parent, name + " (" + n + ")", ext); 633 } 634 635 return file; 636 } 637 638 /** 639 * Splits file name into base name and extension. 640 * If the display name doesn't have an extension that matches the requested MIME type, the 641 * extension is regarded as a part of filename and default extension for that MIME type is 642 * appended. 643 */ 644 public static String[] splitFileName(String mimeType, String displayName) { 645 String name; 646 String ext; 647 648 if (Document.MIME_TYPE_DIR.equals(mimeType)) { 649 name = displayName; 650 ext = null; 651 } else { 652 String mimeTypeFromExt; 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 mimeTypeFromExt = MimeTypeMap.getSingleton().getMimeTypeFromExtension( 660 ext.toLowerCase()); 661 } else { 662 name = displayName; 663 ext = null; 664 mimeTypeFromExt = null; 665 } 666 667 if (mimeTypeFromExt == null) { 668 mimeTypeFromExt = "application/octet-stream"; 669 } 670 671 final String extFromMimeType = MimeTypeMap.getSingleton().getExtensionFromMimeType( 672 mimeType); 673 if (Objects.equals(mimeType, mimeTypeFromExt) || Objects.equals(ext, extFromMimeType)) { 674 // Extension maps back to requested MIME type; allow it 675 } else { 676 // No match; insist that create file matches requested MIME 677 name = displayName; 678 ext = extFromMimeType; 679 } 680 } 681 682 if (ext == null) { 683 ext = ""; 684 } 685 686 return new String[] { name, ext }; 687 } 688 689 private static File buildFile(File parent, String name, String ext) { 690 if (TextUtils.isEmpty(ext)) { 691 return new File(parent, name); 692 } else { 693 return new File(parent, name + "." + ext); 694 } 695 } 696 697 public static @NonNull String[] listOrEmpty(@Nullable File dir) { 698 if (dir == null) return EmptyArray.STRING; 699 final String[] res = dir.list(); 700 if (res != null) { 701 return res; 702 } else { 703 return EmptyArray.STRING; 704 } 705 } 706 707 public static @NonNull File[] listFilesOrEmpty(@Nullable File dir) { 708 if (dir == null) return EMPTY; 709 final File[] res = dir.listFiles(); 710 if (res != null) { 711 return res; 712 } else { 713 return EMPTY; 714 } 715 } 716 717 public static @NonNull File[] listFilesOrEmpty(@Nullable File dir, FilenameFilter filter) { 718 if (dir == null) return EMPTY; 719 final File[] res = dir.listFiles(filter); 720 if (res != null) { 721 return res; 722 } else { 723 return EMPTY; 724 } 725 } 726 727 public static @Nullable File newFileOrNull(@Nullable String path) { 728 return (path != null) ? new File(path) : null; 729 } 730 } 731