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 static android.system.OsConstants.SPLICE_F_MORE;
     20 import static android.system.OsConstants.SPLICE_F_MOVE;
     21 import static android.system.OsConstants.S_ISFIFO;
     22 import static android.system.OsConstants.S_ISREG;
     23 
     24 import android.annotation.NonNull;
     25 import android.annotation.Nullable;
     26 import android.provider.DocumentsContract.Document;
     27 import android.system.ErrnoException;
     28 import android.system.Os;
     29 import android.system.StructStat;
     30 import android.text.TextUtils;
     31 import android.util.Log;
     32 import android.util.Slog;
     33 import android.webkit.MimeTypeMap;
     34 
     35 import com.android.internal.annotations.VisibleForTesting;
     36 import com.android.internal.util.SizedInputStream;
     37 
     38 import libcore.io.IoUtils;
     39 import libcore.util.EmptyArray;
     40 
     41 import java.io.BufferedInputStream;
     42 import java.io.ByteArrayOutputStream;
     43 import java.io.File;
     44 import java.io.FileDescriptor;
     45 import java.io.FileInputStream;
     46 import java.io.FileNotFoundException;
     47 import java.io.FileOutputStream;
     48 import java.io.FilenameFilter;
     49 import java.io.IOException;
     50 import java.io.InputStream;
     51 import java.io.OutputStream;
     52 import java.nio.charset.StandardCharsets;
     53 import java.util.Arrays;
     54 import java.util.Comparator;
     55 import java.util.Objects;
     56 import java.util.concurrent.TimeUnit;
     57 import java.util.regex.Pattern;
     58 import java.util.zip.CRC32;
     59 import java.util.zip.CheckedInputStream;
     60 
     61 /**
     62  * Tools for managing files.  Not for public consumption.
     63  * @hide
     64  */
     65 public class FileUtils {
     66     private static final String TAG = "FileUtils";
     67 
     68     public static final int S_IRWXU = 00700;
     69     public static final int S_IRUSR = 00400;
     70     public static final int S_IWUSR = 00200;
     71     public static final int S_IXUSR = 00100;
     72 
     73     public static final int S_IRWXG = 00070;
     74     public static final int S_IRGRP = 00040;
     75     public static final int S_IWGRP = 00020;
     76     public static final int S_IXGRP = 00010;
     77 
     78     public static final int S_IRWXO = 00007;
     79     public static final int S_IROTH = 00004;
     80     public static final int S_IWOTH = 00002;
     81     public static final int S_IXOTH = 00001;
     82 
     83     /** Regular expression for safe filenames: no spaces or metacharacters.
     84       *
     85       * Use a preload holder so that FileUtils can be compile-time initialized.
     86       */
     87     private static class NoImagePreloadHolder {
     88         public static final Pattern SAFE_FILENAME_PATTERN = Pattern.compile("[\\w%+,./=_-]+");
     89     }
     90 
     91     private static final File[] EMPTY = new File[0];
     92 
     93     private static final boolean ENABLE_COPY_OPTIMIZATIONS = true;
     94 
     95     private static final long COPY_CHECKPOINT_BYTES = 524288;
     96 
     97     public interface ProgressListener {
     98         public void onProgress(long progress);
     99     }
    100 
    101     /**
    102      * Set owner and mode of of given {@link File}.
    103      *
    104      * @param mode to apply through {@code chmod}
    105      * @param uid to apply through {@code chown}, or -1 to leave unchanged
    106      * @param gid to apply through {@code chown}, or -1 to leave unchanged
    107      * @return 0 on success, otherwise errno.
    108      */
    109     public static int setPermissions(File path, int mode, int uid, int gid) {
    110         return setPermissions(path.getAbsolutePath(), mode, uid, gid);
    111     }
    112 
    113     /**
    114      * Set owner and mode of of given path.
    115      *
    116      * @param mode to apply through {@code chmod}
    117      * @param uid to apply through {@code chown}, or -1 to leave unchanged
    118      * @param gid to apply through {@code chown}, or -1 to leave unchanged
    119      * @return 0 on success, otherwise errno.
    120      */
    121     public static int setPermissions(String path, int mode, int uid, int gid) {
    122         try {
    123             Os.chmod(path, mode);
    124         } catch (ErrnoException e) {
    125             Slog.w(TAG, "Failed to chmod(" + path + "): " + e);
    126             return e.errno;
    127         }
    128 
    129         if (uid >= 0 || gid >= 0) {
    130             try {
    131                 Os.chown(path, uid, gid);
    132             } catch (ErrnoException e) {
    133                 Slog.w(TAG, "Failed to chown(" + path + "): " + e);
    134                 return e.errno;
    135             }
    136         }
    137 
    138         return 0;
    139     }
    140 
    141     /**
    142      * Set owner and mode of of given {@link FileDescriptor}.
    143      *
    144      * @param mode to apply through {@code chmod}
    145      * @param uid to apply through {@code chown}, or -1 to leave unchanged
    146      * @param gid to apply through {@code chown}, or -1 to leave unchanged
    147      * @return 0 on success, otherwise errno.
    148      */
    149     public static int setPermissions(FileDescriptor fd, int mode, int uid, int gid) {
    150         try {
    151             Os.fchmod(fd, mode);
    152         } catch (ErrnoException e) {
    153             Slog.w(TAG, "Failed to fchmod(): " + e);
    154             return e.errno;
    155         }
    156 
    157         if (uid >= 0 || gid >= 0) {
    158             try {
    159                 Os.fchown(fd, uid, gid);
    160             } catch (ErrnoException e) {
    161                 Slog.w(TAG, "Failed to fchown(): " + e);
    162                 return e.errno;
    163             }
    164         }
    165 
    166         return 0;
    167     }
    168 
    169     public static void copyPermissions(File from, File to) throws IOException {
    170         try {
    171             final StructStat stat = Os.stat(from.getAbsolutePath());
    172             Os.chmod(to.getAbsolutePath(), stat.st_mode);
    173             Os.chown(to.getAbsolutePath(), stat.st_uid, stat.st_gid);
    174         } catch (ErrnoException e) {
    175             throw e.rethrowAsIOException();
    176         }
    177     }
    178 
    179     /**
    180      * Return owning UID of given path, otherwise -1.
    181      */
    182     public static int getUid(String path) {
    183         try {
    184             return Os.stat(path).st_uid;
    185         } catch (ErrnoException e) {
    186             return -1;
    187         }
    188     }
    189 
    190     /**
    191      * Perform an fsync on the given FileOutputStream.  The stream at this
    192      * point must be flushed but not yet closed.
    193      */
    194     public static boolean sync(FileOutputStream stream) {
    195         try {
    196             if (stream != null) {
    197                 stream.getFD().sync();
    198             }
    199             return true;
    200         } catch (IOException e) {
    201         }
    202         return false;
    203     }
    204 
    205     /**
    206      * @deprecated use {@link #copy(File, File)} instead.
    207      */
    208     @Deprecated
    209     public static boolean copyFile(File srcFile, File destFile) {
    210         try {
    211             copyFileOrThrow(srcFile, destFile);
    212             return true;
    213         } catch (IOException e) {
    214             return false;
    215         }
    216     }
    217 
    218     /**
    219      * @deprecated use {@link #copy(File, File)} instead.
    220      */
    221     @Deprecated
    222     public static void copyFileOrThrow(File srcFile, File destFile) throws IOException {
    223         try (InputStream in = new FileInputStream(srcFile)) {
    224             copyToFileOrThrow(in, destFile);
    225         }
    226     }
    227 
    228     /**
    229      * @deprecated use {@link #copy(InputStream, OutputStream)} instead.
    230      */
    231     @Deprecated
    232     public static boolean copyToFile(InputStream inputStream, File destFile) {
    233         try {
    234             copyToFileOrThrow(inputStream, destFile);
    235             return true;
    236         } catch (IOException e) {
    237             return false;
    238         }
    239     }
    240 
    241     /**
    242      * @deprecated use {@link #copy(InputStream, OutputStream)} instead.
    243      */
    244     @Deprecated
    245     public static void copyToFileOrThrow(InputStream in, File destFile) throws IOException {
    246         if (destFile.exists()) {
    247             destFile.delete();
    248         }
    249         try (FileOutputStream out = new FileOutputStream(destFile)) {
    250             copy(in, out);
    251             try {
    252                 Os.fsync(out.getFD());
    253             } catch (ErrnoException e) {
    254                 throw e.rethrowAsIOException();
    255             }
    256         }
    257     }
    258 
    259     /**
    260      * Copy the contents of one file to another, replacing any existing content.
    261      * <p>
    262      * Attempts to use several optimization strategies to copy the data in the
    263      * kernel before falling back to a userspace copy as a last resort.
    264      *
    265      * @return number of bytes copied.
    266      */
    267     public static long copy(@NonNull File from, @NonNull File to) throws IOException {
    268         return copy(from, to, null, null);
    269     }
    270 
    271     /**
    272      * Copy the contents of one file to another, replacing any existing content.
    273      * <p>
    274      * Attempts to use several optimization strategies to copy the data in the
    275      * kernel before falling back to a userspace copy as a last resort.
    276      *
    277      * @param listener to be periodically notified as the copy progresses.
    278      * @param signal to signal if the copy should be cancelled early.
    279      * @return number of bytes copied.
    280      */
    281     public static long copy(@NonNull File from, @NonNull File to,
    282             @Nullable ProgressListener listener, @Nullable CancellationSignal signal)
    283             throws IOException {
    284         try (FileInputStream in = new FileInputStream(from);
    285                 FileOutputStream out = new FileOutputStream(to)) {
    286             return copy(in, out, listener, signal);
    287         }
    288     }
    289 
    290     /**
    291      * Copy the contents of one stream to another.
    292      * <p>
    293      * Attempts to use several optimization strategies to copy the data in the
    294      * kernel before falling back to a userspace copy as a last resort.
    295      *
    296      * @return number of bytes copied.
    297      */
    298     public static long copy(@NonNull InputStream in, @NonNull OutputStream out) throws IOException {
    299         return copy(in, out, null, null);
    300     }
    301 
    302     /**
    303      * Copy the contents of one stream to another.
    304      * <p>
    305      * Attempts to use several optimization strategies to copy the data in the
    306      * kernel before falling back to a userspace copy as a last resort.
    307      *
    308      * @param listener to be periodically notified as the copy progresses.
    309      * @param signal to signal if the copy should be cancelled early.
    310      * @return number of bytes copied.
    311      */
    312     public static long copy(@NonNull InputStream in, @NonNull OutputStream out,
    313             @Nullable ProgressListener listener, @Nullable CancellationSignal signal)
    314             throws IOException {
    315         if (ENABLE_COPY_OPTIMIZATIONS) {
    316             if (in instanceof FileInputStream && out instanceof FileOutputStream) {
    317                 return copy(((FileInputStream) in).getFD(), ((FileOutputStream) out).getFD(),
    318                         listener, signal);
    319             }
    320         }
    321 
    322         // Worse case fallback to userspace
    323         return copyInternalUserspace(in, out, listener, signal);
    324     }
    325 
    326     /**
    327      * Copy the contents of one FD to another.
    328      * <p>
    329      * Attempts to use several optimization strategies to copy the data in the
    330      * kernel before falling back to a userspace copy as a last resort.
    331      *
    332      * @return number of bytes copied.
    333      */
    334     public static long copy(@NonNull FileDescriptor in, @NonNull FileDescriptor out)
    335             throws IOException {
    336         return copy(in, out, null, null);
    337     }
    338 
    339     /**
    340      * Copy the contents of one FD to another.
    341      * <p>
    342      * Attempts to use several optimization strategies to copy the data in the
    343      * kernel before falling back to a userspace copy as a last resort.
    344      *
    345      * @param listener to be periodically notified as the copy progresses.
    346      * @param signal to signal if the copy should be cancelled early.
    347      * @return number of bytes copied.
    348      */
    349     public static long copy(@NonNull FileDescriptor in, @NonNull FileDescriptor out,
    350             @Nullable ProgressListener listener, @Nullable CancellationSignal signal)
    351             throws IOException {
    352         return copy(in, out, listener, signal, Long.MAX_VALUE);
    353     }
    354 
    355     /**
    356      * Copy the contents of one FD to another.
    357      * <p>
    358      * Attempts to use several optimization strategies to copy the data in the
    359      * kernel before falling back to a userspace copy as a last resort.
    360      *
    361      * @param listener to be periodically notified as the copy progresses.
    362      * @param signal to signal if the copy should be cancelled early.
    363      * @param count the number of bytes to copy.
    364      * @return number of bytes copied.
    365      */
    366     public static long copy(@NonNull FileDescriptor in, @NonNull FileDescriptor out,
    367             @Nullable ProgressListener listener, @Nullable CancellationSignal signal, long count)
    368             throws IOException {
    369         if (ENABLE_COPY_OPTIMIZATIONS) {
    370             try {
    371                 final StructStat st_in = Os.fstat(in);
    372                 final StructStat st_out = Os.fstat(out);
    373                 if (S_ISREG(st_in.st_mode) && S_ISREG(st_out.st_mode)) {
    374                     return copyInternalSendfile(in, out, listener, signal, count);
    375                 } else if (S_ISFIFO(st_in.st_mode) || S_ISFIFO(st_out.st_mode)) {
    376                     return copyInternalSplice(in, out, listener, signal, count);
    377                 }
    378             } catch (ErrnoException e) {
    379                 throw e.rethrowAsIOException();
    380             }
    381         }
    382 
    383         // Worse case fallback to userspace
    384         return copyInternalUserspace(in, out, listener, signal, count);
    385     }
    386 
    387     /**
    388      * Requires one of input or output to be a pipe.
    389      */
    390     @VisibleForTesting
    391     public static long copyInternalSplice(FileDescriptor in, FileDescriptor out,
    392             ProgressListener listener, CancellationSignal signal, long count)
    393             throws ErrnoException {
    394         long progress = 0;
    395         long checkpoint = 0;
    396 
    397         long t;
    398         while ((t = Os.splice(in, null, out, null, Math.min(count, COPY_CHECKPOINT_BYTES),
    399                 SPLICE_F_MOVE | SPLICE_F_MORE)) != 0) {
    400             progress += t;
    401             checkpoint += t;
    402             count -= t;
    403 
    404             if (checkpoint >= COPY_CHECKPOINT_BYTES) {
    405                 if (signal != null) {
    406                     signal.throwIfCanceled();
    407                 }
    408                 if (listener != null) {
    409                     listener.onProgress(progress);
    410                 }
    411                 checkpoint = 0;
    412             }
    413         }
    414         if (listener != null) {
    415             listener.onProgress(progress);
    416         }
    417         return progress;
    418     }
    419 
    420     /**
    421      * Requires both input and output to be a regular file.
    422      */
    423     @VisibleForTesting
    424     public static long copyInternalSendfile(FileDescriptor in, FileDescriptor out,
    425             ProgressListener listener, CancellationSignal signal, long count)
    426             throws ErrnoException {
    427         long progress = 0;
    428         long checkpoint = 0;
    429 
    430         long t;
    431         while ((t = Os.sendfile(out, in, null, Math.min(count, COPY_CHECKPOINT_BYTES))) != 0) {
    432             progress += t;
    433             checkpoint += t;
    434             count -= t;
    435 
    436             if (checkpoint >= COPY_CHECKPOINT_BYTES) {
    437                 if (signal != null) {
    438                     signal.throwIfCanceled();
    439                 }
    440                 if (listener != null) {
    441                     listener.onProgress(progress);
    442                 }
    443                 checkpoint = 0;
    444             }
    445         }
    446         if (listener != null) {
    447             listener.onProgress(progress);
    448         }
    449         return progress;
    450     }
    451 
    452     @VisibleForTesting
    453     public static long copyInternalUserspace(FileDescriptor in, FileDescriptor out,
    454             ProgressListener listener, CancellationSignal signal, long count) throws IOException {
    455         if (count != Long.MAX_VALUE) {
    456             return copyInternalUserspace(new SizedInputStream(new FileInputStream(in), count),
    457                     new FileOutputStream(out), listener, signal);
    458         } else {
    459             return copyInternalUserspace(new FileInputStream(in),
    460                     new FileOutputStream(out), listener, signal);
    461         }
    462     }
    463 
    464     @VisibleForTesting
    465     public static long copyInternalUserspace(InputStream in, OutputStream out,
    466             ProgressListener listener, CancellationSignal signal) throws IOException {
    467         long progress = 0;
    468         long checkpoint = 0;
    469         byte[] buffer = new byte[8192];
    470 
    471         int t;
    472         while ((t = in.read(buffer)) != -1) {
    473             out.write(buffer, 0, t);
    474 
    475             progress += t;
    476             checkpoint += t;
    477 
    478             if (checkpoint >= COPY_CHECKPOINT_BYTES) {
    479                 if (signal != null) {
    480                     signal.throwIfCanceled();
    481                 }
    482                 if (listener != null) {
    483                     listener.onProgress(progress);
    484                 }
    485                 checkpoint = 0;
    486             }
    487         }
    488         if (listener != null) {
    489             listener.onProgress(progress);
    490         }
    491         return progress;
    492     }
    493 
    494     /**
    495      * Check if a filename is "safe" (no metacharacters or spaces).
    496      * @param file  The file to check
    497      */
    498     public static boolean isFilenameSafe(File file) {
    499         // Note, we check whether it matches what's known to be safe,
    500         // rather than what's known to be unsafe.  Non-ASCII, control
    501         // characters, etc. are all unsafe by default.
    502         return NoImagePreloadHolder.SAFE_FILENAME_PATTERN.matcher(file.getPath()).matches();
    503     }
    504 
    505     /**
    506      * Read a text file into a String, optionally limiting the length.
    507      * @param file to read (will not seek, so things like /proc files are OK)
    508      * @param max length (positive for head, negative of tail, 0 for no limit)
    509      * @param ellipsis to add of the file was truncated (can be null)
    510      * @return the contents of the file, possibly truncated
    511      * @throws IOException if something goes wrong reading the file
    512      */
    513     public static String readTextFile(File file, int max, String ellipsis) throws IOException {
    514         InputStream input = new FileInputStream(file);
    515         // wrapping a BufferedInputStream around it because when reading /proc with unbuffered
    516         // input stream, bytes read not equal to buffer size is not necessarily the correct
    517         // indication for EOF; but it is true for BufferedInputStream due to its implementation.
    518         BufferedInputStream bis = new BufferedInputStream(input);
    519         try {
    520             long size = file.length();
    521             if (max > 0 || (size > 0 && max == 0)) {  // "head" mode: read the first N bytes
    522                 if (size > 0 && (max == 0 || size < max)) max = (int) size;
    523                 byte[] data = new byte[max + 1];
    524                 int length = bis.read(data);
    525                 if (length <= 0) return "";
    526                 if (length <= max) return new String(data, 0, length);
    527                 if (ellipsis == null) return new String(data, 0, max);
    528                 return new String(data, 0, max) + ellipsis;
    529             } else if (max < 0) {  // "tail" mode: keep the last N
    530                 int len;
    531                 boolean rolled = false;
    532                 byte[] last = null;
    533                 byte[] data = null;
    534                 do {
    535                     if (last != null) rolled = true;
    536                     byte[] tmp = last; last = data; data = tmp;
    537                     if (data == null) data = new byte[-max];
    538                     len = bis.read(data);
    539                 } while (len == data.length);
    540 
    541                 if (last == null && len <= 0) return "";
    542                 if (last == null) return new String(data, 0, len);
    543                 if (len > 0) {
    544                     rolled = true;
    545                     System.arraycopy(last, len, last, 0, last.length - len);
    546                     System.arraycopy(data, 0, last, last.length - len, len);
    547                 }
    548                 if (ellipsis == null || !rolled) return new String(last);
    549                 return ellipsis + new String(last);
    550             } else {  // "cat" mode: size unknown, read it all in streaming fashion
    551                 ByteArrayOutputStream contents = new ByteArrayOutputStream();
    552                 int len;
    553                 byte[] data = new byte[1024];
    554                 do {
    555                     len = bis.read(data);
    556                     if (len > 0) contents.write(data, 0, len);
    557                 } while (len == data.length);
    558                 return contents.toString();
    559             }
    560         } finally {
    561             bis.close();
    562             input.close();
    563         }
    564     }
    565 
    566     public static void stringToFile(File file, String string) throws IOException {
    567         stringToFile(file.getAbsolutePath(), string);
    568     }
    569 
    570     /*
    571      * Writes the bytes given in {@code content} to the file whose absolute path
    572      * is {@code filename}.
    573      */
    574     public static void bytesToFile(String filename, byte[] content) throws IOException {
    575         if (filename.startsWith("/proc/")) {
    576             final int oldMask = StrictMode.allowThreadDiskWritesMask();
    577             try (FileOutputStream fos = new FileOutputStream(filename)) {
    578                 fos.write(content);
    579             } finally {
    580                 StrictMode.setThreadPolicyMask(oldMask);
    581             }
    582         } else {
    583             try (FileOutputStream fos = new FileOutputStream(filename)) {
    584                 fos.write(content);
    585             }
    586         }
    587     }
    588 
    589     /**
    590      * Writes string to file. Basically same as "echo -n $string > $filename"
    591      *
    592      * @param filename
    593      * @param string
    594      * @throws IOException
    595      */
    596     public static void stringToFile(String filename, String string) throws IOException {
    597         bytesToFile(filename, string.getBytes(StandardCharsets.UTF_8));
    598     }
    599 
    600     /**
    601      * Computes the checksum of a file using the CRC32 checksum routine.
    602      * The value of the checksum is returned.
    603      *
    604      * @param file  the file to checksum, must not be null
    605      * @return the checksum value or an exception is thrown.
    606      */
    607     public static long checksumCrc32(File file) throws FileNotFoundException, IOException {
    608         CRC32 checkSummer = new CRC32();
    609         CheckedInputStream cis = null;
    610 
    611         try {
    612             cis = new CheckedInputStream( new FileInputStream(file), checkSummer);
    613             byte[] buf = new byte[128];
    614             while(cis.read(buf) >= 0) {
    615                 // Just read for checksum to get calculated.
    616             }
    617             return checkSummer.getValue();
    618         } finally {
    619             if (cis != null) {
    620                 try {
    621                     cis.close();
    622                 } catch (IOException e) {
    623                 }
    624             }
    625         }
    626     }
    627 
    628     /**
    629      * Delete older files in a directory until only those matching the given
    630      * constraints remain.
    631      *
    632      * @param minCount Always keep at least this many files.
    633      * @param minAgeMs Always keep files younger than this age, in milliseconds.
    634      * @return if any files were deleted.
    635      */
    636     public static boolean deleteOlderFiles(File dir, int minCount, long minAgeMs) {
    637         if (minCount < 0 || minAgeMs < 0) {
    638             throw new IllegalArgumentException("Constraints must be positive or 0");
    639         }
    640 
    641         final File[] files = dir.listFiles();
    642         if (files == null) return false;
    643 
    644         // Sort with newest files first
    645         Arrays.sort(files, new Comparator<File>() {
    646             @Override
    647             public int compare(File lhs, File rhs) {
    648                 return Long.compare(rhs.lastModified(), lhs.lastModified());
    649             }
    650         });
    651 
    652         // Keep at least minCount files
    653         boolean deleted = false;
    654         for (int i = minCount; i < files.length; i++) {
    655             final File file = files[i];
    656 
    657             // Keep files newer than minAgeMs
    658             final long age = System.currentTimeMillis() - file.lastModified();
    659             if (age > minAgeMs) {
    660                 if (file.delete()) {
    661                     Log.d(TAG, "Deleted old file " + file);
    662                     deleted = true;
    663                 }
    664             }
    665         }
    666         return deleted;
    667     }
    668 
    669     /**
    670      * Test if a file lives under the given directory, either as a direct child
    671      * or a distant grandchild.
    672      * <p>
    673      * Both files <em>must</em> have been resolved using
    674      * {@link File#getCanonicalFile()} to avoid symlink or path traversal
    675      * attacks.
    676      */
    677     public static boolean contains(File[] dirs, File file) {
    678         for (File dir : dirs) {
    679             if (contains(dir, file)) {
    680                 return true;
    681             }
    682         }
    683         return false;
    684     }
    685 
    686     /**
    687      * Test if a file lives under the given directory, either as a direct child
    688      * or a distant grandchild.
    689      * <p>
    690      * Both files <em>must</em> have been resolved using
    691      * {@link File#getCanonicalFile()} to avoid symlink or path traversal
    692      * attacks.
    693      */
    694     public static boolean contains(File dir, File file) {
    695         if (dir == null || file == null) return false;
    696         return contains(dir.getAbsolutePath(), file.getAbsolutePath());
    697     }
    698 
    699     public static boolean contains(String dirPath, String filePath) {
    700         if (dirPath.equals(filePath)) {
    701             return true;
    702         }
    703         if (!dirPath.endsWith("/")) {
    704             dirPath += "/";
    705         }
    706         return filePath.startsWith(dirPath);
    707     }
    708 
    709     public static boolean deleteContentsAndDir(File dir) {
    710         if (deleteContents(dir)) {
    711             return dir.delete();
    712         } else {
    713             return false;
    714         }
    715     }
    716 
    717     public static boolean deleteContents(File dir) {
    718         File[] files = dir.listFiles();
    719         boolean success = true;
    720         if (files != null) {
    721             for (File file : files) {
    722                 if (file.isDirectory()) {
    723                     success &= deleteContents(file);
    724                 }
    725                 if (!file.delete()) {
    726                     Log.w(TAG, "Failed to delete " + file);
    727                     success = false;
    728                 }
    729             }
    730         }
    731         return success;
    732     }
    733 
    734     private static boolean isValidExtFilenameChar(char c) {
    735         switch (c) {
    736             case '\0':
    737             case '/':
    738                 return false;
    739             default:
    740                 return true;
    741         }
    742     }
    743 
    744     /**
    745      * Check if given filename is valid for an ext4 filesystem.
    746      */
    747     public static boolean isValidExtFilename(String name) {
    748         return (name != null) && name.equals(buildValidExtFilename(name));
    749     }
    750 
    751     /**
    752      * Mutate the given filename to make it valid for an ext4 filesystem,
    753      * replacing any invalid characters with "_".
    754      */
    755     public static String buildValidExtFilename(String name) {
    756         if (TextUtils.isEmpty(name) || ".".equals(name) || "..".equals(name)) {
    757             return "(invalid)";
    758         }
    759         final StringBuilder res = new StringBuilder(name.length());
    760         for (int i = 0; i < name.length(); i++) {
    761             final char c = name.charAt(i);
    762             if (isValidExtFilenameChar(c)) {
    763                 res.append(c);
    764             } else {
    765                 res.append('_');
    766             }
    767         }
    768         trimFilename(res, 255);
    769         return res.toString();
    770     }
    771 
    772     private static boolean isValidFatFilenameChar(char c) {
    773         if ((0x00 <= c && c <= 0x1f)) {
    774             return false;
    775         }
    776         switch (c) {
    777             case '"':
    778             case '*':
    779             case '/':
    780             case ':':
    781             case '<':
    782             case '>':
    783             case '?':
    784             case '\\':
    785             case '|':
    786             case 0x7F:
    787                 return false;
    788             default:
    789                 return true;
    790         }
    791     }
    792 
    793     /**
    794      * Check if given filename is valid for a FAT filesystem.
    795      */
    796     public static boolean isValidFatFilename(String name) {
    797         return (name != null) && name.equals(buildValidFatFilename(name));
    798     }
    799 
    800     /**
    801      * Mutate the given filename to make it valid for a FAT filesystem,
    802      * replacing any invalid characters with "_".
    803      */
    804     public static String buildValidFatFilename(String name) {
    805         if (TextUtils.isEmpty(name) || ".".equals(name) || "..".equals(name)) {
    806             return "(invalid)";
    807         }
    808         final StringBuilder res = new StringBuilder(name.length());
    809         for (int i = 0; i < name.length(); i++) {
    810             final char c = name.charAt(i);
    811             if (isValidFatFilenameChar(c)) {
    812                 res.append(c);
    813             } else {
    814                 res.append('_');
    815             }
    816         }
    817         // Even though vfat allows 255 UCS-2 chars, we might eventually write to
    818         // ext4 through a FUSE layer, so use that limit.
    819         trimFilename(res, 255);
    820         return res.toString();
    821     }
    822 
    823     @VisibleForTesting
    824     public static String trimFilename(String str, int maxBytes) {
    825         final StringBuilder res = new StringBuilder(str);
    826         trimFilename(res, maxBytes);
    827         return res.toString();
    828     }
    829 
    830     private static void trimFilename(StringBuilder res, int maxBytes) {
    831         byte[] raw = res.toString().getBytes(StandardCharsets.UTF_8);
    832         if (raw.length > maxBytes) {
    833             maxBytes -= 3;
    834             while (raw.length > maxBytes) {
    835                 res.deleteCharAt(res.length() / 2);
    836                 raw = res.toString().getBytes(StandardCharsets.UTF_8);
    837             }
    838             res.insert(res.length() / 2, "...");
    839         }
    840     }
    841 
    842     public static String rewriteAfterRename(File beforeDir, File afterDir, String path) {
    843         if (path == null) return null;
    844         final File result = rewriteAfterRename(beforeDir, afterDir, new File(path));
    845         return (result != null) ? result.getAbsolutePath() : null;
    846     }
    847 
    848     public static String[] rewriteAfterRename(File beforeDir, File afterDir, String[] paths) {
    849         if (paths == null) return null;
    850         final String[] result = new String[paths.length];
    851         for (int i = 0; i < paths.length; i++) {
    852             result[i] = rewriteAfterRename(beforeDir, afterDir, paths[i]);
    853         }
    854         return result;
    855     }
    856 
    857     /**
    858      * Given a path under the "before" directory, rewrite it to live under the
    859      * "after" directory. For example, {@code /before/foo/bar.txt} would become
    860      * {@code /after/foo/bar.txt}.
    861      */
    862     public static File rewriteAfterRename(File beforeDir, File afterDir, File file) {
    863         if (file == null || beforeDir == null || afterDir == null) return null;
    864         if (contains(beforeDir, file)) {
    865             final String splice = file.getAbsolutePath().substring(
    866                     beforeDir.getAbsolutePath().length());
    867             return new File(afterDir, splice);
    868         }
    869         return null;
    870     }
    871 
    872     private static File buildUniqueFileWithExtension(File parent, String name, String ext)
    873             throws FileNotFoundException {
    874         File file = buildFile(parent, name, ext);
    875 
    876         // If conflicting file, try adding counter suffix
    877         int n = 0;
    878         while (file.exists()) {
    879             if (n++ >= 32) {
    880                 throw new FileNotFoundException("Failed to create unique file");
    881             }
    882             file = buildFile(parent, name + " (" + n + ")", ext);
    883         }
    884 
    885         return file;
    886     }
    887 
    888     /**
    889      * Generates a unique file name under the given parent directory. If the display name doesn't
    890      * have an extension that matches the requested MIME type, the default extension for that MIME
    891      * type is appended. If a file already exists, the name is appended with a numerical value to
    892      * make it unique.
    893      *
    894      * For example, the display name 'example' with 'text/plain' MIME might produce
    895      * 'example.txt' or 'example (1).txt', etc.
    896      *
    897      * @throws FileNotFoundException
    898      */
    899     public static File buildUniqueFile(File parent, String mimeType, String displayName)
    900             throws FileNotFoundException {
    901         final String[] parts = splitFileName(mimeType, displayName);
    902         return buildUniqueFileWithExtension(parent, parts[0], parts[1]);
    903     }
    904 
    905     /**
    906      * Generates a unique file name under the given parent directory, keeping
    907      * any extension intact.
    908      */
    909     public static File buildUniqueFile(File parent, String displayName)
    910             throws FileNotFoundException {
    911         final String name;
    912         final String ext;
    913 
    914         // Extract requested extension from display name
    915         final int lastDot = displayName.lastIndexOf('.');
    916         if (lastDot >= 0) {
    917             name = displayName.substring(0, lastDot);
    918             ext = displayName.substring(lastDot + 1);
    919         } else {
    920             name = displayName;
    921             ext = null;
    922         }
    923 
    924         return buildUniqueFileWithExtension(parent, name, ext);
    925     }
    926 
    927     /**
    928      * Splits file name into base name and extension.
    929      * If the display name doesn't have an extension that matches the requested MIME type, the
    930      * extension is regarded as a part of filename and default extension for that MIME type is
    931      * appended.
    932      */
    933     public static String[] splitFileName(String mimeType, String displayName) {
    934         String name;
    935         String ext;
    936 
    937         if (Document.MIME_TYPE_DIR.equals(mimeType)) {
    938             name = displayName;
    939             ext = null;
    940         } else {
    941             String mimeTypeFromExt;
    942 
    943             // Extract requested extension from display name
    944             final int lastDot = displayName.lastIndexOf('.');
    945             if (lastDot >= 0) {
    946                 name = displayName.substring(0, lastDot);
    947                 ext = displayName.substring(lastDot + 1);
    948                 mimeTypeFromExt = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
    949                         ext.toLowerCase());
    950             } else {
    951                 name = displayName;
    952                 ext = null;
    953                 mimeTypeFromExt = null;
    954             }
    955 
    956             if (mimeTypeFromExt == null) {
    957                 mimeTypeFromExt = "application/octet-stream";
    958             }
    959 
    960             final String extFromMimeType = MimeTypeMap.getSingleton().getExtensionFromMimeType(
    961                     mimeType);
    962             if (Objects.equals(mimeType, mimeTypeFromExt) || Objects.equals(ext, extFromMimeType)) {
    963                 // Extension maps back to requested MIME type; allow it
    964             } else {
    965                 // No match; insist that create file matches requested MIME
    966                 name = displayName;
    967                 ext = extFromMimeType;
    968             }
    969         }
    970 
    971         if (ext == null) {
    972             ext = "";
    973         }
    974 
    975         return new String[] { name, ext };
    976     }
    977 
    978     private static File buildFile(File parent, String name, String ext) {
    979         if (TextUtils.isEmpty(ext)) {
    980             return new File(parent, name);
    981         } else {
    982             return new File(parent, name + "." + ext);
    983         }
    984     }
    985 
    986     public static @NonNull String[] listOrEmpty(@Nullable File dir) {
    987         if (dir == null) return EmptyArray.STRING;
    988         final String[] res = dir.list();
    989         if (res != null) {
    990             return res;
    991         } else {
    992             return EmptyArray.STRING;
    993         }
    994     }
    995 
    996     public static @NonNull File[] listFilesOrEmpty(@Nullable File dir) {
    997         if (dir == null) return EMPTY;
    998         final File[] res = dir.listFiles();
    999         if (res != null) {
   1000             return res;
   1001         } else {
   1002             return EMPTY;
   1003         }
   1004     }
   1005 
   1006     public static @NonNull File[] listFilesOrEmpty(@Nullable File dir, FilenameFilter filter) {
   1007         if (dir == null) return EMPTY;
   1008         final File[] res = dir.listFiles(filter);
   1009         if (res != null) {
   1010             return res;
   1011         } else {
   1012             return EMPTY;
   1013         }
   1014     }
   1015 
   1016     public static @Nullable File newFileOrNull(@Nullable String path) {
   1017         return (path != null) ? new File(path) : null;
   1018     }
   1019 
   1020     /**
   1021      * Creates a directory with name {@code name} under an existing directory {@code baseDir}.
   1022      * Returns a {@code File} object representing the directory on success, {@code null} on
   1023      * failure.
   1024      */
   1025     public static @Nullable File createDir(File baseDir, String name) {
   1026         final File dir = new File(baseDir, name);
   1027 
   1028         if (dir.exists()) {
   1029             return dir.isDirectory() ? dir : null;
   1030         }
   1031 
   1032         return dir.mkdir() ? dir : null;
   1033     }
   1034 
   1035     /**
   1036      * Round the given size of a storage device to a nice round power-of-two
   1037      * value, such as 256MB or 32GB. This avoids showing weird values like
   1038      * "29.5GB" in UI.
   1039      */
   1040     public static long roundStorageSize(long size) {
   1041         long val = 1;
   1042         long pow = 1;
   1043         while ((val * pow) < size) {
   1044             val <<= 1;
   1045             if (val > 512) {
   1046                 val = 1;
   1047                 pow *= 1000;
   1048             }
   1049         }
   1050         return val * pow;
   1051     }
   1052 
   1053     @VisibleForTesting
   1054     public static class MemoryPipe extends Thread implements AutoCloseable {
   1055         private final FileDescriptor[] pipe;
   1056         private final byte[] data;
   1057         private final boolean sink;
   1058 
   1059         private MemoryPipe(byte[] data, boolean sink) throws IOException {
   1060             try {
   1061                 this.pipe = Os.pipe();
   1062             } catch (ErrnoException e) {
   1063                 throw e.rethrowAsIOException();
   1064             }
   1065             this.data = data;
   1066             this.sink = sink;
   1067         }
   1068 
   1069         private MemoryPipe startInternal() {
   1070             super.start();
   1071             return this;
   1072         }
   1073 
   1074         public static MemoryPipe createSource(byte[] data) throws IOException {
   1075             return new MemoryPipe(data, false).startInternal();
   1076         }
   1077 
   1078         public static MemoryPipe createSink(byte[] data) throws IOException {
   1079             return new MemoryPipe(data, true).startInternal();
   1080         }
   1081 
   1082         public FileDescriptor getFD() {
   1083             return sink ? pipe[1] : pipe[0];
   1084         }
   1085 
   1086         public FileDescriptor getInternalFD() {
   1087             return sink ? pipe[0] : pipe[1];
   1088         }
   1089 
   1090         @Override
   1091         public void run() {
   1092             final FileDescriptor fd = getInternalFD();
   1093             try {
   1094                 int i = 0;
   1095                 while (i < data.length) {
   1096                     if (sink) {
   1097                         i += Os.read(fd, data, i, data.length - i);
   1098                     } else {
   1099                         i += Os.write(fd, data, i, data.length - i);
   1100                     }
   1101                 }
   1102             } catch (IOException | ErrnoException e) {
   1103                 // Ignored
   1104             } finally {
   1105                 if (sink) {
   1106                     SystemClock.sleep(TimeUnit.SECONDS.toMillis(1));
   1107                 }
   1108                 IoUtils.closeQuietly(fd);
   1109             }
   1110         }
   1111 
   1112         @Override
   1113         public void close() throws Exception {
   1114             IoUtils.closeQuietly(getFD());
   1115         }
   1116     }
   1117 }
   1118