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