| // 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; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import com.google.devtools.build.lib.concurrent.ThreadSafety.ThreadHostile; |
| import com.google.devtools.build.lib.util.Preconditions; |
| |
| import java.io.IOException; |
| |
| /** |
| * A file system that's capable of identifying paths residing outside its scope |
| * and using a delegator (such as {@link UnionFileSystem}) to re-route them |
| * to appropriate alternative file systems. |
| * |
| * <p>This is most useful for symlinks, which may ostensibly fall beneath some |
| * file system but resolve to paths outside that file system. |
| * |
| * <p>Note that we don't protect against cross-filesystem circular references. |
| * Therefore, care should be taken not to mix two scopable file systems that |
| * can reference each other. This theoretical safety cost is balanced by |
| * decreased code complexity requirements in implementations. |
| */ |
| public abstract class ScopeEscapableFileSystem extends FileSystem { |
| |
| private FileSystem delegator; |
| protected final PathFragment scopeRoot; |
| private boolean enableScopeChecking = true; // Used for testing. |
| |
| /** |
| * Instantiates a new ScopeEscapableFileSystem. |
| * |
| * @param scopeRoot the root path for the file system's scope. Any path |
| * that isn't beneath this one is considered out of scope according |
| * to {@link #inScope}. If null, scope checking is disabled. Note |
| * this is not the same thing as {@link FileSystem#rootPath}, which |
| * generally resolves to "/". |
| */ |
| protected ScopeEscapableFileSystem(PathFragment scopeRoot) { |
| this.scopeRoot = scopeRoot; |
| } |
| |
| @VisibleForTesting |
| void enableScopeChecking(boolean enable) { |
| this.enableScopeChecking = enable; |
| } |
| |
| /** |
| * Sets the delegator used to resolve paths that fall outside this file |
| * system's scope. |
| * |
| * <p>This method is not thread safe. It's intended to be called during |
| * instance initialization, not during active usage. The only reason this |
| * isn't set as immutable state within the constructor is that the delegator |
| * may need a reference to this instance for its own constructor. |
| */ |
| @ThreadHostile |
| public void setDelegator(FileSystem delegator) { |
| this.delegator = delegator; |
| } |
| |
| /** |
| * Uses the delegator to convert a path fragment to a path that's bound |
| * to the file system that manages that path. |
| */ |
| protected Path getDelegatedPath(PathFragment path) { |
| Preconditions.checkState(delegator != null); |
| return delegator.getPath(path); |
| } |
| |
| /** |
| * Proxy for {@link FileSystem#resolveOneLink} that sends the input path |
| * through the delegator. |
| */ |
| protected PathFragment resolveOneLinkWithDelegator(final PathFragment path) throws IOException { |
| Preconditions.checkState(delegator != null); |
| return delegator.resolveOneLink(getDelegatedPath(path)); |
| } |
| |
| /** |
| * Proxy for {@link FileSystem#stat} that sends the input path through |
| * the delegator. |
| */ |
| protected FileStatus statWithDelegator(final PathFragment path, final boolean followSymlinks) |
| throws IOException { |
| Preconditions.checkState(delegator != null); |
| return delegator.stat(getDelegatedPath(path), followSymlinks); |
| } |
| |
| /** |
| * Returns true if the given path is within this file system's scope, false |
| * otherwise. |
| * |
| * @param parentDepth the number of segments in the path's parent directory |
| * (only meaningful for paths that begin with ".."). The parent directory |
| * itself is assumed to be in scope. |
| * @param normalizedPath input path, expected to be normalized such that all |
| * ".." and "." segments are removed (with the exception of a possible |
| * prefix sequence of contiguous ".." segments) |
| */ |
| protected boolean inScope(int parentDepth, PathFragment normalizedPath) { |
| if (scopeRoot == null || !enableScopeChecking) { |
| return true; |
| } else if (normalizedPath.isAbsolute()) { |
| return normalizedPath.startsWith(scopeRoot); |
| } else { |
| // Efficiency note: we're not accounting for "/scope/root/../root" paths here, i.e. paths |
| // that appear to go out of scope but ultimately stay within scope. This may result in |
| // unnecessary re-delegation back into the same FS. we're choosing to forgo that |
| // optimization under the assumption that such scenarios are rare and unimportant to |
| // overall performance. We can always enhance this if needed. |
| return parentDepth - leadingParentReferences(normalizedPath) >= scopeRoot.segmentCount(); |
| } |
| } |
| |
| /** |
| * Given a path that's normalized (no ".." or "." segments), except for a possible |
| * prefix sequence of contiguous ".." segments, returns the size of that prefix |
| * sequence. |
| * |
| * <p>Example allowed inputs: "/absolute/path", "relative/path", "../../relative/path". |
| * Example disallowed inputs: "/absolute/path/../path2", "relative/../path", "../relative/../p". |
| */ |
| protected int leadingParentReferences(PathFragment normalizedPath) { |
| int leadingParentReferences = 0; |
| for (int i = 0; i < normalizedPath.segmentCount() && |
| normalizedPath.getSegment(i).equals(".."); i++) { |
| leadingParentReferences++; |
| } |
| return leadingParentReferences; |
| } |
| } |