1/*
2 * Copyright (C) 2015 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 */
16package libcore.tzdata.update;
17
18import java.io.BufferedReader;
19import java.io.File;
20import java.io.FileInputStream;
21import java.io.IOException;
22import java.io.InputStreamReader;
23import java.nio.charset.StandardCharsets;
24import java.util.ArrayList;
25import java.util.Arrays;
26import java.util.LinkedList;
27import java.util.List;
28import java.util.zip.CRC32;
29
30/**
31 * Utility methods for files operations.
32 */
33public final class FileUtils {
34
35    private FileUtils() {
36    }
37
38    /**
39     * Creates a new {@link java.io.File} from the {@code parentDir} and {@code name}, but only if
40     * the resulting file would exist beneath {@code parentDir}. Useful if {@code name} could
41     * contain "/../" or symlinks. The returned object has a canonicalized path.
42     *
43     * @throws java.io.IOException if the file would not exist beneath {@code parentDir}
44     */
45    public static File createSubFile(File parentDir, String name) throws IOException {
46        // The subFile must exist beneath parentDir. If name contains "/../" this may not be the
47        // case so we check.
48        File subFile = new File(parentDir, name).getCanonicalFile();
49        if (!subFile.getPath().startsWith(parentDir.getCanonicalPath())) {
50            throw new IOException(name + " must exist beneath " + parentDir +
51                    ". Canonicalized subpath: " + subFile);
52        }
53        return subFile;
54    }
55
56    /**
57     * Makes sure a directory exists. If it doesn't exist, it is created. Parent directories are
58     * also created as needed. If {@code makeWorldReadable} is {@code true} the directory's default
59     * permissions will be set. Even when {@code makeWorldReadable} is {@code true}, only
60     * directories explicitly created will have their permissions set; existing directories are
61     * untouched.
62     *
63     * @throws IOException if the directory or one of its parents did not already exist and could
64     *     not be created
65     */
66    public static void ensureDirectoriesExist(File dir, boolean makeWorldReadable)
67            throws IOException {
68        LinkedList<File> dirs = new LinkedList<>();
69        File currentDir = dir;
70        do {
71            dirs.addFirst(currentDir);
72            currentDir = currentDir.getParentFile();
73        } while (currentDir != null);
74
75        for (File dirToCheck : dirs) {
76            if (!dirToCheck.exists()) {
77                if (!dirToCheck.mkdir()) {
78                    throw new IOException("Unable to create directory: " + dir);
79                }
80                if (makeWorldReadable) {
81                    makeDirectoryWorldAccessible(dirToCheck);
82                }
83            } else if (!dirToCheck.isDirectory()) {
84                throw new IOException(dirToCheck + " exists but is not a directory");
85            }
86        }
87    }
88
89    public static void makeDirectoryWorldAccessible(File directory) throws IOException {
90        if (!directory.isDirectory()) {
91            throw new IOException(directory + " must be a directory");
92        }
93        makeWorldReadable(directory);
94        if (!directory.setExecutable(true, false /* ownerOnly */)) {
95            throw new IOException("Unable to make " + directory + " world-executable");
96        }
97    }
98
99    public static void makeWorldReadable(File file) throws IOException {
100        if (!file.setReadable(true, false /* ownerOnly */)) {
101            throw new IOException("Unable to make " + file + " world-readable");
102        }
103    }
104
105    /**
106     * Calculates the checksum from the contents of a file.
107     */
108    public static long calculateChecksum(File file) throws IOException {
109        final int BUFFER_SIZE = 8196;
110        CRC32 crc32 = new CRC32();
111        try (FileInputStream fis = new FileInputStream(file)) {
112            byte[] buffer = new byte[BUFFER_SIZE];
113            int count;
114            while ((count = fis.read(buffer)) != -1) {
115                crc32.update(buffer, 0, count);
116            }
117        }
118        return crc32.getValue();
119    }
120
121    public static void rename(File from, File to) throws IOException {
122        ensureFileDoesNotExist(to);
123        if (!from.renameTo(to)) {
124            throw new IOException("Unable to rename " + from + " to " + to);
125        }
126    }
127
128    public static void ensureFileDoesNotExist(File file) throws IOException {
129        if (file.exists()) {
130            if (!file.isFile()) {
131                throw new IOException(file + " is not a file");
132            }
133            doDelete(file);
134        }
135    }
136
137    public static void doDelete(File file) throws IOException {
138        if (!file.delete()) {
139            throw new IOException("Unable to delete: " + file);
140        }
141    }
142
143    public static boolean isSymlink(File file) throws IOException {
144        String baseName = file.getName();
145        String canonicalPathExceptBaseName =
146                new File(file.getParentFile().getCanonicalFile(), baseName).getPath();
147        return !file.getCanonicalPath().equals(canonicalPathExceptBaseName);
148    }
149
150    public static void deleteRecursive(File toDelete) throws IOException {
151        if (toDelete.isDirectory()) {
152            for (File file : toDelete.listFiles()) {
153                if (file.isDirectory() && !FileUtils.isSymlink(file)) {
154                    // The isSymlink() check is important so that we don't delete files in other
155                    // directories: only the symlink itself.
156                    deleteRecursive(file);
157                } else {
158                    // Delete symlinks to directories or files.
159                    FileUtils.doDelete(file);
160                }
161            }
162            String[] remainingFiles = toDelete.list();
163            if (remainingFiles.length != 0) {
164                throw new IOException("Unable to delete files: " + Arrays
165                        .toString(remainingFiles));
166            }
167        }
168        FileUtils.doDelete(toDelete);
169    }
170
171    public static boolean filesExist(File rootDir, String... fileNames) throws IOException {
172        for (String fileName : fileNames) {
173            File file = new File(rootDir, fileName);
174            if (!file.exists()) {
175                return false;
176            }
177        }
178        return true;
179    }
180
181    /**
182     * Read all lines from a UTF-8 encoded file, returning them as a list of strings.
183     */
184    public static List<String> readLines(File file) throws IOException {
185        FileInputStream in = new FileInputStream(file);
186        try (BufferedReader fileReader = new BufferedReader(
187                new InputStreamReader(in, StandardCharsets.UTF_8));
188        ) {
189            List<String> lines = new ArrayList<>();
190            String line;
191            while ((line = fileReader.readLine()) != null) {
192                lines.add(line);
193            }
194            return lines;
195        }
196    }
197}
198