blob: c12818bae6ed6c6f05e341ccadfdf3c5292c4be6 [file] [log] [blame]
// Copyright 2019 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 com.google.common.base.Preconditions;
import com.google.common.hash.Hasher;
import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec;
import com.google.devtools.build.lib.util.FileType;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.nio.channels.ReadableByteChannel;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import javax.annotation.Nullable;
/**
* A local file path representing a file on the host machine. You should use this when you want to
* access local files via the file system.
*
* <p>Paths are always absolute.
*
* <p>Strings are normalized with '.' and '..' removed and resolved (if possible), any multiple
* slashes ('/') removed, and any trailing slash also removed. Windows drive letters are uppercased.
* The current implementation does not touch the incoming path string unless the string actually
* needs to be normalized.
*
* <p>There is some limited support for Windows-style paths. Most importantly, drive identifiers in
* front of a path (c:/abc) are supported and such paths are correctly recognized as absolute, as
* are paths with backslash separators (C:\\foo\\bar). However, advanced Windows-style features like
* \\\\network\\paths and \\\\?\\unc\\paths are not supported. We are currently using forward
* slashes ('/') even on Windows, so backslashes '\' get converted to forward slashes during
* normalization.
*
* <p>Mac and Windows file paths are case insensitive. Case is preserved.
*/
@ThreadSafe
@AutoCodec
public class Path implements Comparable<Path>, Serializable, FileType.HasFileType {
private static FileSystem fileSystemForSerialization;
/**
* We need to specify used FileSystem. In this case we can save memory during the serialization.
*/
public static void setFileSystemForSerialization(FileSystem fileSystem) {
fileSystemForSerialization = fileSystem;
}
/**
* Returns FileSystem that we are using.
*/
public static FileSystem getFileSystemForSerialization() {
return fileSystemForSerialization;
}
private static final OsPathPolicy OS = OsPathPolicy.getFilePathOs();
private static final char SEPARATOR = OS.getSeparator();
private String path;
private int driveStrLength; // 1 on Unix, 3 on Windows
private FileSystem fileSystem;
/** Creates a local path that is specific to the host OS. */
static Path create(String path, FileSystem fileSystem) {
Preconditions.checkNotNull(path);
int normalizationLevel = OS.needsToNormalize(path);
String normalizedPath = OS.normalize(path, normalizationLevel);
return createAlreadyNormalized(normalizedPath, fileSystem);
}
@AutoCodec.VisibleForSerialization
@AutoCodec.Instantiator
static Path createAlreadyNormalized(String path, FileSystem fileSystem) {
int driveStrLength = OS.getDriveStrLength(path);
return createAlreadyNormalized(path, driveStrLength, fileSystem);
}
static Path createAlreadyNormalized(String path, int driveStrLength, FileSystem fileSystem) {
return new Path(path, driveStrLength, fileSystem);
}
/** This method expects path to already be normalized. */
private Path(String path, int driveStrLength, FileSystem fileSystem) {
Preconditions.checkArgument(driveStrLength > 0, "Paths must be absolute: '%s'", path);
this.path = Preconditions.checkNotNull(path);
this.driveStrLength = driveStrLength;
this.fileSystem = fileSystem;
}
public String getPathString() {
return path;
}
@Override
public String filePathForFileTypeMatcher() {
return path;
}
/**
* Returns the name of the leaf file or directory.
*
* <p>If called on a {@link Path} instance for a mount name (eg. '/' or 'C:/'), the empty string
* is returned.
*/
public String getBaseName() {
int lastSeparator = path.lastIndexOf(SEPARATOR);
return lastSeparator < driveStrLength
? path.substring(driveStrLength)
: path.substring(lastSeparator + 1);
}
/** Synonymous with {@link Path#getRelative(String)}. */
public Path getChild(String child) {
FileSystemUtils.checkBaseName(child);
return getRelative(child);
}
/**
* Returns a {@link Path} instance representing the relative path between this {@link Path} and
* the given path.
*/
public Path getRelative(PathFragment other) {
Preconditions.checkNotNull(other);
String otherStr = other.getPathString();
// Fast-path: The path fragment is already normal, use cheaper normalization check
return getRelative(otherStr, other.getDriveStrLength(), OS.needsToNormalizeSuffix(otherStr));
}
/**
* Returns a {@link Path} instance representing the relative path between this {@link Path} and
* the given path.
*/
public Path getRelative(String other) {
Preconditions.checkNotNull(other);
return getRelative(other, OS.getDriveStrLength(other), OS.needsToNormalize(other));
}
private Path getRelative(String other, int otherDriveStrLength, int normalizationLevel) {
if (other.isEmpty()) {
return this;
}
// This is an absolute path, simply return it
if (otherDriveStrLength > 0) {
String normalizedPath = OS.normalize(other, normalizationLevel);
return new Path(normalizedPath, otherDriveStrLength, fileSystem);
}
String newPath;
if (path.length() == driveStrLength) {
newPath = path + other;
} else {
newPath = path + '/' + other;
}
// Note that even if other came from a PathFragment instance we still might
// need to normalize the result if (for instance) other is a path that
// starts with '..'
newPath = OS.normalize(newPath, normalizationLevel);
return new Path(newPath, driveStrLength, fileSystem);
}
/**
* Returns the parent directory of this {@link Path}.
*
* <p>If called on a root (like '/'), it returns null.
*/
@Nullable
public Path getParentDirectory() {
int lastSeparator = path.lastIndexOf(SEPARATOR);
if (lastSeparator < driveStrLength) {
if (path.length() > driveStrLength) {
String newPath = path.substring(0, driveStrLength);
return new Path(newPath, driveStrLength, fileSystem);
} else {
return null;
}
}
String newPath = path.substring(0, lastSeparator);
return new Path(newPath, driveStrLength, fileSystem);
}
/**
* Returns the drive.
*
* <p>On unix, this will return "/". On Windows it will return the drive letter, like "C:/".
*/
public String getDriveStr() {
return path.substring(0, driveStrLength);
}
/**
* Returns the {@link Path} relative to the base {@link Path}.
*
* <p>For example, <code>Path.create("foo/bar/wiz").relativeTo(Path.create("foo"))
* </code> returns <code>Path.create("bar/wiz")</code>.
*
* <p>If the {@link Path} is not a child of the passed {@link Path} an {@link
* IllegalArgumentException} is thrown. In particular, this will happen whenever the two {@link
* Path} instances aren't both absolute or both relative.
*/
public PathFragment relativeTo(Path base) {
Preconditions.checkNotNull(base);
checkSameFileSystem(base);
String basePath = base.path;
if (!OS.startsWith(path, basePath)) {
throw new IllegalArgumentException(
String.format("Path '%s' is not under '%s', cannot relativize", this, base));
}
int bn = basePath.length();
if (bn == 0) {
return PathFragment.createAlreadyNormalized(path, driveStrLength);
}
if (path.length() == bn) {
return PathFragment.EMPTY_FRAGMENT;
}
final int lastSlashIndex;
if (basePath.charAt(bn - 1) == '/') {
lastSlashIndex = bn - 1;
} else {
lastSlashIndex = bn;
}
if (path.charAt(lastSlashIndex) != '/') {
throw new IllegalArgumentException(
String.format("Path '%s' is not under '%s', cannot relativize", this, base));
}
String newPath = path.substring(lastSlashIndex + 1);
return PathFragment.createAlreadyNormalized(newPath, 0);
}
/**
* Returns whether this path is an ancestor of another path.
*
* <p>A path is considered an ancestor of itself.
*/
public boolean startsWith(Path other) {
if (fileSystem != other.fileSystem) {
return false;
}
return startsWith(other.path, other.driveStrLength);
}
/**
* Returns whether this path is an ancestor of another path.
*
* <p>A path is considered an ancestor of itself.
*
* <p>An absolute path can never be an ancestor of a relative path fragment.
*/
public boolean startsWith(PathFragment other) {
if (!other.isAbsolute()) {
return false;
}
String otherPath = other.getPathString();
return startsWith(otherPath, OS.getDriveStrLength(otherPath));
}
private boolean startsWith(String otherPath, int otherDriveStrLength) {
Preconditions.checkNotNull(otherPath);
if (otherPath.length() > path.length()) {
return false;
}
if (driveStrLength != otherDriveStrLength) {
return false;
}
if (!OS.startsWith(path, otherPath)) {
return false;
}
return path.length() == otherPath.length() // Handle equal paths
|| otherPath.length() == driveStrLength // Handle (eg.) 'C:/foo' starts with 'C:/'
// Handle 'true' ancestors, eg. "foo/bar" starts with "foo", but does not start with "fo"
|| path.charAt(otherPath.length()) == SEPARATOR;
}
public FileSystem getFileSystem() {
return fileSystem;
}
public PathFragment asFragment() {
return PathFragment.createAlreadyNormalized(path, driveStrLength);
}
@Override
public String toString() {
return path;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Path other = (Path) o;
if (fileSystem != other.fileSystem) {
return false;
}
return OS.equals(this.path, other.path);
}
@Override
public int hashCode() {
// Do not include file system for efficiency.
// In practice we never construct paths from different file systems.
return OS.hash(this.path);
}
@Override
public int compareTo(Path o) {
// If they are on different file systems, the file system decides the ordering.
FileSystem otherFs = o.getFileSystem();
if (!fileSystem.equals(otherFs)) {
int thisFileSystemHash = System.identityHashCode(fileSystem);
int otherFileSystemHash = System.identityHashCode(otherFs);
if (thisFileSystemHash < otherFileSystemHash) {
return -1;
} else if (thisFileSystemHash > otherFileSystemHash) {
return 1;
}
}
return OS.compare(this.path, o.path);
}
/** Returns true iff this path denotes an existing file of any kind. Follows symbolic links. */
public boolean exists() {
return fileSystem.exists(this, true);
}
/**
* Returns true iff this path denotes an existing file of any kind.
*
* @param followSymlinks if {@link Symlinks#FOLLOW}, and this path denotes a symbolic link, the
* link is dereferenced until a file other than a symbolic link is found
*/
public boolean exists(Symlinks followSymlinks) {
return fileSystem.exists(this, followSymlinks.toBoolean());
}
/**
* Returns a new, immutable collection containing the names of all entities within the directory
* denoted by the current path. Follows symbolic links.
*
* @throws FileNotFoundException If the directory is not found
* @throws IOException If the path does not denote a directory
*/
public Collection<Path> getDirectoryEntries() throws IOException, FileNotFoundException {
Collection<String> entries = fileSystem.getDirectoryEntries(this);
Collection<Path> result = new ArrayList<>(entries.size());
for (String entry : entries) {
result.add(getChild(entry));
}
return result;
}
/**
* Returns a collection of the names and types of all entries within the directory denoted by the
* current path. Follows symbolic links if {@code followSymlinks} is true. Note that the order of
* the returned entries is not guaranteed.
*
* @param followSymlinks whether to follow symlinks or not
* @throws FileNotFoundException If the directory is not found
* @throws IOException If the path does not denote a directory
*/
public Collection<Dirent> readdir(Symlinks followSymlinks) throws IOException {
return fileSystem.readdir(this, followSymlinks.toBoolean());
}
/**
* Returns the status of a file, following symbolic links.
*
* @throws IOException if there was an error obtaining the file status. Note, some implementations
* may defer the I/O, and hence the throwing of the exception, until the accessor methods of
* {@code FileStatus} are called.
*/
public FileStatus stat() throws IOException {
return fileSystem.stat(this, true);
}
/** Like stat(), but returns null on file-nonexistence instead of throwing. */
public FileStatus statNullable() {
return statNullable(Symlinks.FOLLOW);
}
/** Like stat(), but returns null on file-nonexistence instead of throwing. */
public FileStatus statNullable(Symlinks symlinks) {
return fileSystem.statNullable(this, symlinks.toBoolean());
}
/**
* Returns the status of a file, optionally following symbolic links.
*
* @param followSymlinks if {@link Symlinks#FOLLOW}, and this path denotes a symbolic link, the
* link is dereferenced until a file other than a symbolic link is found
* @throws IOException if there was an error obtaining the file status. Note, some implementations
* may defer the I/O, and hence the throwing of the exception, until the accessor methods of
* {@code FileStatus} are called
*/
public FileStatus stat(Symlinks followSymlinks) throws IOException {
return fileSystem.stat(this, followSymlinks.toBoolean());
}
/**
* Like {@link #stat}, but may return null if the file is not found (corresponding to {@code
* ENOENT} and {@code ENOTDIR} in Unix's stat(2) function) instead of throwing. Follows symbolic
* links.
*/
public FileStatus statIfFound() throws IOException {
return fileSystem.statIfFound(this, true);
}
/**
* Like {@link #stat}, but may return null if the file is not found (corresponding to {@code
* ENOENT} and {@code ENOTDIR} in Unix's stat(2) function) instead of throwing.
*
* @param followSymlinks if {@link Symlinks#FOLLOW}, and this path denotes a symbolic link, the
* link is dereferenced until a file other than a symbolic link is found
*/
public FileStatus statIfFound(Symlinks followSymlinks) throws IOException {
return fileSystem.statIfFound(this, followSymlinks.toBoolean());
}
/** Returns true iff this path denotes an existing directory. Follows symbolic links. */
public boolean isDirectory() {
return fileSystem.isDirectory(this, true);
}
/**
* Returns true iff this path denotes an existing directory.
*
* @param followSymlinks if {@link Symlinks#FOLLOW}, and this path denotes a symbolic link, the
* link is dereferenced until a file other than a symbolic link is found
*/
public boolean isDirectory(Symlinks followSymlinks) {
return fileSystem.isDirectory(this, followSymlinks.toBoolean());
}
/**
* Returns true iff this path denotes an existing regular or special file. Follows symbolic links.
*
* <p>For our purposes, "file" includes special files (socket, fifo, block or char devices) too;
* it excludes symbolic links and directories.
*/
public boolean isFile() {
return fileSystem.isFile(this, true);
}
/**
* Returns true iff this path denotes an existing regular or special file.
*
* <p>For our purposes, a "file" includes special files (socket, fifo, block or char devices) too;
* it excludes symbolic links and directories.
*
* @param followSymlinks if {@link Symlinks#FOLLOW}, and this path denotes a symbolic link, the
* link is dereferenced until a file other than a symbolic link is found.
*/
public boolean isFile(Symlinks followSymlinks) {
return fileSystem.isFile(this, followSymlinks.toBoolean());
}
/**
* Returns true iff this path denotes an existing special file (e.g. fifo). Follows symbolic
* links.
*/
public boolean isSpecialFile() {
return fileSystem.isSpecialFile(this, true);
}
/**
* Returns true iff this path denotes an existing special file (e.g. fifo).
*
* @param followSymlinks if {@link Symlinks#FOLLOW}, and this path denotes a symbolic link, the
* link is dereferenced until a path other than a symbolic link is found.
*/
public boolean isSpecialFile(Symlinks followSymlinks) {
return fileSystem.isSpecialFile(this, followSymlinks.toBoolean());
}
/**
* Returns true iff this path denotes an existing symbolic link. Does not follow symbolic links.
*/
public boolean isSymbolicLink() {
return fileSystem.isSymbolicLink(this);
}
/**
* Returns an output stream to the file denoted by the current path, creating it and truncating it
* if necessary. The stream is opened for writing.
*
* @throws FileNotFoundException If the file cannot be found or created.
* @throws IOException If a different error occurs.
*/
public OutputStream getOutputStream() throws IOException, FileNotFoundException {
return getOutputStream(false);
}
/**
* Returns an output stream to the file denoted by the current path, creating it and truncating it
* if necessary. The stream is opened for writing.
*
* @param append whether to open the file in append mode.
* @throws FileNotFoundException If the file cannot be found or created.
* @throws IOException If a different error occurs.
*/
public OutputStream getOutputStream(boolean append) throws IOException, FileNotFoundException {
return fileSystem.getOutputStream(this, append);
}
/**
* Creates a directory with the name of the current path, not following symbolic links. Returns
* normally iff the directory exists after the call: true if the directory was created by this
* call, false if the directory was already in existence. Throws an exception if the directory
* could not be created for any reason.
*
* @throws IOException if the directory creation failed for any reason
*/
public boolean createDirectory() throws IOException {
return fileSystem.createDirectory(this);
}
/**
* Ensures that the directory with the name of the current path and all its ancestor directories
* exist.
*
* <p>Does not return whether the directory already existed or was created by some other
* concurrent call to this method.
*
* @throws IOException if the directory creation failed for any reason
*/
public void createDirectoryAndParents() throws IOException {
fileSystem.createDirectoryAndParents(this);
}
/**
* Creates a symbolic link with the name of the current path, following symbolic links. The
* referent of the created symlink is is the absolute path "target"; it is not possible to create
* relative symbolic links via this method.
*
* @throws IOException if the creation of the symbolic link was unsuccessful for any reason
*/
public void createSymbolicLink(Path target) throws IOException {
checkSameFileSystem(target);
fileSystem.createSymbolicLink(this, target.asFragment());
}
/**
* Creates a symbolic link with the name of the current path, following symbolic links. The
* referent of the created symlink is is the path fragment "target", which may be absolute or
* relative.
*
* @throws IOException if the creation of the symbolic link was unsuccessful for any reason
*/
public void createSymbolicLink(PathFragment target) throws IOException {
fileSystem.createSymbolicLink(this, target);
}
/**
* Returns the target of the current path, which must be a symbolic link. The link contents are
* returned exactly, and may contain an absolute or relative path. Analogous to readlink(2).
*
* <p>Note: for {@link FileSystem}s where {@link FileSystem#supportsSymbolicLinksNatively(Path)}
* returns false, this method will throw an {@link UnsupportedOperationException} if the link
* points to a non-existent file.
*
* @return the content (i.e. target) of the symbolic link
* @throws IOException if the current path is not a symbolic link, or the contents of the link
* could not be read for any reason
*/
public PathFragment readSymbolicLink() throws IOException {
return fileSystem.readSymbolicLink(this);
}
/**
* If the current path is a symbolic link, returns the target of this symbolic link. The semantics
* are intentionally left underspecified otherwise to permit efficient implementations.
*
* @return the content (i.e. target) of the symbolic link
* @throws IOException if the current path is not a symbolic link, or the contents of the link
* could not be read for any reason
*/
public PathFragment readSymbolicLinkUnchecked() throws IOException {
return fileSystem.readSymbolicLinkUnchecked(this);
}
/**
* Create a hard link for the current path.
*
* @param link the path of the new link
* @throws IOException if there was an error executing {@link FileSystem#createHardLink}
*/
public void createHardLink(Path link) throws IOException {
fileSystem.createHardLink(link, this);
}
/**
* Returns the canonical path for this path, by repeatedly replacing symbolic links with their
* referents. Analogous to realpath(3).
*
* @return the canonical path for this path
* @throws IOException if any symbolic link could not be resolved, or other error occurred (for
* example, the path does not exist)
*/
public Path resolveSymbolicLinks() throws IOException {
return fileSystem.resolveSymbolicLinks(this);
}
/**
* Renames the file denoted by the current path to the location "target", not following symbolic
* links.
*
* <p>Files cannot be atomically renamed across devices; copying is required. Use {@link
* FileSystemUtils#copyFile} followed by {@link Path#delete}.
*
* @throws IOException if the rename failed for any reason
*/
public void renameTo(Path target) throws IOException {
checkSameFileSystem(target);
fileSystem.renameTo(this, target);
}
/**
* Returns the size in bytes of the file denoted by the current path, following symbolic links.
*
* <p>The size of a directory or special file is undefined and should not be used.
*
* @throws FileNotFoundException if the file denoted by the current path does not exist
* @throws IOException if the file's metadata could not be read, or some other error occurred
*/
public long getFileSize() throws IOException, FileNotFoundException {
return fileSystem.getFileSize(this, true);
}
/**
* Returns the size in bytes of the file denoted by the current path.
*
* <p>The size of directory or special file is undefined. The size of a symbolic link is the
* length of the name of its referent.
*
* @param followSymlinks if {@link Symlinks#FOLLOW}, and this path denotes a symbolic link, the
* link is deferenced until a file other than a symbol link is found
* @throws FileNotFoundException if the file denoted by the current path does not exist
* @throws IOException if the file's metadata could not be read, or some other error occurred
*/
public long getFileSize(Symlinks followSymlinks) throws IOException, FileNotFoundException {
return fileSystem.getFileSize(this, followSymlinks.toBoolean());
}
/**
* Deletes the file denoted by this path, not following symbolic links. Returns normally iff the
* file doesn't exist after the call: true if this call deleted the file, false if the file
* already didn't exist. Throws an exception if the file could not be deleted for any reason.
*
* @return true iff the file was actually deleted by this call
* @throws IOException if the deletion failed but the file was present prior to the call
*/
public boolean delete() throws IOException {
return fileSystem.delete(this);
}
/**
* Deletes all directory trees recursively beneath this path and removes the path as well.
*
* @throws IOException if the hierarchy cannot be removed successfully
*/
public void deleteTree() throws IOException {
fileSystem.deleteTree(this);
}
/**
* Deletes all directory trees recursively beneath this path. Does nothing if the path is not a
* directory.
*
* @throws IOException if the hierarchy cannot be removed successfully
*/
public void deleteTreesBelow() throws IOException {
fileSystem.deleteTreesBelow(this);
}
/**
* Returns the last modification time of the file, in milliseconds since the UNIX epoch, of the
* file denoted by the current path, following symbolic links.
*
* <p>Caveat: many filesystems store file times in seconds, so do not rely on the millisecond
* precision.
*
* @throws IOException if the operation failed for any reason
*/
public long getLastModifiedTime() throws IOException {
return fileSystem.getLastModifiedTime(this, true);
}
/**
* Returns the last modification time of the file, in milliseconds since the UNIX epoch, of the
* file denoted by the current path.
*
* <p>Caveat: many filesystems store file times in seconds, so do not rely on the millisecond
* precision.
*
* @param followSymlinks if {@link Symlinks#FOLLOW}, and this path denotes a symbolic link, the
* link is dereferenced until a file other than a symbolic link is found
* @throws IOException if the modification time for the file could not be obtained for any reason
*/
public long getLastModifiedTime(Symlinks followSymlinks) throws IOException {
return fileSystem.getLastModifiedTime(this, followSymlinks.toBoolean());
}
/**
* Sets the modification time of the file denoted by the current path. Follows symbolic links. If
* newTime is -1, the current time according to the kernel is used; this may differ from the JVM's
* clock.
*
* <p>Caveat: many filesystems store file times in seconds, so do not rely on the millisecond
* precision.
*
* @param newTime time, in milliseconds since the UNIX epoch, or -1L, meaning use the kernel's
* current time
* @throws IOException if the modification time for the file could not be set for any reason
*/
public void setLastModifiedTime(long newTime) throws IOException {
fileSystem.setLastModifiedTime(this, newTime);
}
/**
* Returns the value of the given extended attribute name or null if the attribute does not exist
* or the file system does not support extended attributes. Follows symlinks.
*/
public byte[] getxattr(String name) throws IOException {
return getxattr(name, Symlinks.FOLLOW);
}
/**
* Returns the value of the given extended attribute name or null if the attribute does not exist
* or the file system does not support extended attributes.
*
* @param followSymlinks whether to follow symlinks or not
*/
public byte[] getxattr(String name, Symlinks followSymlinks) throws IOException {
return fileSystem.getxattr(this, name, followSymlinks.toBoolean());
}
/**
* Gets a fast digest for the given path, or {@code null} if there isn't one available. The digest
* should be suitable for detecting changes to the file.
*/
public byte[] getFastDigest() throws IOException {
return fileSystem.getFastDigest(this);
}
/**
* Returns the digest of the file denoted by the current path, following symbolic links.
*
* @return a new byte array containing the file's digest
* @throws IOException if the digest could not be computed for any reason
*/
public byte[] getDigest() throws IOException {
return fileSystem.getDigest(this);
}
/**
* Return a string representation, as hexadecimal digits, of some hash of the directory.
*
* <p>The hash itself is computed according to the design document
* https://github.com/bazelbuild/proposals/blob/master/designs/2018-07-13-repository-hashing.md
* and takes enough information into account, to detect the typical non-reproducibility
* of source-like repository rules, while leaving out what will change from invocation to
* invocation of a repository rule (in particular file owners) and can reasonably be ignored
* when considering if a repository is "the same source tree".
*
* @return a string representation of the bash of the directory
* @throws IOException if the digest could not be computed for any reason
*/
public String getDirectoryDigest() throws IOException {
List<String> entries = new ArrayList<String>(fileSystem.getDirectoryEntries(this));
Collections.sort(entries);
Hasher hasher = fileSystem.getDigestFunction().getHashFunction().newHasher();
for (String entry : entries) {
Path path = this.getChild(entry);
FileStatus stat = path.stat(Symlinks.NOFOLLOW);
hasher.putUnencodedChars(entry);
if (stat.isFile()) {
if (path.isExecutable()) {
hasher.putChar('x');
} else {
hasher.putChar('-');
}
hasher.putBytes(path.getDigest());
} else if (stat.isDirectory()) {
hasher.putChar('d').putUnencodedChars(path.getDirectoryDigest());
} else if (stat.isSymbolicLink()) {
PathFragment link = path.readSymbolicLink();
if (link.isAbsolute()) {
try {
Path resolved = path.resolveSymbolicLinks();
if (resolved.isFile()) {
if (resolved.isExecutable()) {
hasher.putChar('x');
} else {
hasher.putChar('-');
}
hasher.putBytes(resolved.getDigest());
} else {
// link to a non-file: include the link itself in the hash
hasher.putChar('l').putUnencodedChars(link.toString());
}
} catch (IOException e) {
// dangling link: include the link itself in the hash
hasher.putChar('l').putUnencodedChars(link.toString());
}
} else {
// relative link: include the link itself in the hash
hasher.putChar('l').putUnencodedChars(link.toString());
}
} else {
// Neither file, nor directory, nor symlink. So do not include further information
// in the hash, asuming it will not be used during the BUILD anyway.
hasher.putChar('s');
}
}
return hasher.hash().toString();
}
/**
* Opens the file denoted by this path, following symbolic links, for reading, and returns an
* input stream to it.
*
* @throws IOException if the file was not found or could not be opened for reading
*/
public InputStream getInputStream() throws IOException {
return fileSystem.getInputStream(this);
}
/**
* Opens the file denoted by this path, following symbolic links, for reading, and returns a file
* channel for it.
*
* @throws IOException if the file was not found or could not be opened for reading
*/
public ReadableByteChannel createReadableByteChannel() throws IOException {
return fileSystem.createReadableByteChannel(this);
}
/**
* Returns a java.io.File representation of this path.
*
* <p>Caveat: the result may be useless if this path's getFileSystem() is not
* the UNIX filesystem.
*/
public File getPathFile() {
return new File(getPathString());
}
/**
* Returns true if the file denoted by the current path, following symbolic links, is writable for
* the current user.
*
* @throws FileNotFoundException if the file does not exist, a dangling symbolic link was
* encountered, or the file's metadata could not be read
*/
public boolean isWritable() throws IOException, FileNotFoundException {
return fileSystem.isWritable(this);
}
/**
* Sets the read permissions of the file denoted by the current path, following symbolic links.
* Permissions apply to the current user.
*
* @param readable if true, the file is set to readable; otherwise the file is made non-readable
* @throws FileNotFoundException if the file does not exist
* @throws IOException If the action cannot be taken (ie. permissions)
*/
public void setReadable(boolean readable) throws IOException, FileNotFoundException {
fileSystem.setReadable(this, readable);
}
/**
* Sets the write permissions of the file denoted by the current path, following symbolic links.
* Permissions apply to the current user.
*
* <p>TODO(bazel-team): (2009) what about owner/group/others?
*
* @param writable if true, the file is set to writable; otherwise the file is made non-writable
* @throws FileNotFoundException if the file does not exist
* @throws IOException If the action cannot be taken (ie. permissions)
*/
public void setWritable(boolean writable) throws IOException, FileNotFoundException {
fileSystem.setWritable(this, writable);
}
/**
* Returns true iff the file specified by the current path, following symbolic links, is
* executable by the current user.
*
* @throws FileNotFoundException if the file does not exist or a dangling symbolic link was
* encountered
* @throws IOException if some other I/O error occurred
*/
public boolean isExecutable() throws IOException, FileNotFoundException {
return fileSystem.isExecutable(this);
}
/**
* Returns true iff the file specified by the current path, following symbolic links, is readable
* by the current user.
*
* @throws FileNotFoundException if the file does not exist or a dangling symbolic link was
* encountered
* @throws IOException if some other I/O error occurred
*/
public boolean isReadable() throws IOException, FileNotFoundException {
return fileSystem.isReadable(this);
}
/**
* Sets the execute permission on the file specified by the current path, following symbolic
* links. Permissions apply to the current user.
*
* @throws FileNotFoundException if the file does not exist or a dangling symbolic link was
* encountered
* @throws IOException if the metadata change failed, for example because of permissions
*/
public void setExecutable(boolean executable) throws IOException, FileNotFoundException {
fileSystem.setExecutable(this, executable);
}
/**
* Sets the permissions on the file specified by the current path, following symbolic links. If
* permission changes on this path's {@link FileSystem} are slow (e.g. one syscall per change),
* this method should aim to be faster than setting each permission individually. If this path's
* {@link FileSystem} does not support group and others permissions, those bits will be ignored.
*
* @throws FileNotFoundException if the file does not exist or a dangling symbolic link was
* encountered
* @throws IOException if the metadata change failed, for example because of permissions
*/
public void chmod(int mode) throws IOException {
fileSystem.chmod(this, mode);
}
public void prefetchPackageAsync(int maxDirs) {
fileSystem.prefetchPackageAsync(this, maxDirs);
}
private void checkSameFileSystem(Path that) {
if (this.fileSystem != that.fileSystem) {
throw new IllegalArgumentException(
"Files are on different filesystems: " + this + ", " + that);
}
}
private void writeObject(ObjectOutputStream out) throws IOException {
Preconditions.checkState(
fileSystem == fileSystemForSerialization, "%s %s", fileSystem, fileSystemForSerialization);
out.writeUTF(path);
}
private void readObject(ObjectInputStream in) throws IOException {
path = in.readUTF();
fileSystem = fileSystemForSerialization;
driveStrLength = OS.getDriveStrLength(path);
}
}