// Copyright 2014 The Bazel Authors. All rights reserved.
//
// 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 com.google.devtools.build.lib.vfs;

import static java.nio.charset.StandardCharsets.ISO_8859_1;

import com.google.common.base.Predicate;
import com.google.common.io.ByteSink;
import com.google.common.io.ByteSource;
import com.google.common.io.ByteStreams;
import com.google.devtools.build.lib.concurrent.ThreadSafety.ConditionallyThreadSafe;
import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
import com.google.devtools.build.lib.util.Preconditions;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Set;

/**
 * Helper functions that implement often-used complex operations on file
 * systems.
 */
@ConditionallyThreadSafe // ThreadSafe except for deleteTree.
public class FileSystemUtils {

  private FileSystemUtils() {}

  /****************************************************************************
   * Path and PathFragment functions.
   */

  /**
   * Throws exceptions if {@code baseName} is not a valid base name. A valid
   * base name:
   * <ul>
   * <li>Is not null
   * <li>Is not an empty string
   * <li>Is not "." or ".."
   * <li>Does not contain a slash
   * </ul>
   */
  @ThreadSafe
  public static void checkBaseName(String baseName) {
    if (baseName.length() == 0) {
      throw new IllegalArgumentException("Child must not be empty string ('')");
    }
    if (baseName.equals(".") || baseName.equals("..")) {
      throw new IllegalArgumentException("baseName must not be '" + baseName + "'");
    }
    if (baseName.indexOf('/') != -1) {
      throw new IllegalArgumentException("baseName must not contain a slash: '" + baseName + "'");
    }
  }

  /**
   * Returns the common ancestor between two paths, or null if none (including
   * if they are on different filesystems).
   */
  public static Path commonAncestor(Path a, Path b) {
    while (a != null && !b.startsWith(a)) {
      a = a.getParentDirectory();  // returns null at root
    }
    return a;
  }

  /**
   * Returns the longest common ancestor of the two path fragments, or either "/" or "" (depending
   * on whether {@code a} is absolute or relative) if there is none.
   */
  public static PathFragment commonAncestor(PathFragment a, PathFragment b) {
    while (a != null && !b.startsWith(a)) {
      a = a.getParentDirectory();
    }

    return a;
  }
  /**
   * Returns a path fragment from a given from-dir to a given to-path. May be
   * either a short relative path "foo/bar", an up'n'over relative path
   * "../../foo/bar" or an absolute path.
   */
  public static PathFragment relativePath(Path fromDir, Path to) {
    if (to.getFileSystem() != fromDir.getFileSystem()) {
      throw new IllegalArgumentException("fromDir and to must be on the same FileSystem");
    }

    return relativePath(fromDir.asFragment(), to.asFragment());
  }

  /**
   * Returns a path fragment from a given from-dir to a given to-path.
   */
  public static PathFragment relativePath(PathFragment fromDir, PathFragment to) {
    if (to.equals(fromDir)) {
      return PathFragment.create(".");  // same dir, just return '.'
    }
    if (to.startsWith(fromDir)) {
      return to.relativeTo(fromDir);  // easy case--it's a descendant
    }
    PathFragment ancestor = commonAncestor(fromDir, to);
    if (ancestor == null) {
      return to;  // no common ancestor, use 'to'
    }
    int levels = fromDir.relativeTo(ancestor).segmentCount();
    StringBuilder dotdots = new StringBuilder();
    for (int i = 0; i < levels; i++) {
      dotdots.append("../");
    }
    return PathFragment.create(dotdots.toString()).getRelative(to.relativeTo(ancestor));
  }

  /**
   * Removes the shortest suffix beginning with '.' from the basename of the
   * filename string. If the basename contains no '.', the filename is returned
   * unchanged.
   *
   * <p>e.g. "foo/bar.x" -> "foo/bar"
   *
   * <p>Note that if the filename is composed entirely of ".", this method will return the string
   * with one fewer ".", which may have surprising effects.
   */
  @ThreadSafe
  public static String removeExtension(String filename) {
    int lastDotIndex = filename.lastIndexOf('.');
    if (lastDotIndex == -1) { return filename; }
    int lastSlashIndex = filename.lastIndexOf('/');
    if (lastSlashIndex > lastDotIndex) {
      return filename;
    }
    return filename.substring(0, lastDotIndex);
  }

  /**
   * Removes the shortest suffix beginning with '.' from the basename of the
   * PathFragment. If the basename contains no '.', the filename is returned
   * unchanged.
   *
   * <p>e.g. "foo/bar.x" -> "foo/bar"
   *
   * <p>Note that if the base filename is composed entirely of ".", this method will return the
   * filename with one fewer "." in the base filename, which may have surprising effects.
   */
  @ThreadSafe
  public static PathFragment removeExtension(PathFragment path) {
    return path.replaceName(removeExtension(path.getBaseName()));
  }

  /**
   * Removes the shortest suffix beginning with '.' from the basename of the
   * Path. If the basename contains no '.', the filename is returned
   * unchanged.
   *
   * <p>e.g. "foo/bar.x" -> "foo/bar"
   *
   * <p>Note that if the base filename is composed entirely of ".", this method will return the
   * filename with one fewer "." in the base filename, which may have surprising effects.
   */
  @ThreadSafe
  public static Path removeExtension(Path path) {
    return path.getFileSystem().getPath(removeExtension(path.asFragment()));
  }

  /**
   * Returns a new {@code PathFragment} formed by replacing the extension of the
   * last path segment of {@code path} with {@code newExtension}. Null is
   * returned iff {@code path} has zero segments.
   */
  public static PathFragment replaceExtension(PathFragment path, String newExtension) {
    return path.replaceName(removeExtension(path.getBaseName()) + newExtension);
  }

  /**
   * Returns a new {@code PathFragment} formed by replacing the extension of the
   * last path segment of {@code path} with {@code newExtension}. Null is
   * returned iff {@code path} has zero segments or it doesn't end with {@code oldExtension}.
   */
  public static PathFragment replaceExtension(PathFragment path, String newExtension,
      String oldExtension) {
    String base = path.getBaseName();
    if (!base.endsWith(oldExtension)) {
      return null;
    }
    String newBase = base.substring(0, base.length() - oldExtension.length()) + newExtension;
    return path.replaceName(newBase);
  }

  /**
   * Returns a new {@code Path} formed by replacing the extension of the
   * last path segment of {@code path} with {@code newExtension}. Null is
   * returned iff {@code path} has zero segments.
   */
  public static Path replaceExtension(Path path, String newExtension) {
    PathFragment fragment = replaceExtension(path.asFragment(), newExtension);
    return fragment == null ? null : path.getFileSystem().getPath(fragment);
  }

  /**
   * Returns a new {@code PathFragment} formed by adding the extension to the last path segment of
   * {@code path}. Null is returned if {@code path} has zero segments.
   */
  public static PathFragment appendExtension(PathFragment path, String newExtension) {
    return path.replaceName(path.getBaseName() + newExtension);
  }

  /**
   * Returns a new {@code PathFragment} formed by replacing the first, or all if
   * {@code replaceAll} is true, {@code oldSegment} of {@code path} with {@code
   * newSegment}.
   */
  public static PathFragment replaceSegments(PathFragment path,
      String oldSegment, String newSegment, boolean replaceAll) {
    int count = path.segmentCount();
    for (int i = 0; i < count; i++) {
      if (path.getSegment(i).equals(oldSegment)) {
        path = PathFragment.create(
            path.subFragment(0, i),
            PathFragment.create(newSegment),
            path.subFragment(i+1, count));
        if (!replaceAll) {
          return path;
        }
      }
    }
    return path;
  }

  /**
   * Returns a new {@code PathFragment} formed by appending the given string to the last path
   * segment of {@code path} without removing the extension.  Returns null if {@code path}
   * has zero segments.
   */
  public static PathFragment appendWithoutExtension(PathFragment path, String toAppend) {
    return path.replaceName(appendWithoutExtension(path.getBaseName(), toAppend));
  }

  /**
   * Given a string that represents a file with an extension separated by a '.' and a string
   * to append, return a string in which {@code toAppend} has been appended to {@code name}
   * before the last '.' character.  If {@code name} does not include a '.', appends {@code
   * toAppend} at the end.
   *
   * <p>For example,
   * ("libfoo.jar", "-src") ==> "libfoo-src.jar"
   * ("libfoo", "-src") ==> "libfoo-src"
   */
  private static String appendWithoutExtension(String name, String toAppend) {
    int dotIndex = name.lastIndexOf('.');
    if (dotIndex > 0) {
      String baseName = name.substring(0, dotIndex);
      String extension = name.substring(dotIndex);
      return baseName + toAppend + extension;
    } else {
      return name + toAppend;
    }
  }

  /****************************************************************************
   * FileSystem property functions.
   */

  /**
   * Return the current working directory as expressed by the System property
   * 'user.dir'.
   */
  public static Path getWorkingDirectory(FileSystem fs) {
    return fs.getPath(getWorkingDirectory());
  }

  /**
   * Returns the current working directory as expressed by the System property
   * 'user.dir'. This version does not require a {@link FileSystem}.
   */
  public static PathFragment getWorkingDirectory() {
    return PathFragment.create(System.getProperty("user.dir", "/"));
  }

  /****************************************************************************
   * Path FileSystem mutating operations.
   */

  /**
   * "Touches" the file or directory specified by the path, following symbolic
   * links. If it does not exist, it is created as an empty file; otherwise, the
   * time of last access is updated to the current time.
   *
   * @throws IOException if there was an error while touching the file
   */
  @ThreadSafe
  public static void touchFile(Path path) throws IOException {
    if (path.exists()) {
      // -1L means "use the current time", and is ultimately implemented by
      // utime(path, null), thereby using the kernel's clock, not the JVM's.
      // (A previous implementation based on the JVM clock was found to be
      // skewy.)
      path.setLastModifiedTime(-1L);
    } else {
      createEmptyFile(path);
    }
  }

  /**
   * Creates an empty regular file with the name of the current path, following
   * symbolic links.
   *
   * @throws IOException if the file could not be created for any reason
   *         (including that there was already a file at that location)
   */
  public static void createEmptyFile(Path path) throws IOException {
    path.getOutputStream().close();
  }

  /**
   * Creates or updates a symbolic link from 'link' to 'target'. Replaces
   * existing symbolic links with target, and skips the link creation if it is
   * already present. Will also create any missing ancestor directories of the
   * link. This method is non-atomic
   *
   * <p>Note: this method will throw an IOException if there is an unequal
   * non-symlink at link.
   *
   * @throws IOException if the creation of the symbolic link was unsuccessful
   *         for any reason.
   */
  @ThreadSafe  // but not atomic
  public static void ensureSymbolicLink(Path link, Path target) throws IOException {
    ensureSymbolicLink(link, target.asFragment());
  }

  /**
   * Creates or updates a symbolic link from 'link' to 'target'. Replaces
   * existing symbolic links with target, and skips the link creation if it is
   * already present. Will also create any missing ancestor directories of the
   * link. This method is non-atomic
   *
   * <p>Note: this method will throw an IOException if there is an unequal
   * non-symlink at link.
   *
   * @throws IOException if the creation of the symbolic link was unsuccessful
   *         for any reason.
   */
  @ThreadSafe  // but not atomic
  public static void ensureSymbolicLink(Path link, String target) throws IOException {
    ensureSymbolicLink(link, PathFragment.create(target));
  }

  /**
   * Creates or updates a symbolic link from 'link' to 'target'. Replaces
   * existing symbolic links with target, and skips the link creation if it is
   * already present. Will also create any missing ancestor directories of the
   * link. This method is non-atomic
   *
   * <p>Note: this method will throw an IOException if there is an unequal
   * non-symlink at link.
   *
   * @throws IOException if the creation of the symbolic link was unsuccessful
   *         for any reason.
   */
  @ThreadSafe  // but not atomic
  public static void ensureSymbolicLink(Path link, PathFragment target) throws IOException {
    // TODO(bazel-team): (2009) consider adding the logic for recovering from the case when
    // we have already created a parent directory symlink earlier.
    try {
      if (link.readSymbolicLink().equals(target)) {
        return;  // Do nothing if the link is already there.
      }
    } catch (IOException e) { // link missing or broken
      /* fallthru and do the work below */
    }
    if (link.isSymbolicLink()) {
      link.delete();  // Remove the symlink since it is pointing somewhere else.
    } else {
      createDirectoryAndParents(link.getParentDirectory());
    }
    try {
      link.createSymbolicLink(target);
    } catch (IOException e) {
      // Only pass on exceptions caused by a true link creation failure.
      if (!link.isSymbolicLink() ||
          !link.resolveSymbolicLinks().equals(link.getRelative(target))) {
        throw e;
      }
    }
  }

  public static ByteSource asByteSource(final Path path) {
    return new ByteSource() {
      @Override public InputStream openStream() throws IOException {
        return path.getInputStream();
      }
    };
  }

  public static ByteSink asByteSink(final Path path, final boolean append) {
    return new ByteSink() {
      @Override public OutputStream openStream() throws IOException {
        return path.getOutputStream(append);
      }
    };
  }

  public static ByteSink asByteSink(final Path path) {
    return asByteSink(path, false);
  }

  /**
   * Copies the file from location "from" to location "to", while overwriting a
   * potentially existing "to". File's last modified time, executable and
   * writable bits are also preserved.
   *
   * <p>If no error occurs, the method returns normally. If a parent directory does
   * not exist, a FileNotFoundException is thrown. An IOException is thrown when
   * other erroneous situations occur. (e.g. read errors)
   */
  @ThreadSafe  // but not atomic
  public static void copyFile(Path from, Path to) throws IOException {
    try {
      to.delete();
    } catch (IOException e) {
      throw new IOException("error copying file: "
          + "couldn't delete destination: " + e.getMessage());
    }
    asByteSource(from).copyTo(asByteSink(to));
    to.setLastModifiedTime(from.getLastModifiedTime()); // Preserve mtime.
    if (!from.isWritable()) {
      to.setWritable(false); // Make file read-only if original was read-only.
    }
    to.setExecutable(from.isExecutable()); // Copy executable bit.
  }

  /**
   * Moves the file from location "from" to location "to", while overwriting a
   * potentially existing "to". File's last modified time, executable and
   * writable bits are also preserved.
   *
   * <p>If no error occurs, the method returns normally. If a parent directory does
   * not exist, a FileNotFoundException is thrown. An IOException is thrown when
   * other erroneous situations occur. (e.g. read errors)
   */
  @ThreadSafe  // but not atomic
  public static void moveFile(Path from, Path to) throws IOException {
    long mtime = from.getLastModifiedTime();
    boolean writable = from.isWritable();
    boolean executable = from.isExecutable();

    // We don't try-catch here for better performance.
    to.delete();
    try {
      from.renameTo(to);
    } catch (IOException e) {
      asByteSource(from).copyTo(asByteSink(to));
      if (!from.delete()) {
        if (!to.delete()) {
          throw new IOException("Unable to delete " + to);
        }
        throw new IOException("Unable to delete " + from);
      }
    }
    to.setLastModifiedTime(mtime); // Preserve mtime.
    if (!writable) {
      to.setWritable(false); // Make file read-only if original was read-only.
    }
    to.setExecutable(executable); // Copy executable bit.
  }

  /**
   * Copies a tool binary from one path to another, returning the target path.
   * The directory of the target path must already exist. The target copy's time
   * is set to match, as well as its read-only and executable flags. The
   * operation is skipped if the target file has the same time and size as the
   * source.
   */
  public static Path copyTool(Path source, Path target) throws IOException {
    FileStatus sourceStat = null;
    FileStatus targetStat = target.statNullable();
    if (targetStat != null) {
      // stat the source file only if we'll need the stat.
      sourceStat = source.stat(Symlinks.FOLLOW);
    }
    if (targetStat == null ||
        targetStat.getLastModifiedTime() != sourceStat.getLastModifiedTime() ||
        targetStat.getSize() != sourceStat.getSize()) {
      copyFile(source, target);
      target.setWritable(source.isWritable());
      target.setExecutable(source.isExecutable());
      target.setLastModifiedTime(source.getLastModifiedTime());
    }
    return target;
  }

  /****************************************************************************
   * Directory tree operations.
   */

  /**
   * Returns a new collection containing all of the paths below a given root
   * path, for which the given predicate is true. Symbolic links are not
   * followed, and may appear in the result.
   *
   * @throws IOException If the root does not denote a directory
   */
  @ThreadSafe
  public static Collection<Path> traverseTree(Path root, Predicate<? super Path> predicate)
      throws IOException {
    List<Path> paths = new ArrayList<>();
    traverseTree(paths, root, predicate);
    return paths;
  }

  /**
   * Populates an existing Path List, adding all of the paths below a given root
   * path for which the given predicate is true. Symbolic links are not
   * followed, and may appear in the result.
   *
   * @throws IOException If the root does not denote a directory
   */
  @ThreadSafe
  public static void traverseTree(Collection<Path> paths, Path root,
      Predicate<? super Path> predicate) throws IOException {
    for (Path p : root.getDirectoryEntries()) {
      if (predicate.apply(p)) {
        paths.add(p);
      }
      if (p.isDirectory(Symlinks.NOFOLLOW)) {
        traverseTree(paths, p, predicate);
      }
    }
  }

  /**
   * Deletes 'p', and everything recursively beneath it if it's a directory.
   * Does not follow any symbolic links.
   *
   * @throws IOException if any file could not be removed.
   */
  @ThreadSafe
  public static void deleteTree(Path p) throws IOException {
    deleteTreesBelow(p);
    p.delete();
  }

  /**
   * Deletes all dir trees recursively beneath 'dir' if it's a directory,
   * nothing otherwise. Does not follow any symbolic links.
   *
   * @throws IOException if any file could not be removed.
   */
  @ThreadSafe
  public static void deleteTreesBelow(Path dir) throws IOException {
    if (dir.isDirectory(Symlinks.NOFOLLOW)) {  // real directories (not symlinks)
      dir.setReadable(true);
      dir.setWritable(true);
      dir.setExecutable(true);
      for (Path child : dir.getDirectoryEntries()) {
        deleteTree(child);
      }
    }
  }

  /**
   * Copies all dir trees under a given 'from' dir to location 'to', while overwriting
   * all files in the potentially existing 'to'. Resolves symbolic links.
   *
   * <p>The source and the destination must be non-overlapping, otherwise an
   * IllegalArgumentException will be thrown. This method cannot be used to copy
   * a dir tree to a sub tree of itself.
   *
   * <p>If no error occurs, the method returns normally. If the given 'from' does
   * not exist, a FileNotFoundException is thrown. An IOException is thrown when
   * other erroneous situations occur. (e.g. read errors)
   */
  @ThreadSafe
  public static void copyTreesBelow(Path from , Path to) throws IOException {
    if (to.startsWith(from)) {
      throw new IllegalArgumentException(to + " is a subdirectory of " + from);
    }

    Collection<Path> entries = from.getDirectoryEntries();
    for (Path entry : entries) {
      if (entry.isFile()) {
        Path newEntry = to.getChild(entry.getBaseName());
        copyFile(entry, newEntry);
      } else {
        Path subDir = to.getChild(entry.getBaseName());
        subDir.createDirectory();
        copyTreesBelow(entry, subDir);
      }
    }
  }

  /**
   * Moves all dir trees under a given 'from' dir to location 'to', while overwriting
   * all files in the potentially existing 'to'. Doesn't resolve symbolic links.
   *
   * <p>The source and the destination must be non-overlapping, otherwise an
   * IllegalArgumentException will be thrown. This method cannot be used to copy
   * a dir tree to a sub tree of itself.
   *
   * <p>If no error occurs, the method returns normally. If the given 'from' does
   * not exist, a FileNotFoundException is thrown. An IOException is thrown when
   * other erroneous situations occur. (e.g. read errors)
   */
  @ThreadSafe
  public static void moveTreesBelow(Path from , Path to) throws IOException {
    if (to.startsWith(from)) {
      throw new IllegalArgumentException(to + " is a subdirectory of " + from);
    }

    Collection<Path> entries = from.getDirectoryEntries();
    for (Path entry : entries) {
      if (entry.isDirectory(Symlinks.NOFOLLOW)) {
        Path subDir = to.getChild(entry.getBaseName());
        subDir.createDirectory();
        moveTreesBelow(entry, subDir);
      } else {
        Path newEntry = to.getChild(entry.getBaseName());
        moveFile(entry, newEntry);
      }
    }
  }

  /**
   * Attempts to create a directory with the name of the given path, creating
   * ancestors as necessary.
   *
   * <p>Postcondition: completes normally iff {@code dir} denotes an existing
   * directory (not necessarily canonical); completes abruptly otherwise.
   *
   * @return true if the directory was successfully created anew, false if it
   *   already existed (including the case where {@code dir} denotes a symlink
   *   to an existing directory)
   * @throws IOException if the directory could not be created
   */
  @ThreadSafe
  public static boolean createDirectoryAndParents(Path dir) throws IOException {
    return createDirectoryAndParentsWithCache(null, dir);
  }

  /**
   * Attempts to create a directory with the name of the given path, creating ancestors as
   * necessary. Only creates directories or their parents if they are not contained in the set
   * {@code createdDirs} and instead assumes that they already exist. This saves a round-trip to the
   * kernel, but is only safe when no one deletes directories that have been created by this method.
   *
   * <p>Postcondition: completes normally iff {@code dir} denotes an existing directory (not
   * necessarily canonical); completes abruptly otherwise.
   *
   * @return true if the directory was successfully created anew, false if it already existed
   *     (including the case where {@code dir} denotes a symlink to an existing directory)
   * @throws IOException if the directory could not be created
   */
  @ThreadSafe
  public static boolean createDirectoryAndParentsWithCache(Set<Path> createdDirs, Path dir)
      throws IOException {
    // Optimised for minimal number of I/O calls.

    // Don't attempt to create the root directory.
    if (dir.getParentDirectory() == null) {
      return false;
    }

    // We already created that directory.
    if (createdDirs != null && createdDirs.contains(dir)) {
      return false;
    }

    FileSystem filesystem = dir.getFileSystem();
    if (filesystem instanceof UnionFileSystem) {
      // If using UnionFS, make sure that we do not traverse filesystem boundaries when creating
      // parent directories by rehoming the path on the most specific filesystem.
      FileSystem delegate = ((UnionFileSystem) filesystem).getDelegate(dir);
      dir = delegate.getPath(dir.asFragment());
    }

    try {
      boolean result = dir.createDirectory();
      if (createdDirs != null) {
        createdDirs.add(dir);
      }
      return result;
    } catch (IOException e) {
      if (e.getMessage().endsWith(" (No such file or directory)")) { // ENOENT
        createDirectoryAndParentsWithCache(createdDirs, dir.getParentDirectory());
        boolean result = dir.createDirectory();
        if (createdDirs != null) {
          createdDirs.add(dir);
        }
        return result;
      } else if (e.getMessage().endsWith(" (File exists)") && dir.isDirectory()) { // EEXIST
        if (createdDirs != null) {
          createdDirs.add(dir);
        }
        return false;
      } else {
        throw e; // some other error (e.g. ENOTDIR, EACCES, etc.)
      }
    }
  }

  /**
   * Attempts to remove a relative chain of directories under a given base.
   * Returns {@code true} if the removal was successful, and returns {@code
   * false} if the removal fails because a directory was not empty. An
   * {@link IOException} is thrown for any other errors.
   */
  @ThreadSafe
  public static boolean removeDirectoryAndParents(Path base, PathFragment toRemove) {
    if (toRemove.isAbsolute()) {
      return false;
    }
    try {
      for (; toRemove.segmentCount() > 0; toRemove = toRemove.getParentDirectory()) {
        Path p = base.getRelative(toRemove);
        if (p.exists()) {
          p.delete();
        }
      }
    } catch (IOException e) {
      return false;
    }
    return true;
  }

  /****************************************************************************
   * Whole-file I/O utilities for characters and bytes. These convenience
   * methods are not efficient and should not be used for large amounts of data!
   */

  /**
   * Decodes the given byte array assumed to be encoded with ISO-8859-1 encoding (isolatin1).
   */
  public static char[] convertFromLatin1(byte[] content) {
    char[] latin1 = new char[content.length];
    for (int i = 0; i < latin1.length; i++) { // yeah, latin1 is this easy! :-)
      latin1[i] = (char) (0xff & content[i]);
    }
    return latin1;
  }

  /**
   * Writes lines to file using ISO-8859-1 encoding (isolatin1).
   */
  @ThreadSafe // but not atomic
  public static void writeIsoLatin1(Path file, String... lines) throws IOException {
    writeLinesAs(file, ISO_8859_1, lines);
  }

  /**
   * Append lines to file using ISO-8859-1 encoding (isolatin1).
   */
  @ThreadSafe // but not atomic
  public static void appendIsoLatin1(Path file, String... lines) throws IOException {
    appendLinesAs(file, ISO_8859_1, lines);
  }

  /**
   * Writes the specified String as ISO-8859-1 (latin1) encoded bytes to the
   * file. Follows symbolic links.
   *
   * @throws IOException if there was an error
   */
  public static void writeContentAsLatin1(Path outputFile, String content) throws IOException {
    writeContent(outputFile, ISO_8859_1, content);
  }

  /**
   * Writes the specified String using the specified encoding to the file.
   * Follows symbolic links.
   *
   * @throws IOException if there was an error
   */
  public static void writeContent(Path outputFile, Charset charset, String content)
      throws IOException {
    asByteSink(outputFile).asCharSink(charset).write(content);
  }

  /**
   * Writes lines to file using the given encoding, ending every line with a
   * line break '\n' character.
   */
  @ThreadSafe // but not atomic
  public static void writeLinesAs(Path file, Charset charset, String... lines)
      throws IOException {
    writeLinesAs(file, charset, Arrays.asList(lines));
  }

  /**
   * Appends lines to file using the given encoding, ending every line with a
   * line break '\n' character.
   */
  @ThreadSafe // but not atomic
  public static void appendLinesAs(Path file, Charset charset, String... lines)
      throws IOException {
    appendLinesAs(file, charset, Arrays.asList(lines));
  }

  /**
   * Writes lines to file using the given encoding, ending every line with a
   * line break '\n' character.
   */
  @ThreadSafe // but not atomic
  public static void writeLinesAs(Path file, Charset charset, Iterable<String> lines)
      throws IOException {
    createDirectoryAndParents(file.getParentDirectory());
    asByteSink(file).asCharSink(charset).writeLines(lines);
  }

  /**
   * Appends lines to file using the given encoding, ending every line with a
   * line break '\n' character.
   */
  @ThreadSafe // but not atomic
  public static void appendLinesAs(Path file, Charset charset, Iterable<String> lines)
      throws IOException {
    createDirectoryAndParents(file.getParentDirectory());
    asByteSink(file, true).asCharSink(charset).writeLines(lines);
  }

  /**
   * Writes the specified byte array to the output file. Follows symbolic links.
   *
   * @throws IOException if there was an error
   */
  public static void writeContent(Path outputFile, byte[] content) throws IOException {
    asByteSink(outputFile).write(content);
  }

  /**
   * Updates the contents of the output file if they do not match the given array, thus maintaining
   * the mtime and ctime in case of no updates. Follows symbolic links.
   *
   * <p>If the output file already exists but is unreadable, this tries to overwrite it with the new
   * contents. In other words: unreadable or missing files are considered to be non-matching.
   *
   * @throws IOException if there was an error
   */
  public static void maybeUpdateContent(Path outputFile, byte[] newContent) throws IOException {
    byte[] currentContent;
    try {
      currentContent = readContent(outputFile);
    } catch (IOException e) {
      // Ignore error per the rationale given in the docstring. Keep in mind that what we are doing
      // here is for performance reasons only so we should only break if the real action (that is,
      // the write) fails -- not any of the optimization steps.
      currentContent = null;
    }

    if (currentContent == null) {
      writeContent(outputFile, newContent);
    } else {
      if (!Arrays.equals(newContent, currentContent)) {
        if (!outputFile.isWritable()) {
          outputFile.delete();
        }
        writeContent(outputFile, newContent);
      }
    }
  }

  /**
   * Returns the entirety of the specified input stream and returns it as a char
   * array, decoding characters using ISO-8859-1 (Latin1).
   *
   * @throws IOException if there was an error
   */
  public static char[] readContentAsLatin1(InputStream in) throws IOException {
    return convertFromLatin1(ByteStreams.toByteArray(in));
  }

  /**
   * Returns the entirety of the specified file and returns it as a char array,
   * decoding characters using ISO-8859-1 (Latin1).
   *
   * @throws IOException if there was an error
   */
  public static char[] readContentAsLatin1(Path inputFile) throws IOException {
    return convertFromLatin1(readContent(inputFile));
  }

  /**
   * Returns an iterable that allows iterating over ISO-8859-1 (Latin1) text
   * file contents line by line. If the file ends in a line break, the iterator
   * will return an empty string as the last element.
   *
   * @throws IOException if there was an error
   */
  public static Iterable<String> iterateLinesAsLatin1(Path inputFile) throws IOException {
    return readLines(inputFile, ISO_8859_1);
  }

  /**
   * Returns an iterable that allows iterating over text file contents line by line in the given
   * {@link Charset}. If the file ends in a line break, the iterator will return an empty string
   * as the last element.
   *
   * @throws IOException if there was an error
   */
  public static Iterable<String> readLines(Path inputFile, Charset charset) throws IOException {
    return asByteSource(inputFile).asCharSource(charset).readLines();
  }

  /**
   * Returns the entirety of the specified file and returns it as a byte array.
   *
   * @throws IOException if there was an error
   */
  public static byte[] readContent(Path inputFile) throws IOException {
    return asByteSource(inputFile).read();
  }

  /**
   * Reads the entire file using the given charset and returns the contents as a string
   */
  public static String readContent(Path inputFile, Charset charset) throws IOException {
    return asByteSource(inputFile).asCharSource(charset).read();
  }

  /**
   * Reads at most {@code limit} bytes from {@code inputFile} and returns it as a byte array.
   *
   * @throws IOException if there was an error.
   */
  public static byte[] readContentWithLimit(Path inputFile, int limit) throws IOException {
    Preconditions.checkArgument(limit >= 0, "limit needs to be >=0, but it is %s", limit);
    ByteSource byteSource = asByteSource(inputFile);
    byte[] buffer = new byte[limit];
    try (InputStream inputStream = byteSource.openBufferedStream()) {
      int read = ByteStreams.read(inputStream, buffer, 0, limit);
      return read == limit ? buffer : Arrays.copyOf(buffer, read);
    }
  }

  /**
   * Reads the given file {@code path}, assumed to have size {@code fileSize}, and does a sanity
   * check on the number of bytes read.
   *
   * <p>Use this method when you already know the size of the file. The sanity check is intended to
   * catch issues where filesystems incorrectly truncate files.
   *
   * @throws IOException if there was an error, or if fewer than {@code fileSize} bytes were read.
   */
  public static byte[] readWithKnownFileSize(Path path, long fileSize) throws IOException {
    if (fileSize > Integer.MAX_VALUE) {
      throw new IOException("Cannot read file with size larger than 2GB");
    }
    int fileSizeInt = (int) fileSize;
    byte[] bytes = readContentWithLimit(path, fileSizeInt);
    if (fileSizeInt > bytes.length) {
      throw new IOException("Unexpected short read from file '" + path
          + "' (expected " + fileSizeInt + ", got " + bytes.length + " bytes)");
    }
    return bytes;
  }

  /**
   * Dumps diagnostic information about the specified filesystem to {@code out}.
   * This is the implementation of the filesystem part of the 'blaze dump'
   * command. It lives here, rather than in DumpCommand, because it requires
   * privileged access to members of this package.
   *
   * <p>Its results are unspecified and MUST NOT be interpreted programmatically.
   */
  public static void dump(FileSystem fs, final PrintStream out) {
    // Unfortunately there's no "letrec" for anonymous functions so we have to
    // (a) name the function, (b) put it in a box and (c) use List not array
    // because of the generic type.  *sigh*.
    final List<Predicate<Path>> dumpFunction = new ArrayList<>();
    dumpFunction.add(
        child -> {
          Path path = child;
          out.println("  " + path + " (" + path.toDebugString() + ")");
          path.applyToChildren(dumpFunction.get(0));
          return false;
        });

    fs.getRootDirectory().applyToChildren(dumpFunction.get(0));
  }

  /**
   * Returns the type of the file system path belongs to.
   */
  public static String getFileSystem(Path path) {
    return path.getFileSystem().getFileSystemType(path);
  }

  /**
   * Returns whether the given path starts with any of the paths in the given
   * list of prefixes.
   */
  public static boolean startsWithAny(Path path, Iterable<Path> prefixes) {
    for (Path prefix : prefixes) {
      if (path.startsWith(prefix)) {
        return true;
      }
    }
    return false;
  }

  /**
   * Returns whether the given path starts with any of the paths in the given
   * list of prefixes.
   */
  public static boolean startsWithAny(PathFragment path, Iterable<PathFragment> prefixes) {
    for (PathFragment prefix : prefixes) {
      if (path.startsWith(prefix)) {
        return true;
      }
    }
    return false;
  }


  /**
   * Create a new hard link file at "linkPath" for file at "originalPath". If "originalPath" is a
   * directory, then for each entry, create link under "linkPath" recursively.
   *
   * @param linkPath The path of the new link file to be created
   * @param originalPath The path of the original file
   * @throws IOException if there was an error executing {@link Path#createHardLink}
   */
  public static void createHardLink(Path linkPath, Path originalPath) throws IOException {

    // Directory
    if (originalPath.isDirectory()) {
      for (Path originalSubpath : originalPath.getDirectoryEntries()) {
        Path linkSubpath = linkPath.getRelative(originalSubpath.relativeTo(originalPath));
        createHardLink(linkSubpath, originalSubpath);
      }
      // Other types of file
    } else {
      Path parentDir = linkPath.getParentDirectory();
      if (!parentDir.exists()) {
        FileSystemUtils.createDirectoryAndParents(parentDir);
      }
      originalPath.createHardLink(linkPath);
    }
  }
}
