/* * Copyright (C) 2006 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.os; import android.annotation.NonNull; import android.annotation.Nullable; import android.provider.DocumentsContract.Document; import android.system.ErrnoException; import android.system.Os; import android.system.StructStat; import android.text.TextUtils; import android.util.Log; import android.util.Slog; import android.webkit.MimeTypeMap; import com.android.internal.annotations.VisibleForTesting; import libcore.util.EmptyArray; import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileWriter; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Comparator; import java.util.Objects; import java.util.regex.Pattern; import java.util.zip.CRC32; import java.util.zip.CheckedInputStream; /** * Tools for managing files. Not for public consumption. * @hide */ public class FileUtils { private static final String TAG = "FileUtils"; public static final int S_IRWXU = 00700; public static final int S_IRUSR = 00400; public static final int S_IWUSR = 00200; public static final int S_IXUSR = 00100; public static final int S_IRWXG = 00070; public static final int S_IRGRP = 00040; public static final int S_IWGRP = 00020; public static final int S_IXGRP = 00010; public static final int S_IRWXO = 00007; public static final int S_IROTH = 00004; public static final int S_IWOTH = 00002; public static final int S_IXOTH = 00001; /** Regular expression for safe filenames: no spaces or metacharacters. * * Use a preload holder so that FileUtils can be compile-time initialized. */ private static class NoImagePreloadHolder { public static final Pattern SAFE_FILENAME_PATTERN = Pattern.compile("[\\w%+,./=_-]+"); } private static final File[] EMPTY = new File[0]; /** * Set owner and mode of of given {@link File}. * * @param mode to apply through {@code chmod} * @param uid to apply through {@code chown}, or -1 to leave unchanged * @param gid to apply through {@code chown}, or -1 to leave unchanged * @return 0 on success, otherwise errno. */ public static int setPermissions(File path, int mode, int uid, int gid) { return setPermissions(path.getAbsolutePath(), mode, uid, gid); } /** * Set owner and mode of of given path. * * @param mode to apply through {@code chmod} * @param uid to apply through {@code chown}, or -1 to leave unchanged * @param gid to apply through {@code chown}, or -1 to leave unchanged * @return 0 on success, otherwise errno. */ public static int setPermissions(String path, int mode, int uid, int gid) { try { Os.chmod(path, mode); } catch (ErrnoException e) { Slog.w(TAG, "Failed to chmod(" + path + "): " + e); return e.errno; } if (uid >= 0 || gid >= 0) { try { Os.chown(path, uid, gid); } catch (ErrnoException e) { Slog.w(TAG, "Failed to chown(" + path + "): " + e); return e.errno; } } return 0; } /** * Set owner and mode of of given {@link FileDescriptor}. * * @param mode to apply through {@code chmod} * @param uid to apply through {@code chown}, or -1 to leave unchanged * @param gid to apply through {@code chown}, or -1 to leave unchanged * @return 0 on success, otherwise errno. */ public static int setPermissions(FileDescriptor fd, int mode, int uid, int gid) { try { Os.fchmod(fd, mode); } catch (ErrnoException e) { Slog.w(TAG, "Failed to fchmod(): " + e); return e.errno; } if (uid >= 0 || gid >= 0) { try { Os.fchown(fd, uid, gid); } catch (ErrnoException e) { Slog.w(TAG, "Failed to fchown(): " + e); return e.errno; } } return 0; } public static void copyPermissions(File from, File to) throws IOException { try { final StructStat stat = Os.stat(from.getAbsolutePath()); Os.chmod(to.getAbsolutePath(), stat.st_mode); Os.chown(to.getAbsolutePath(), stat.st_uid, stat.st_gid); } catch (ErrnoException e) { throw e.rethrowAsIOException(); } } /** * Return owning UID of given path, otherwise -1. */ public static int getUid(String path) { try { return Os.stat(path).st_uid; } catch (ErrnoException e) { return -1; } } /** * Perform an fsync on the given FileOutputStream. The stream at this * point must be flushed but not yet closed. */ public static boolean sync(FileOutputStream stream) { try { if (stream != null) { stream.getFD().sync(); } return true; } catch (IOException e) { } return false; } @Deprecated public static boolean copyFile(File srcFile, File destFile) { try { copyFileOrThrow(srcFile, destFile); return true; } catch (IOException e) { return false; } } // copy a file from srcFile to destFile, return true if succeed, return // false if fail public static void copyFileOrThrow(File srcFile, File destFile) throws IOException { try (InputStream in = new FileInputStream(srcFile)) { copyToFileOrThrow(in, destFile); } } @Deprecated public static boolean copyToFile(InputStream inputStream, File destFile) { try { copyToFileOrThrow(inputStream, destFile); return true; } catch (IOException e) { return false; } } /** * Copy data from a source stream to destFile. * Return true if succeed, return false if failed. */ public static void copyToFileOrThrow(InputStream inputStream, File destFile) throws IOException { if (destFile.exists()) { destFile.delete(); } FileOutputStream out = new FileOutputStream(destFile); try { byte[] buffer = new byte[4096]; int bytesRead; while ((bytesRead = inputStream.read(buffer)) >= 0) { out.write(buffer, 0, bytesRead); } } finally { out.flush(); try { out.getFD().sync(); } catch (IOException e) { } out.close(); } } /** * Check if a filename is "safe" (no metacharacters or spaces). * @param file The file to check */ public static boolean isFilenameSafe(File file) { // Note, we check whether it matches what's known to be safe, // rather than what's known to be unsafe. Non-ASCII, control // characters, etc. are all unsafe by default. return NoImagePreloadHolder.SAFE_FILENAME_PATTERN.matcher(file.getPath()).matches(); } /** * Read a text file into a String, optionally limiting the length. * @param file to read (will not seek, so things like /proc files are OK) * @param max length (positive for head, negative of tail, 0 for no limit) * @param ellipsis to add of the file was truncated (can be null) * @return the contents of the file, possibly truncated * @throws IOException if something goes wrong reading the file */ public static String readTextFile(File file, int max, String ellipsis) throws IOException { InputStream input = new FileInputStream(file); // wrapping a BufferedInputStream around it because when reading /proc with unbuffered // input stream, bytes read not equal to buffer size is not necessarily the correct // indication for EOF; but it is true for BufferedInputStream due to its implementation. BufferedInputStream bis = new BufferedInputStream(input); try { long size = file.length(); if (max > 0 || (size > 0 && max == 0)) { // "head" mode: read the first N bytes if (size > 0 && (max == 0 || size < max)) max = (int) size; byte[] data = new byte[max + 1]; int length = bis.read(data); if (length <= 0) return ""; if (length <= max) return new String(data, 0, length); if (ellipsis == null) return new String(data, 0, max); return new String(data, 0, max) + ellipsis; } else if (max < 0) { // "tail" mode: keep the last N int len; boolean rolled = false; byte[] last = null; byte[] data = null; do { if (last != null) rolled = true; byte[] tmp = last; last = data; data = tmp; if (data == null) data = new byte[-max]; len = bis.read(data); } while (len == data.length); if (last == null && len <= 0) return ""; if (last == null) return new String(data, 0, len); if (len > 0) { rolled = true; System.arraycopy(last, len, last, 0, last.length - len); System.arraycopy(data, 0, last, last.length - len, len); } if (ellipsis == null || !rolled) return new String(last); return ellipsis + new String(last); } else { // "cat" mode: size unknown, read it all in streaming fashion ByteArrayOutputStream contents = new ByteArrayOutputStream(); int len; byte[] data = new byte[1024]; do { len = bis.read(data); if (len > 0) contents.write(data, 0, len); } while (len == data.length); return contents.toString(); } } finally { bis.close(); input.close(); } } public static void stringToFile(File file, String string) throws IOException { stringToFile(file.getAbsolutePath(), string); } /** * Writes string to file. Basically same as "echo -n $string > $filename" * * @param filename * @param string * @throws IOException */ public static void stringToFile(String filename, String string) throws IOException { FileWriter out = new FileWriter(filename); try { out.write(string); } finally { out.close(); } } /** * Computes the checksum of a file using the CRC32 checksum routine. * The value of the checksum is returned. * * @param file the file to checksum, must not be null * @return the checksum value or an exception is thrown. */ public static long checksumCrc32(File file) throws FileNotFoundException, IOException { CRC32 checkSummer = new CRC32(); CheckedInputStream cis = null; try { cis = new CheckedInputStream( new FileInputStream(file), checkSummer); byte[] buf = new byte[128]; while(cis.read(buf) >= 0) { // Just read for checksum to get calculated. } return checkSummer.getValue(); } finally { if (cis != null) { try { cis.close(); } catch (IOException e) { } } } } /** * Delete older files in a directory until only those matching the given * constraints remain. * * @param minCount Always keep at least this many files. * @param minAge Always keep files younger than this age. * @return if any files were deleted. */ public static boolean deleteOlderFiles(File dir, int minCount, long minAge) { if (minCount < 0 || minAge < 0) { throw new IllegalArgumentException("Constraints must be positive or 0"); } final File[] files = dir.listFiles(); if (files == null) return false; // Sort with newest files first Arrays.sort(files, new Comparator() { @Override public int compare(File lhs, File rhs) { return (int) (rhs.lastModified() - lhs.lastModified()); } }); // Keep at least minCount files boolean deleted = false; for (int i = minCount; i < files.length; i++) { final File file = files[i]; // Keep files newer than minAge final long age = System.currentTimeMillis() - file.lastModified(); if (age > minAge) { if (file.delete()) { Log.d(TAG, "Deleted old file " + file); deleted = true; } } } return deleted; } /** * Test if a file lives under the given directory, either as a direct child * or a distant grandchild. *

* Both files must have been resolved using * {@link File#getCanonicalFile()} to avoid symlink or path traversal * attacks. */ public static boolean contains(File[] dirs, File file) { for (File dir : dirs) { if (contains(dir, file)) { return true; } } return false; } /** * Test if a file lives under the given directory, either as a direct child * or a distant grandchild. *

* Both files must have been resolved using * {@link File#getCanonicalFile()} to avoid symlink or path traversal * attacks. */ public static boolean contains(File dir, File file) { if (dir == null || file == null) return false; String dirPath = dir.getAbsolutePath(); String filePath = file.getAbsolutePath(); if (dirPath.equals(filePath)) { return true; } if (!dirPath.endsWith("/")) { dirPath += "/"; } return filePath.startsWith(dirPath); } public static boolean deleteContentsAndDir(File dir) { if (deleteContents(dir)) { return dir.delete(); } else { return false; } } public static boolean deleteContents(File dir) { File[] files = dir.listFiles(); boolean success = true; if (files != null) { for (File file : files) { if (file.isDirectory()) { success &= deleteContents(file); } if (!file.delete()) { Log.w(TAG, "Failed to delete " + file); success = false; } } } return success; } private static boolean isValidExtFilenameChar(char c) { switch (c) { case '\0': case '/': return false; default: return true; } } /** * Check if given filename is valid for an ext4 filesystem. */ public static boolean isValidExtFilename(String name) { return (name != null) && name.equals(buildValidExtFilename(name)); } /** * Mutate the given filename to make it valid for an ext4 filesystem, * replacing any invalid characters with "_". */ public static String buildValidExtFilename(String name) { if (TextUtils.isEmpty(name) || ".".equals(name) || "..".equals(name)) { return "(invalid)"; } final StringBuilder res = new StringBuilder(name.length()); for (int i = 0; i < name.length(); i++) { final char c = name.charAt(i); if (isValidExtFilenameChar(c)) { res.append(c); } else { res.append('_'); } } trimFilename(res, 255); return res.toString(); } private static boolean isValidFatFilenameChar(char c) { if ((0x00 <= c && c <= 0x1f)) { return false; } switch (c) { case '"': case '*': case '/': case ':': case '<': case '>': case '?': case '\\': case '|': case 0x7F: return false; default: return true; } } /** * Check if given filename is valid for a FAT filesystem. */ public static boolean isValidFatFilename(String name) { return (name != null) && name.equals(buildValidFatFilename(name)); } /** * Mutate the given filename to make it valid for a FAT filesystem, * replacing any invalid characters with "_". */ public static String buildValidFatFilename(String name) { if (TextUtils.isEmpty(name) || ".".equals(name) || "..".equals(name)) { return "(invalid)"; } final StringBuilder res = new StringBuilder(name.length()); for (int i = 0; i < name.length(); i++) { final char c = name.charAt(i); if (isValidFatFilenameChar(c)) { res.append(c); } else { res.append('_'); } } // Even though vfat allows 255 UCS-2 chars, we might eventually write to // ext4 through a FUSE layer, so use that limit. trimFilename(res, 255); return res.toString(); } @VisibleForTesting public static String trimFilename(String str, int maxBytes) { final StringBuilder res = new StringBuilder(str); trimFilename(res, maxBytes); return res.toString(); } private static void trimFilename(StringBuilder res, int maxBytes) { byte[] raw = res.toString().getBytes(StandardCharsets.UTF_8); if (raw.length > maxBytes) { maxBytes -= 3; while (raw.length > maxBytes) { res.deleteCharAt(res.length() / 2); raw = res.toString().getBytes(StandardCharsets.UTF_8); } res.insert(res.length() / 2, "..."); } } public static String rewriteAfterRename(File beforeDir, File afterDir, String path) { if (path == null) return null; final File result = rewriteAfterRename(beforeDir, afterDir, new File(path)); return (result != null) ? result.getAbsolutePath() : null; } public static String[] rewriteAfterRename(File beforeDir, File afterDir, String[] paths) { if (paths == null) return null; final String[] result = new String[paths.length]; for (int i = 0; i < paths.length; i++) { result[i] = rewriteAfterRename(beforeDir, afterDir, paths[i]); } return result; } /** * Given a path under the "before" directory, rewrite it to live under the * "after" directory. For example, {@code /before/foo/bar.txt} would become * {@code /after/foo/bar.txt}. */ public static File rewriteAfterRename(File beforeDir, File afterDir, File file) { if (file == null || beforeDir == null || afterDir == null) return null; if (contains(beforeDir, file)) { final String splice = file.getAbsolutePath().substring( beforeDir.getAbsolutePath().length()); return new File(afterDir, splice); } return null; } /** * Generates a unique file name under the given parent directory. If the display name doesn't * have an extension that matches the requested MIME type, the default extension for that MIME * type is appended. If a file already exists, the name is appended with a numerical value to * make it unique. * * For example, the display name 'example' with 'text/plain' MIME might produce * 'example.txt' or 'example (1).txt', etc. * * @throws FileNotFoundException */ public static File buildUniqueFile(File parent, String mimeType, String displayName) throws FileNotFoundException { final String[] parts = splitFileName(mimeType, displayName); final String name = parts[0]; final String ext = parts[1]; File file = buildFile(parent, name, ext); // If conflicting file, try adding counter suffix int n = 0; while (file.exists()) { if (n++ >= 32) { throw new FileNotFoundException("Failed to create unique file"); } file = buildFile(parent, name + " (" + n + ")", ext); } return file; } /** * Splits file name into base name and extension. * If the display name doesn't have an extension that matches the requested MIME type, the * extension is regarded as a part of filename and default extension for that MIME type is * appended. */ public static String[] splitFileName(String mimeType, String displayName) { String name; String ext; if (Document.MIME_TYPE_DIR.equals(mimeType)) { name = displayName; ext = null; } else { String mimeTypeFromExt; // Extract requested extension from display name final int lastDot = displayName.lastIndexOf('.'); if (lastDot >= 0) { name = displayName.substring(0, lastDot); ext = displayName.substring(lastDot + 1); mimeTypeFromExt = MimeTypeMap.getSingleton().getMimeTypeFromExtension( ext.toLowerCase()); } else { name = displayName; ext = null; mimeTypeFromExt = null; } if (mimeTypeFromExt == null) { mimeTypeFromExt = "application/octet-stream"; } final String extFromMimeType = MimeTypeMap.getSingleton().getExtensionFromMimeType( mimeType); if (Objects.equals(mimeType, mimeTypeFromExt) || Objects.equals(ext, extFromMimeType)) { // Extension maps back to requested MIME type; allow it } else { // No match; insist that create file matches requested MIME name = displayName; ext = extFromMimeType; } } if (ext == null) { ext = ""; } return new String[] { name, ext }; } private static File buildFile(File parent, String name, String ext) { if (TextUtils.isEmpty(ext)) { return new File(parent, name); } else { return new File(parent, name + "." + ext); } } public static @NonNull String[] listOrEmpty(@Nullable File dir) { if (dir == null) return EmptyArray.STRING; final String[] res = dir.list(); if (res != null) { return res; } else { return EmptyArray.STRING; } } public static @NonNull File[] listFilesOrEmpty(@Nullable File dir) { if (dir == null) return EMPTY; final File[] res = dir.listFiles(); if (res != null) { return res; } else { return EMPTY; } } public static @NonNull File[] listFilesOrEmpty(@Nullable File dir, FilenameFilter filter) { if (dir == null) return EMPTY; final File[] res = dir.listFiles(filter); if (res != null) { return res; } else { return EMPTY; } } public static @Nullable File newFileOrNull(@Nullable String path) { return (path != null) ? new File(path) : null; } }