/* * Copyright (C) 2015 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 libcore.tzdata.update; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedList; import java.util.List; import java.util.zip.CRC32; /** * Utility methods for files operations. */ public final class FileUtils { private FileUtils() { } /** * Creates a new {@link java.io.File} from the {@code parentDir} and {@code name}, but only if * the resulting file would exist beneath {@code parentDir}. Useful if {@code name} could * contain "/../" or symlinks. The returned object has a canonicalized path. * * @throws java.io.IOException if the file would not exist beneath {@code parentDir} */ public static File createSubFile(File parentDir, String name) throws IOException { // The subFile must exist beneath parentDir. If name contains "/../" this may not be the // case so we check. File subFile = new File(parentDir, name).getCanonicalFile(); if (!subFile.getPath().startsWith(parentDir.getCanonicalPath())) { throw new IOException(name + " must exist beneath " + parentDir + ". Canonicalized subpath: " + subFile); } return subFile; } /** * Makes sure a directory exists. If it doesn't exist, it is created. Parent directories are * also created as needed. If {@code makeWorldReadable} is {@code true} the directory's default * permissions will be set. Even when {@code makeWorldReadable} is {@code true}, only * directories explicitly created will have their permissions set; existing directories are * untouched. * * @throws IOException if the directory or one of its parents did not already exist and could * not be created */ public static void ensureDirectoriesExist(File dir, boolean makeWorldReadable) throws IOException { LinkedList dirs = new LinkedList<>(); File currentDir = dir; do { dirs.addFirst(currentDir); currentDir = currentDir.getParentFile(); } while (currentDir != null); for (File dirToCheck : dirs) { if (!dirToCheck.exists()) { if (!dirToCheck.mkdir()) { throw new IOException("Unable to create directory: " + dir); } if (makeWorldReadable) { makeDirectoryWorldAccessible(dirToCheck); } } else if (!dirToCheck.isDirectory()) { throw new IOException(dirToCheck + " exists but is not a directory"); } } } public static void makeDirectoryWorldAccessible(File directory) throws IOException { if (!directory.isDirectory()) { throw new IOException(directory + " must be a directory"); } makeWorldReadable(directory); if (!directory.setExecutable(true, false /* ownerOnly */)) { throw new IOException("Unable to make " + directory + " world-executable"); } } public static void makeWorldReadable(File file) throws IOException { if (!file.setReadable(true, false /* ownerOnly */)) { throw new IOException("Unable to make " + file + " world-readable"); } } /** * Calculates the checksum from the contents of a file. */ public static long calculateChecksum(File file) throws IOException { final int BUFFER_SIZE = 8196; CRC32 crc32 = new CRC32(); try (FileInputStream fis = new FileInputStream(file)) { byte[] buffer = new byte[BUFFER_SIZE]; int count; while ((count = fis.read(buffer)) != -1) { crc32.update(buffer, 0, count); } } return crc32.getValue(); } public static void rename(File from, File to) throws IOException { ensureFileDoesNotExist(to); if (!from.renameTo(to)) { throw new IOException("Unable to rename " + from + " to " + to); } } public static void ensureFileDoesNotExist(File file) throws IOException { if (file.exists()) { if (!file.isFile()) { throw new IOException(file + " is not a file"); } doDelete(file); } } public static void doDelete(File file) throws IOException { if (!file.delete()) { throw new IOException("Unable to delete: " + file); } } public static boolean isSymlink(File file) throws IOException { String baseName = file.getName(); String canonicalPathExceptBaseName = new File(file.getParentFile().getCanonicalFile(), baseName).getPath(); return !file.getCanonicalPath().equals(canonicalPathExceptBaseName); } public static void deleteRecursive(File toDelete) throws IOException { if (toDelete.isDirectory()) { for (File file : toDelete.listFiles()) { if (file.isDirectory() && !FileUtils.isSymlink(file)) { // The isSymlink() check is important so that we don't delete files in other // directories: only the symlink itself. deleteRecursive(file); } else { // Delete symlinks to directories or files. FileUtils.doDelete(file); } } String[] remainingFiles = toDelete.list(); if (remainingFiles.length != 0) { throw new IOException("Unable to delete files: " + Arrays .toString(remainingFiles)); } } FileUtils.doDelete(toDelete); } public static boolean filesExist(File rootDir, String... fileNames) throws IOException { for (String fileName : fileNames) { File file = new File(rootDir, fileName); if (!file.exists()) { return false; } } return true; } /** * Read all lines from a UTF-8 encoded file, returning them as a list of strings. */ public static List readLines(File file) throws IOException { FileInputStream in = new FileInputStream(file); try (BufferedReader fileReader = new BufferedReader( new InputStreamReader(in, StandardCharsets.UTF_8)); ) { List lines = new ArrayList<>(); String line; while ((line = fileReader.readLine()) != null) { lines.add(line); } return lines; } } }