blob: e763ebe4e8a35d55fa9400c374460a9795df56bd [file] [log] [blame]
// 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.inmemoryfs;
import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadSafe;
import com.google.devtools.build.lib.unix.FileAccessException;
import com.google.devtools.build.lib.util.Clock;
import com.google.devtools.build.lib.util.JavaClock;
import com.google.devtools.build.lib.util.Preconditions;
import com.google.devtools.build.lib.vfs.FileStatus;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.lib.vfs.ScopeEscapableFileSystem;
import com.google.devtools.build.lib.vfs.Symlinks;
import java.io.ByteArrayInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.Stack;
import javax.annotation.Nullable;
/**
* This class provides a complete in-memory file system.
*
* <p>Naming convention: we use "path" for all {@link Path} variables, since these
* represent *names* and we use "node" or "inode" for InMemoryContentInfo
* variables, since these correspond to inodes in the UNIX file system.
*
* <p>The code is structured to be as similar to the implementation of UNIX "namei"
* as is reasonably possibly. This provides a firm reference point for many
* concepts and makes compatibility easier to achieve.
*
* <p>As a scope-escapable file system, this class supports re-delegation of symbolic links
* that escape its root. This is done through the use of {@link OutOfScopeFileStatus}
* and {@link OutOfScopeDirectoryStatus} objects, which may be returned by
* getDirectory, pathWalk, and scopeLimitedStat. Any code that calls one of these
* methods (either directly or indirectly) is obligated to check the possibility
* that its info represents an out-of-scope path. Lack of such a check will result
* in unchecked runtime exceptions upon any request for status data (as well as
* possible logical errors).
*/
@ThreadSafe
public class InMemoryFileSystem extends ScopeEscapableFileSystem {
private final Clock clock;
// The root inode (a directory).
private final InMemoryDirectoryInfo rootInode;
// Maximum number of traversals before ELOOP is thrown.
private static final int MAX_TRAVERSALS = 256;
/**
* Creates a new InMemoryFileSystem with scope checking disabled (all paths are considered to be
* within scope) and a default clock.
*/
public InMemoryFileSystem() {
this(new JavaClock());
}
/**
* Creates a new InMemoryFileSystem with scope checking disabled (all
* paths are considered to be within scope).
*/
public InMemoryFileSystem(Clock clock) {
this(clock, null);
}
/**
* Creates a new InMemoryFileSystem with scope checking bound to
* scopeRoot, i.e. any path that's not below scopeRoot is considered
* to be out of scope.
*/
protected InMemoryFileSystem(Clock clock, PathFragment scopeRoot) {
super(scopeRoot);
this.clock = clock;
this.rootInode = new InMemoryDirectoryInfo(clock);
rootInode.addChild(".", rootInode);
rootInode.addChild("..", rootInode);
}
/**
* The errors that {@link InMemoryFileSystem} might issue for different sorts of IO failures.
*/
public enum Error {
ENOENT("No such file or directory"),
EACCES("Permission denied"),
ENOTDIR("Not a directory"),
EEXIST("File exists"),
EBUSY("Device or resource busy"),
ENOTEMPTY("Directory not empty"),
EISDIR("Is a directory"),
ELOOP("Too many levels of symbolic links");
private final String message;
private Error(String message) {
this.message = message;
}
@Override
public String toString() {
return message;
}
/** Implemented by exceptions that contain the extra info of which Error caused them. */
private static interface WithError {
Error getError();
}
/**
* The exceptions below extend their parent classes in order to additionally store the error
* that caused them. However, they must impersonate their parents to any outside callers,
* including in their toString() method, which prints the class name followed by the exception
* method. This method returns the same value as the toString() method of a {@link Throwable}'s
* parent would, so that the child class can have the same toString() value.
*/
private static String parentThrowableToString(Throwable obj) {
String s = obj.getClass().getSuperclass().getName();
String message = obj.getLocalizedMessage();
return (message != null) ? (s + ": " + message) : s;
}
private static class IOExceptionWithError extends IOException implements WithError {
private final Error errorCode;
private IOExceptionWithError(String message, Error errorCode) {
super(message);
this.errorCode = errorCode;
}
@Override
public Error getError() {
return errorCode;
}
@Override
public String toString() {
return parentThrowableToString(this);
}
}
private static class FileNotFoundExceptionWithError
extends FileNotFoundException implements WithError {
private final Error errorCode;
private FileNotFoundExceptionWithError(String message, Error errorCode) {
super(message);
this.errorCode = errorCode;
}
@Override
public Error getError() {
return errorCode;
}
@Override
public String toString() {
return parentThrowableToString(this);
}
}
private static class FileAccessExceptionWithError
extends FileAccessException implements WithError {
private final Error errorCode;
private FileAccessExceptionWithError(String message, Error errorCode) {
super(message);
this.errorCode = errorCode;
}
@Override
public Error getError() {
return errorCode;
}
@Override
public String toString() {
return parentThrowableToString(this);
}
}
/**
* Returns a new IOException for the error. The exception message
* contains 'path', and is consistent with the messages returned by
* c.g.common.unix.FilesystemUtils.
*/
public IOException exception(Path path) throws IOException {
String m = path + " (" + message + ")";
if (this == EACCES) {
throw new FileAccessExceptionWithError(m, this);
} else if (this == ENOENT) {
throw new FileNotFoundExceptionWithError(m, this);
} else {
throw new IOExceptionWithError(m, this);
}
}
}
/**
* {@inheritDoc}
*
* <p>If <code>/proc/mounts</code> does not exist return {@code "inmemoryfs"}.
*/
@Override
public String getFileSystemType(Path path) {
return path.getRelative("/proc/mounts").exists() ? super.getFileSystemType(path) : "inmemoryfs";
}
/****************************************************************************
* "Kernel" primitives: basic directory lookup primitives, in topological
* order.
*/
/**
* Unlinks the entry 'child' from its existing parent directory 'dir'. Dual to
* insert. This succeeds even if 'child' names a non-empty directory; we need
* that for renameTo. 'child' must be a member of its parent directory,
* however. Fails if the directory was read-only.
*/
private void unlink(InMemoryDirectoryInfo dir, String child, Path errorPath)
throws IOException {
if (!dir.isWritable()) { throw Error.EACCES.exception(errorPath); }
dir.removeChild(child);
}
/**
* Inserts inode 'childInode' into the existing directory 'dir' under the
* specified 'name'. Dual to unlink. Fails if the directory was read-only.
*/
private void insert(InMemoryDirectoryInfo dir, String child,
InMemoryContentInfo childInode, Path errorPath)
throws IOException {
if (!dir.isWritable()) { throw Error.EACCES.exception(errorPath); }
dir.addChild(child, childInode);
}
/**
* Given an existing directory 'dir', looks up 'name' within it and returns
* its inode. Assumes the file exists, unless 'create', in which case it will
* try to create it. May fail with ENOTDIR, EACCES, ENOENT. Error messages
* will be reported against file 'path'.
*/
private InMemoryContentInfo directoryLookup(InMemoryContentInfo dir,
String name,
boolean create,
Path path) throws IOException {
if (!dir.isDirectory()) { throw Error.ENOTDIR.exception(path); }
InMemoryDirectoryInfo imdi = (InMemoryDirectoryInfo) dir;
if (!imdi.isExecutable()) { throw Error.EACCES.exception(path); }
InMemoryContentInfo child = imdi.getChild(name);
if (child == null) {
if (!create) {
throw Error.ENOENT.exception(path);
} else {
child = makeFileInfo(clock, path.asFragment());
insert(imdi, name, child, path);
}
}
return child;
}
/**
* Low-level path-to-inode lookup routine. Analogous to path_walk() in many
* UNIX kernels. Given 'path', walks the directory tree from the root,
* resolving all symbolic links, and returns the designated inode.
*
* <p>If 'create' is false, the inode must exist; otherwise, it will be created
* and added to its parent directory, which must exist.
*
* <p>Iff the given path escapes this file system's scope, the returned value
* is an {@link OutOfScopeFileStatus} instance. Any code that calls this method
* needs to check for that possibility (via {@link ScopeEscapableStatus#outOfScope}).
*
* <p>May fail with ENOTDIR, ENOENT, EACCES, ELOOP.
*/
private synchronized InMemoryContentInfo pathWalk(Path path, boolean create)
throws IOException {
// Implementation note: This is where we check for out-of-scope symlinks and
// trigger re-delegation to another file system accordingly. This code handles
// both absolute and relative symlinks. Some assumptions we make: First, only
// symlink targets as read from getNormalizedLinkContent() can escape our scope.
// This is because Path objects are all canonicalized (see {@link Path#getRelative},
// etc.) and symlink target segments that get added to the stack are in-scope by
// definition. Second, symlink targets with relative segments must have the form
// [".."]*[standard segment]+, i.e. only the ".." non-standard segment is allowed
// and it may only appear as part of a contiguous prefix sequence.
Stack<String> stack = new Stack<>();
PathFragment rootPathFragment = rootPath.asFragment();
for (Path p = path; !p.asFragment().equals(rootPathFragment); p = p.getParentDirectory()) {
stack.push(p.getBaseName());
}
InMemoryContentInfo inode = rootInode;
int parentDepth = -1;
int traversals = 0;
while (!stack.isEmpty()) {
traversals++;
String name = stack.pop();
parentDepth += name.equals("..") ? -1 : 1;
// ENOENT on last segment with 'create' => create a new file.
InMemoryContentInfo child = directoryLookup(inode, name, create && stack.isEmpty(), path);
if (child.isSymbolicLink()) {
PathFragment linkTarget = ((InMemoryLinkInfo) child).getNormalizedLinkContent();
if (!inScope(parentDepth, linkTarget)) {
return outOfScopeStatus(linkTarget, parentDepth, stack);
}
if (linkTarget.isAbsolute()) {
inode = rootInode;
parentDepth = -1;
}
if (traversals > MAX_TRAVERSALS) {
throw Error.ELOOP.exception(path);
}
for (int ii = linkTarget.segmentCount() - 1; ii >= 0; --ii) {
stack.push(linkTarget.getSegment(ii)); // Note this may include ".." segments.
}
} else {
inode = child;
}
}
return inode;
}
/**
* Helper routine for pathWalk: given a symlink target known to escape this file system's
* scope (and that has the form [".."]*[standard segment]+), the number of segments
* in the directory containing the symlink, and the remaining path segments following
* the symlink in the original input to pathWalk, returns an OutofScopeFileStatus
* initialized with an appropriate out-of-scope reformulation of pathWalk's original
* input.
*/
private OutOfScopeFileStatus outOfScopeStatus(PathFragment linkTarget, int parentDepth,
Stack<String> descendantSegments) {
PathFragment escapingPath;
if (linkTarget.isAbsolute()) {
escapingPath = linkTarget;
} else {
// Relative out-of-scope paths must look like "../../../a/b/c". Find the target's
// parent path depth by subtracting one from parentDepth for each ".." reference.
// Then use that to retrieve a prefix of the scope root, which is the target's
// canonicalized parent path.
int leadingParentRefs = leadingParentReferences(linkTarget);
int baseDepth = parentDepth - leadingParentRefs;
Preconditions.checkState(baseDepth < scopeRoot.segmentCount());
escapingPath = baseDepth > 0
? scopeRoot.subFragment(0, baseDepth)
: scopeRoot.subFragment(0, 0);
// Now add in everything that comes after the ".." sequence.
for (int i = leadingParentRefs; i < linkTarget.segmentCount(); i++) {
escapingPath = escapingPath.getRelative(linkTarget.getSegment(i));
}
}
// We've now converted the symlink to its target in canonicalized absolute path
// form. Since the symlink wasn't necessarily the final segment in the original
// input sent to pathWalk, now add in every segment that came after.
while (!descendantSegments.empty()) {
escapingPath = escapingPath.getRelative(descendantSegments.pop());
}
return new OutOfScopeFileStatus(escapingPath);
}
/**
* Given 'path', returns the existing directory inode it designates,
* following symbolic links.
*
* <p>May fail with ENOTDIR, or any exception from pathWalk.
*
* <p>Iff the given path escapes this file system's scope, this method skips
* ENOTDIR checking and returns an OutOfScopeDirectoryStatus instance. Any
* code that calls this method needs to check for that possibility
* (via {@link ScopeEscapableStatus#outOfScope}).
*/
private InMemoryDirectoryInfo getDirectory(Path path) throws IOException {
InMemoryContentInfo dirInfo = pathWalk(path, false);
if (dirInfo.outOfScope()) {
return new OutOfScopeDirectoryStatus(dirInfo.getEscapingPath());
} else if (!dirInfo.isDirectory()) {
throw Error.ENOTDIR.exception(path);
} else {
return (InMemoryDirectoryInfo) dirInfo;
}
}
/**
* Helper method for stat, scopeLimitedStat: lock the internal state and return the
* path's (no symlink-followed) stat if the path's parent directory is within scope,
* else return an "out of scope" reference to the path's parent directory (which will
* presumably be re-delegated to another FS).
*/
private synchronized InMemoryContentInfo getNoFollowStatOrOutOfScopeParent(Path path)
throws IOException {
InMemoryDirectoryInfo dirInfo = getDirectory(path.getParentDirectory());
return dirInfo.outOfScope()
? dirInfo
: directoryLookup(dirInfo, path.getBaseName(), /*create=*/false, path);
}
/**
* Given 'path', returns the existing inode it designates, optionally
* following symbolic links. Analogous to UNIX stat(2)/lstat(2), except that
* it returns a mutable inode we can modify directly.
*/
@Override
public FileStatus stat(Path path, boolean followSymlinks) throws IOException {
if (followSymlinks) {
InMemoryContentInfo status = scopeLimitedStat(path, true);
return status.outOfScope()
? statWithDelegator(status.getEscapingPath(), true)
: status;
} else {
if (path.equals(rootPath)) {
return rootInode;
} else {
InMemoryContentInfo status = getNoFollowStatOrOutOfScopeParent(path);
// If out of scope, status references the path's parent directory. Else it references the
// path itself.
return status.outOfScope()
? getDelegatedPath(status.getEscapingPath().getRelative(
path.getBaseName())).stat(Symlinks.NOFOLLOW)
: status;
}
}
}
@Override
@Nullable
public FileStatus statIfFound(Path path, boolean followSymlinks) throws IOException {
try {
return stat(path, followSymlinks);
} catch (IOException e) {
if (e instanceof Error.WithError) {
Error errorCode = ((Error.WithError) e).getError();
if (errorCode == Error.ENOENT || errorCode == Error.ENOTDIR) {
return null;
}
}
throw e;
}
}
/**
* Version of stat that returns an inode if the input path stays entirely within
* this file system's scope, otherwise an {@link OutOfScopeFileStatus}.
*
* <p>Any code that calls this method needs to check for either possibility via
* {@link ScopeEscapableStatus#outOfScope}.
*/
protected InMemoryContentInfo scopeLimitedStat(Path path, boolean followSymlinks)
throws IOException {
if (followSymlinks) {
return pathWalk(path, false);
} else {
if (path.equals(rootPath)) {
return rootInode;
} else {
InMemoryContentInfo status = getNoFollowStatOrOutOfScopeParent(path);
// If out of scope, status references the path's parent directory. Else it references the
// path itself.
return status.outOfScope()
? new OutOfScopeFileStatus(status.getEscapingPath().getRelative(path.getBaseName()))
: status;
}
}
}
/****************************************************************************
* FileSystem methods
*/
/**
* This is a helper routing for {@link #resolveSymbolicLinks(Path)}, i.e.
* the "user-mode" routing for canonicalising paths. It is analogous to the
* code in glibc's realpath(3).
*
* <p>Just like realpath, resolveSymbolicLinks requires a quadratic number of
* directory lookups: n path segments are statted, and each stat requires a
* linear amount of work in the "kernel" routine.
*/
@Override
protected PathFragment resolveOneLink(Path path) throws IOException {
// Beware, this seemingly simple code belies the complex specification of
// FileSystem.resolveOneLink().
InMemoryContentInfo status = scopeLimitedStat(path, false);
if (status.outOfScope()) {
return resolveOneLinkWithDelegator(status.getEscapingPath());
} else {
return status.isSymbolicLink()
? ((InMemoryLinkInfo) status).getLinkContent()
: null;
}
}
@Override
protected boolean isDirectory(Path path, boolean followSymlinks) {
try {
return stat(path, followSymlinks).isDirectory();
} catch (IOException e) {
return false;
}
}
@Override
protected boolean isFile(Path path, boolean followSymlinks) {
try {
return stat(path, followSymlinks).isFile();
} catch (IOException e) {
return false;
}
}
@Override
protected boolean isSpecialFile(Path path, boolean followSymlinks) {
try {
return stat(path, followSymlinks).isSpecialFile();
} catch (IOException e) {
return false;
}
}
@Override
protected boolean isSymbolicLink(Path path) {
try {
return stat(path, false).isSymbolicLink();
} catch (IOException e) {
return false;
}
}
@Override
protected boolean exists(Path path, boolean followSymlinks) {
try {
stat(path, followSymlinks);
return true;
} catch (IOException e) {
return false;
}
}
/**
* Like {@link #exists}, but checks for existence within this filesystem's scope.
*/
protected boolean scopeLimitedExists(Path path, boolean followSymlinks) {
try {
// Path#asFragment() always returns an absolute path, so inScope() is called with
// parentDepth = 0.
return inScope(0, path.asFragment()) && !scopeLimitedStat(path, followSymlinks).outOfScope();
} catch (IOException e) {
return false;
}
}
@Override
protected boolean isReadable(Path path) throws IOException {
InMemoryContentInfo status = scopeLimitedStat(path, true);
return status.outOfScope()
? getDelegatedPath(status.getEscapingPath()).isReadable()
: status.isReadable();
}
@Override
protected void setReadable(Path path, boolean readable) throws IOException {
InMemoryContentInfo status;
synchronized (this) {
status = scopeLimitedStat(path, true);
if (!status.outOfScope()) {
status.setReadable(readable);
return;
}
}
// If we get here, we're out of scope.
getDelegatedPath(status.getEscapingPath()).setReadable(readable);
}
@Override
protected boolean isWritable(Path path) throws IOException {
InMemoryContentInfo status = scopeLimitedStat(path, true);
return status.outOfScope()
? getDelegatedPath(status.getEscapingPath()).isWritable()
: status.isWritable();
}
@Override
protected void setWritable(Path path, boolean writable) throws IOException {
InMemoryContentInfo status;
synchronized (this) {
status = scopeLimitedStat(path, true);
if (!status.outOfScope()) {
status.setWritable(writable);
return;
}
}
// If we get here, we're out of scope.
getDelegatedPath(status.getEscapingPath()).setWritable(writable);
}
@Override
protected boolean isExecutable(Path path) throws IOException {
InMemoryContentInfo status = scopeLimitedStat(path, true);
return status.outOfScope()
? getDelegatedPath(status.getEscapingPath()).isExecutable()
: status.isExecutable();
}
@Override
protected void setExecutable(Path path, boolean executable)
throws IOException {
InMemoryContentInfo status;
synchronized (this) {
status = scopeLimitedStat(path, true);
if (!status.outOfScope()) {
status.setExecutable(executable);
return;
}
}
// If we get here, we're out of scope.
getDelegatedPath(status.getEscapingPath()).setExecutable(executable);
}
@Override
public boolean supportsModifications() {
return true;
}
@Override
public boolean supportsSymbolicLinksNatively() {
return true;
}
@Override
public boolean supportsHardLinksNatively() {
return true;
}
@Override
public boolean isFilePathCaseSensitive() {
return true;
}
/**
* Constructs a new inode. Provided so that subclasses of InMemoryFileSystem
* can inject subclasses of FileInfo properly.
*/
protected FileInfo makeFileInfo(Clock clock, PathFragment frag) {
return new InMemoryFileInfo(clock);
}
/**
* Returns a new path constructed by appending the child's base name to the
* escaped parent path. For example, assume our file system root is /foo
* and /foo/link1 -> /bar. This method can be used on child = /foo/link1/link2/name
* and parent = /bar/link2 to return /bar/link2/name, which is a semi-resolved
* path bound to a different file system.
*/
private Path getDelegatedPath(PathFragment escapedParent, Path child) {
return getDelegatedPath(escapedParent.getRelative(child.getBaseName()));
}
@Override
protected boolean createDirectory(Path path) throws IOException {
if (path.equals(rootPath)) { throw Error.EACCES.exception(path); }
InMemoryDirectoryInfo parent;
synchronized (this) {
parent = getDirectory(path.getParentDirectory());
if (!parent.outOfScope()) {
InMemoryContentInfo child = parent.getChild(path.getBaseName());
if (child != null) { // already exists
if (child.isDirectory()) {
return false;
} else {
throw Error.EEXIST.exception(path);
}
}
InMemoryDirectoryInfo newDir = new InMemoryDirectoryInfo(clock);
newDir.addChild(".", newDir);
newDir.addChild("..", parent);
insert(parent, path.getBaseName(), newDir, path);
return true;
}
}
// If we get here, we're out of scope.
return getDelegatedPath(parent.getEscapingPath(), path).createDirectory();
}
@Override
protected void createSymbolicLink(Path path, PathFragment targetFragment)
throws IOException {
if (path.equals(rootPath)) { throw Error.EACCES.exception(path); }
InMemoryDirectoryInfo parent;
synchronized (this) {
parent = getDirectory(path.getParentDirectory());
if (!parent.outOfScope()) {
if (parent.getChild(path.getBaseName()) != null) { throw Error.EEXIST.exception(path); }
insert(parent, path.getBaseName(), new InMemoryLinkInfo(clock, targetFragment), path);
return;
}
}
// If we get here, we're out of scope.
getDelegatedPath(parent.getEscapingPath(), path).createSymbolicLink(targetFragment);
}
@Override
protected PathFragment readSymbolicLink(Path path) throws IOException {
InMemoryContentInfo status = scopeLimitedStat(path, false);
if (status.outOfScope()) {
return getDelegatedPath(status.getEscapingPath()).readSymbolicLink();
} else if (status.isSymbolicLink()) {
Preconditions.checkState(status instanceof InMemoryLinkInfo);
return ((InMemoryLinkInfo) status).getLinkContent();
} else {
throw new NotASymlinkException(path);
}
}
@Override
protected long getFileSize(Path path, boolean followSymlinks)
throws IOException {
return stat(path, followSymlinks).getSize();
}
@Override
protected Collection<Path> getDirectoryEntries(Path path) throws IOException {
InMemoryDirectoryInfo dirInfo;
synchronized (this) {
dirInfo = getDirectory(path);
if (!dirInfo.outOfScope()) {
FileStatus status = stat(path, false);
Preconditions.checkState(status instanceof InMemoryContentInfo);
if (!((InMemoryContentInfo) status).isReadable()) {
throw new IOException("Directory is not readable");
}
Set<String> allChildren = dirInfo.getAllChildren();
List<Path> result = new ArrayList<>(allChildren.size());
for (String child : allChildren) {
if (!(child.equals(".") || child.equals(".."))) {
result.add(path.getChild(child));
}
}
return result;
}
}
// If we get here, we're out of scope.
return getDelegatedPath(dirInfo.getEscapingPath()).getDirectoryEntries();
}
@Override
protected boolean delete(Path path) throws IOException {
if (path.equals(rootPath)) { throw Error.EBUSY.exception(path); }
if (!exists(path, false)) { return false; }
InMemoryDirectoryInfo parent;
synchronized (this) {
parent = getDirectory(path.getParentDirectory());
if (!parent.outOfScope()) {
InMemoryContentInfo child = parent.getChild(path.getBaseName());
if (child.isDirectory() && child.getSize() > 2) { throw Error.ENOTEMPTY.exception(path); }
unlink(parent, path.getBaseName(), path);
return true;
}
}
// If we get here, we're out of scope.
return getDelegatedPath(parent.getEscapingPath(), path).delete();
}
@Override
protected long getLastModifiedTime(Path path, boolean followSymlinks)
throws IOException {
return stat(path, followSymlinks).getLastModifiedTime();
}
@Override
protected void setLastModifiedTime(Path path, long newTime) throws IOException {
InMemoryContentInfo status;
synchronized (this) {
status = scopeLimitedStat(path, true);
if (!status.outOfScope()) {
status.setLastModifiedTime(newTime == -1L
? clock.currentTimeMillis()
: newTime);
return;
}
}
// If we get here, we're out of scope.
getDelegatedPath(status.getEscapingPath()).setLastModifiedTime(newTime);
}
@Override
protected InputStream getInputStream(Path path) throws IOException {
InMemoryContentInfo status;
synchronized (this) {
status = scopeLimitedStat(path, true);
if (!status.outOfScope()) {
if (status.isDirectory()) { throw Error.EISDIR.exception(path); }
if (!path.isReadable()) { throw Error.EACCES.exception(path); }
Preconditions.checkState(status instanceof FileInfo);
return new ByteArrayInputStream(((FileInfo) status).readContent());
}
}
// If we get here, we're out of scope.
return getDelegatedPath(status.getEscapingPath()).getInputStream();
}
/**
* Creates a new file at the given path and returns its inode. If the path
* escapes this file system's scope, trivially returns an "out of scope" status.
* Calling code should check for both possibilities via
* {@link ScopeEscapableStatus#outOfScope}.
*/
protected InMemoryContentInfo getOrCreateWritableInode(Path path)
throws IOException {
// open(WR_ONLY) of a dangling link writes through the link. That means
// that the usual path lookup operations have to behave differently when
// resolving a path with the intent to create it: instead of failing with
// ENOENT they have to return an open file. This is exactly how UNIX
// kernels do it, which is what we're trying to emulate.
InMemoryContentInfo child = pathWalk(path, /*create=*/true);
Preconditions.checkNotNull(child);
if (child.outOfScope()) {
return child;
} else if (child.isDirectory()) {
throw Error.EISDIR.exception(path);
} else { // existing or newly-created file
if (!child.isWritable()) { throw Error.EACCES.exception(path); }
return child;
}
}
@Override
protected OutputStream getOutputStream(Path path, boolean append)
throws IOException {
InMemoryContentInfo status;
synchronized (this) {
status = getOrCreateWritableInode(path);
if (!status.outOfScope()) {
return ((FileInfo) getOrCreateWritableInode(path)).getOutputStream(append);
}
}
// If we get here, we're out of scope.
return getDelegatedPath(status.getEscapingPath()).getOutputStream(append);
}
@Override
protected void renameTo(Path sourcePath, Path targetPath)
throws IOException {
if (sourcePath.equals(rootPath)) { throw Error.EACCES.exception(sourcePath); }
if (targetPath.equals(rootPath)) { throw Error.EACCES.exception(targetPath); }
InMemoryDirectoryInfo sourceParent;
InMemoryDirectoryInfo targetParent;
synchronized (this) {
sourceParent = getDirectory(sourcePath.getParentDirectory());
targetParent = getDirectory(targetPath.getParentDirectory());
// Handle the rename if both paths are within our scope.
if (!sourceParent.outOfScope() && !targetParent.outOfScope()) {
InMemoryContentInfo sourceInode = sourceParent.getChild(sourcePath.getBaseName());
if (sourceInode == null) { throw Error.ENOENT.exception(sourcePath); }
InMemoryContentInfo targetInode = targetParent.getChild(targetPath.getBaseName());
unlink(sourceParent, sourcePath.getBaseName(), sourcePath);
try {
// TODO(bazel-team): (2009) test with symbolic links.
// Precondition checks:
if (targetInode != null) { // already exists
if (targetInode.isDirectory()) {
if (!sourceInode.isDirectory()) {
throw new IOException(sourcePath + " -> " + targetPath + " (" + Error.EISDIR + ")");
}
if (targetInode.getSize() > 2) {
throw Error.ENOTEMPTY.exception(targetPath);
}
} else if (sourceInode.isDirectory()) {
throw new IOException(sourcePath + " -> " + targetPath + " (" + Error.ENOTDIR + ")");
}
unlink(targetParent, targetPath.getBaseName(), targetPath);
}
sourceInode.movedTo(targetPath);
insert(targetParent, targetPath.getBaseName(), sourceInode, targetPath);
return;
} catch (IOException e) {
sourceInode.movedTo(sourcePath);
insert(sourceParent, sourcePath.getBaseName(), sourceInode, sourcePath); // restore source
throw e;
}
}
}
// If we get here, either one or both paths is out of scope.
if (sourceParent.outOfScope() && targetParent.outOfScope()) {
Path delegatedSource = getDelegatedPath(sourceParent.getEscapingPath(), sourcePath);
Path delegatedTarget = getDelegatedPath(targetParent.getEscapingPath(), targetPath);
delegatedSource.renameTo(delegatedTarget);
} else {
// We don't support cross-file system renaming.
throw Error.EACCES.exception(targetPath);
}
}
@Override
protected void createFSDependentHardLink(Path linkPath, Path originalPath)
throws IOException {
// Same check used when creating a symbolic link
if (originalPath.equals(rootPath)) {
throw Error.EACCES.exception(originalPath);
}
InMemoryDirectoryInfo linkParent;
synchronized (this) {
linkParent = getDirectory(linkPath.getParentDirectory());
// Same check used when creating a symbolic link
if (!linkParent.outOfScope()) {
if (linkParent.getChild(linkPath.getBaseName()) != null) {
throw Error.EEXIST.exception(linkPath);
}
insert(
linkParent,
linkPath.getBaseName(),
getDirectory(originalPath.getParentDirectory()).getChild(originalPath.getBaseName()),
linkPath);
return;
}
}
// If we get here, we're out of scope.
getDelegatedPath(linkParent.getEscapingPath(), originalPath).createHardLink(linkPath);
}
}