blob: c3634d7bf622e0ae0211ad4801db4314bb801387 [file] [log] [blame]
// 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;
}
}