Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add factory for creating paths relative to well-known roots #15931

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Copyright 2022 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.runtime;

import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.lib.vfs.FileSystem;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.lib.vfs.Symlinks;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* Factory for creating {@link PathFragment}s from command-line options.
*
* <p>The difference between this and using {@link PathFragment#create(String)} directly is that
* this factory replaces values starting with {@code %<name>%} with the corresponding (named) roots
* (e.g., {@code %workspace%/foo} becomes {@code </path/to/workspace>/foo}).
*/
public final class CommandLinePathFactory {
private static final Pattern REPLACEMENT_PATTERN = Pattern.compile("^(%([a-z_]+)%/)?([^%].*)$");

private static final Splitter PATH_SPLITTER = Splitter.on(File.pathSeparator);

private final FileSystem fileSystem;
private final ImmutableMap<String, Path> roots;

public CommandLinePathFactory(FileSystem fileSystem, ImmutableMap<String, Path> roots) {
this.fileSystem = Preconditions.checkNotNull(fileSystem);
this.roots = Preconditions.checkNotNull(roots);
}

/** Creates a {@link Path}. */
public Path create(Map<String, String> env, String value) throws IOException {
Preconditions.checkNotNull(env);
Preconditions.checkNotNull(value);

Matcher matcher = REPLACEMENT_PATTERN.matcher(value);
Preconditions.checkArgument(matcher.matches());

String rootName = matcher.group(2);
PathFragment path = PathFragment.create(matcher.group(3));
if (path.containsUplevelReferences()) {
throw new IllegalArgumentException(
String.format(
Locale.US, "Path must not contain any uplevel references ('..'), got '%s'", value));
}

// Case 1: `path` is relative to a well-known root.
if (!Strings.isNullOrEmpty(rootName)) {
// The regex above cannot check that `value` is not of form `%foo%//abc` (group 2 will be
// `foo` and group 3 will be `/abc`).
Preconditions.checkArgument(!path.isAbsolute());

Path root = roots.get(rootName);
if (root == null) {
throw new IllegalArgumentException(String.format(Locale.US, "Unknown root %s", rootName));
}
return root.getRelative(path);
}

// Case 2: `value` is an absolute path.
if (path.isAbsolute()) {
return fileSystem.getPath(path);
}

// Case 3: `value` is a relative path.
//
// Since relative paths from the command-line are ambiguous to where they are relative to (i.e.,
// relative to the workspace?, the directory Bazel is running in? relative to the `.bazelrc` the
// flag is from?), we only allow relative paths with a single segment (i.e., no `/`) and treat
// it as relative to the user's `PATH`.
if (path.segmentCount() > 1) {
throw new IllegalArgumentException(
"Path must either be absolute or not contain any path separators");
}

String pathVariable = env.getOrDefault("PATH", "");
if (!Strings.isNullOrEmpty(pathVariable)) {
for (String lookupPath : PATH_SPLITTER.split(pathVariable)) {
Path maybePath = fileSystem.getPath(lookupPath).getRelative(path);
if (maybePath.exists(Symlinks.FOLLOW)
&& maybePath.isFile(Symlinks.FOLLOW)
&& maybePath.isExecutable()) {
return maybePath;
}
}
}

throw new FileNotFoundException(
String.format(
Locale.US, "Could not find file with name '%s' on PATH '%s'", path, pathVariable));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
// Copyright 2022 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.runtime;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;

import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.lib.vfs.DigestHashFunction;
import com.google.devtools.build.lib.vfs.FileSystem;
import com.google.devtools.build.lib.vfs.Path;
import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.OutputStream;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** Tests for {@link CommandLinePathFactory}. */
@RunWith(JUnit4.class)
public class CommandLinePathFactoryTest {
private static final Joiner PATH_JOINER = Joiner.on(File.pathSeparator);

private FileSystem filesystem = null;

@Before
public void prepareFilesystem() throws Exception {
filesystem = new InMemoryFileSystem(DigestHashFunction.SHA256);
}

private void createExecutable(String path) throws Exception {
Preconditions.checkNotNull(path);

createExecutable(filesystem.getPath(path));
}

private void createExecutable(Path path) throws Exception {
Preconditions.checkNotNull(path);

path.getParentDirectory().createDirectoryAndParents();
try (OutputStream stream = path.getOutputStream()) {
// Just create an empty file, nothing to do.
}
path.setExecutable(true);
}

@Test
public void emptyPathIsRejected() {
CommandLinePathFactory factory = new CommandLinePathFactory(filesystem, ImmutableMap.of());

assertThrows(IllegalArgumentException.class, () -> factory.create(ImmutableMap.of(), ""));
}

@Test
public void createFromAbsolutePath() throws Exception {
CommandLinePathFactory factory = new CommandLinePathFactory(filesystem, ImmutableMap.of());

assertThat(factory.create(ImmutableMap.of(), "/absolute/path/1"))
.isEqualTo(filesystem.getPath("/absolute/path/1"));
assertThat(factory.create(ImmutableMap.of(), "/absolute/path/2"))
.isEqualTo(filesystem.getPath("/absolute/path/2"));
}

@Test
public void createWithNamedRoot() throws Exception {
CommandLinePathFactory factory =
new CommandLinePathFactory(
filesystem,
ImmutableMap.of(
"workspace", filesystem.getPath("/path/to/workspace"),
"output_base", filesystem.getPath("/path/to/output/base")));

assertThat(factory.create(ImmutableMap.of(), "/absolute/path/1"))
.isEqualTo(filesystem.getPath("/absolute/path/1"));
assertThat(factory.create(ImmutableMap.of(), "/absolute/path/2"))
.isEqualTo(filesystem.getPath("/absolute/path/2"));

assertThat(factory.create(ImmutableMap.of(), "%workspace%/foo"))
.isEqualTo(filesystem.getPath("/path/to/workspace/foo"));
assertThat(factory.create(ImmutableMap.of(), "%workspace%/foo/bar"))
.isEqualTo(filesystem.getPath("/path/to/workspace/foo/bar"));

assertThat(factory.create(ImmutableMap.of(), "%output_base%/foo"))
.isEqualTo(filesystem.getPath("/path/to/output/base/foo"));
assertThat(factory.create(ImmutableMap.of(), "%output_base%/foo/bar"))
.isEqualTo(filesystem.getPath("/path/to/output/base/foo/bar"));
}

@Test
public void pathLeakingOutsideOfRoot() {
CommandLinePathFactory factory =
new CommandLinePathFactory(
filesystem, ImmutableMap.of("a", filesystem.getPath("/path/to/a")));

assertThrows(
IllegalArgumentException.class, () -> factory.create(ImmutableMap.of(), "%a%/../foo"));
assertThrows(
IllegalArgumentException.class, () -> factory.create(ImmutableMap.of(), "%a%/b/../.."));
}

@Test
public void unknownRoot() {
CommandLinePathFactory factory =
new CommandLinePathFactory(
filesystem, ImmutableMap.of("a", filesystem.getPath("/path/to/a")));

assertThrows(
IllegalArgumentException.class, () -> factory.create(ImmutableMap.of(), "%workspace%/foo"));
assertThrows(
IllegalArgumentException.class,
() -> factory.create(ImmutableMap.of(), "%output_base%/foo"));
}

@Test
public void rootWithDoubleSlash() {
CommandLinePathFactory factory =
new CommandLinePathFactory(
filesystem, ImmutableMap.of("a", filesystem.getPath("/path/to/a")));

assertThrows(
IllegalArgumentException.class, () -> factory.create(ImmutableMap.of(), "%a%//foo"));
}

@Test
public void relativePathWithMultipleSegments() {
CommandLinePathFactory factory = new CommandLinePathFactory(filesystem, ImmutableMap.of());

assertThrows(IllegalArgumentException.class, () -> factory.create(ImmutableMap.of(), "a/b"));
assertThrows(
IllegalArgumentException.class, () -> factory.create(ImmutableMap.of(), "a/b/c/d"));
}

@Test
public void pathLookup() throws Exception {
CommandLinePathFactory factory = new CommandLinePathFactory(filesystem, ImmutableMap.of());

createExecutable("/bin/true");
createExecutable("/bin/false");
createExecutable("/usr/bin/foo-bar.exe");
createExecutable("/usr/local/bin/baz");
createExecutable("/home/yannic/bin/abc");
createExecutable("/home/yannic/bin/true");

ImmutableMap<String, String> env =
ImmutableMap.of(
"PATH", PATH_JOINER.join("/bin", "/usr/bin", "/usr/local/bin", "/home/yannic/bin"));
assertThat(factory.create(env, "true")).isEqualTo(filesystem.getPath("/bin/true"));
assertThat(factory.create(env, "false")).isEqualTo(filesystem.getPath("/bin/false"));
assertThat(factory.create(env, "foo-bar.exe"))
.isEqualTo(filesystem.getPath("/usr/bin/foo-bar.exe"));
assertThat(factory.create(env, "baz")).isEqualTo(filesystem.getPath("/usr/local/bin/baz"));
assertThat(factory.create(env, "abc")).isEqualTo(filesystem.getPath("/home/yannic/bin/abc"));

// `.exe` is required.
assertThrows(FileNotFoundException.class, () -> factory.create(env, "foo-bar"));
}

@Test
public void pathLookupWithUndefinedPath() {
CommandLinePathFactory factory = new CommandLinePathFactory(filesystem, ImmutableMap.of());

assertThrows(FileNotFoundException.class, () -> factory.create(ImmutableMap.of(), "a"));
assertThrows(FileNotFoundException.class, () -> factory.create(ImmutableMap.of(), "foo"));
}

@Test
public void pathLookupWithNonExistingDirectoryOnPath() {
CommandLinePathFactory factory = new CommandLinePathFactory(filesystem, ImmutableMap.of());

assertThrows(
FileNotFoundException.class,
() -> factory.create(ImmutableMap.of("PATH", "/does/not/exist"), "a"));
}

@Test
public void pathLookupWithExistingAndNonExistingDirectoryOnPath() throws Exception {
CommandLinePathFactory factory = new CommandLinePathFactory(filesystem, ImmutableMap.of());

createExecutable("/bin/foo");
createExecutable("/usr/bin/bar");
assertThrows(
FileNotFoundException.class,
() ->
factory.create(
ImmutableMap.of("PATH", PATH_JOINER.join("/bin", "/does/not/exist", "/usr/bin")),
"a"));
}
}