| // 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); |
| } |
| } |