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