Home | History | Annotate | Download | only in os
      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      */
     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      */
     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      */
    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      */
    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      */
    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
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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 
    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 
    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      */
    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      */
    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 
    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      */
    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      */
    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
    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 
    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 
    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 
    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      */
    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      */
    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 
    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 
    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