| /* |
| * 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.system.ErrnoException; |
| import android.text.TextUtils; |
| import android.system.Os; |
| import android.system.OsConstants; |
| import android.util.Log; |
| import android.util.Slog; |
| |
| 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.IOException; |
| import java.io.InputStream; |
| import java.util.Arrays; |
| import java.util.Comparator; |
| 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 */ |
| private static final Pattern SAFE_FILENAME_PATTERN = Pattern.compile("[\\w%+,./=_-]+"); |
| |
| /** |
| * 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; |
| } |
| |
| /** |
| * 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; |
| } |
| |
| // copy a file from srcFile to destFile, return true if succeed, return |
| // false if fail |
| public static boolean copyFile(File srcFile, File destFile) { |
| boolean result = false; |
| try { |
| InputStream in = new FileInputStream(srcFile); |
| try { |
| result = copyToFile(in, destFile); |
| } finally { |
| in.close(); |
| } |
| } catch (IOException e) { |
| result = false; |
| } |
| return result; |
| } |
| |
| /** |
| * Copy data from a source stream to destFile. |
| * Return true if succeed, return false if failed. |
| */ |
| public static boolean copyToFile(InputStream inputStream, File destFile) { |
| try { |
| 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(); |
| } |
| return true; |
| } catch (IOException e) { |
| return false; |
| } |
| } |
| |
| /** |
| * 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 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(); |
| } |
| } |
| |
| /** |
| * 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<File>() { |
| @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. |
| * <p> |
| * Both files <em>must</em> have been resolved using |
| * {@link File#getCanonicalFile()} to avoid symlink or path traversal |
| * attacks. |
| */ |
| public static boolean contains(File dir, File file) { |
| if (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 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; |
| } |
| |
| /** |
| * Assert that given filename is valid on ext4. |
| */ |
| public static boolean isValidExtFilename(String name) { |
| if (TextUtils.isEmpty(name) || ".".equals(name) || "..".equals(name)) { |
| return false; |
| } |
| for (int i = 0; i < name.length(); i++) { |
| final char c = name.charAt(i); |
| if (c == '\0' || c == '/') { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| 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) return null; |
| if (contains(beforeDir, file)) { |
| final String splice = file.getAbsolutePath().substring( |
| beforeDir.getAbsolutePath().length()); |
| return new File(afterDir, splice); |
| } |
| return null; |
| } |
| } |