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