// 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.util.Clock; import com.google.devtools.build.lib.util.JavaClock; import com.google.devtools.build.lib.util.OS; import com.google.devtools.build.lib.util.Preconditions; import com.google.devtools.build.lib.vfs.FileAccessException; 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.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 OS.getCurrent() != OS.WINDOWS; } /** * 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"); } Collection<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); } }