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