aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileSystem.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileSystem.java')
-rw-r--r--src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileSystem.java920
1 files changed, 920 insertions, 0 deletions
diff --git a/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileSystem.java b/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileSystem.java
new file mode 100644
index 0000000000..8a3b823cdd
--- /dev/null
+++ b/src/main/java/com/google/devtools/build/lib/vfs/inmemoryfs/InMemoryFileSystem.java
@@ -0,0 +1,920 @@
+// Copyright 2014 Google Inc. 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.common.base.Preconditions;
+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.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 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 supportsSymbolicLinks() {
+ 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);
+ }
+ }
+}