Skip to content

Commit 6f73205

Browse files
authored
Add factory for creating paths relative to well-known roots (bazelbuild#15931)
* Add factory for creating paths relative to well-known roots This change adds a factory for creating `PathFragments` relative to pre-defined (named) roots (e.g., relative to `%workspace%`). The syntax is choosen to match existing ad-hoc solutions for `%workspace%`, or `%builtins%` in other places (so that we can ideally migrate them in a follow-up). We'll use this for parsing paths from the command-line (e.g., `--credential_helper=%workspace%/foo`). Progress on https://github.com/bazelbuild/proposals/blob/main/designs/2022-06-07-bazel-credential-helpers.md Closes bazelbuild#15805. PiperOrigin-RevId: 460950483 Change-Id: Ie263fb6d6c2ea938a850a72793d551135df6862e * Remove use of var
1 parent ae386f3 commit 6f73205

File tree

2 files changed

+316
-0
lines changed

2 files changed

+316
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// Copyright 2022 The Bazel Authors. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
package com.google.devtools.build.lib.runtime;
15+
16+
import com.google.common.base.Preconditions;
17+
import com.google.common.base.Splitter;
18+
import com.google.common.base.Strings;
19+
import com.google.common.collect.ImmutableMap;
20+
import com.google.devtools.build.lib.vfs.FileSystem;
21+
import com.google.devtools.build.lib.vfs.Path;
22+
import com.google.devtools.build.lib.vfs.PathFragment;
23+
import com.google.devtools.build.lib.vfs.Symlinks;
24+
import java.io.File;
25+
import java.io.FileNotFoundException;
26+
import java.io.IOException;
27+
import java.util.Locale;
28+
import java.util.Map;
29+
import java.util.regex.Matcher;
30+
import java.util.regex.Pattern;
31+
32+
/**
33+
* Factory for creating {@link PathFragment}s from command-line options.
34+
*
35+
* <p>The difference between this and using {@link PathFragment#create(String)} directly is that
36+
* this factory replaces values starting with {@code %<name>%} with the corresponding (named) roots
37+
* (e.g., {@code %workspace%/foo} becomes {@code </path/to/workspace>/foo}).
38+
*/
39+
public final class CommandLinePathFactory {
40+
private static final Pattern REPLACEMENT_PATTERN = Pattern.compile("^(%([a-z_]+)%/)?([^%].*)$");
41+
42+
private static final Splitter PATH_SPLITTER = Splitter.on(File.pathSeparator);
43+
44+
private final FileSystem fileSystem;
45+
private final ImmutableMap<String, Path> roots;
46+
47+
public CommandLinePathFactory(FileSystem fileSystem, ImmutableMap<String, Path> roots) {
48+
this.fileSystem = Preconditions.checkNotNull(fileSystem);
49+
this.roots = Preconditions.checkNotNull(roots);
50+
}
51+
52+
/** Creates a {@link Path}. */
53+
public Path create(Map<String, String> env, String value) throws IOException {
54+
Preconditions.checkNotNull(env);
55+
Preconditions.checkNotNull(value);
56+
57+
Matcher matcher = REPLACEMENT_PATTERN.matcher(value);
58+
Preconditions.checkArgument(matcher.matches());
59+
60+
String rootName = matcher.group(2);
61+
PathFragment path = PathFragment.create(matcher.group(3));
62+
if (path.containsUplevelReferences()) {
63+
throw new IllegalArgumentException(
64+
String.format(
65+
Locale.US, "Path must not contain any uplevel references ('..'), got '%s'", value));
66+
}
67+
68+
// Case 1: `path` is relative to a well-known root.
69+
if (!Strings.isNullOrEmpty(rootName)) {
70+
// The regex above cannot check that `value` is not of form `%foo%//abc` (group 2 will be
71+
// `foo` and group 3 will be `/abc`).
72+
Preconditions.checkArgument(!path.isAbsolute());
73+
74+
Path root = roots.get(rootName);
75+
if (root == null) {
76+
throw new IllegalArgumentException(String.format(Locale.US, "Unknown root %s", rootName));
77+
}
78+
return root.getRelative(path);
79+
}
80+
81+
// Case 2: `value` is an absolute path.
82+
if (path.isAbsolute()) {
83+
return fileSystem.getPath(path);
84+
}
85+
86+
// Case 3: `value` is a relative path.
87+
//
88+
// Since relative paths from the command-line are ambiguous to where they are relative to (i.e.,
89+
// relative to the workspace?, the directory Bazel is running in? relative to the `.bazelrc` the
90+
// flag is from?), we only allow relative paths with a single segment (i.e., no `/`) and treat
91+
// it as relative to the user's `PATH`.
92+
if (path.segmentCount() > 1) {
93+
throw new IllegalArgumentException(
94+
"Path must either be absolute or not contain any path separators");
95+
}
96+
97+
String pathVariable = env.getOrDefault("PATH", "");
98+
if (!Strings.isNullOrEmpty(pathVariable)) {
99+
for (String lookupPath : PATH_SPLITTER.split(pathVariable)) {
100+
Path maybePath = fileSystem.getPath(lookupPath).getRelative(path);
101+
if (maybePath.exists(Symlinks.FOLLOW)
102+
&& maybePath.isFile(Symlinks.FOLLOW)
103+
&& maybePath.isExecutable()) {
104+
return maybePath;
105+
}
106+
}
107+
}
108+
109+
throw new FileNotFoundException(
110+
String.format(
111+
Locale.US, "Could not find file with name '%s' on PATH '%s'", path, pathVariable));
112+
}
113+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
// Copyright 2022 The Bazel Authors. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
package com.google.devtools.build.lib.runtime;
15+
16+
import static com.google.common.truth.Truth.assertThat;
17+
import static org.junit.Assert.assertThrows;
18+
19+
import com.google.common.base.Joiner;
20+
import com.google.common.base.Preconditions;
21+
import com.google.common.collect.ImmutableMap;
22+
import com.google.devtools.build.lib.vfs.DigestHashFunction;
23+
import com.google.devtools.build.lib.vfs.FileSystem;
24+
import com.google.devtools.build.lib.vfs.Path;
25+
import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
26+
import java.io.File;
27+
import java.io.FileNotFoundException;
28+
import java.io.OutputStream;
29+
import org.junit.Before;
30+
import org.junit.Test;
31+
import org.junit.runner.RunWith;
32+
import org.junit.runners.JUnit4;
33+
34+
/** Tests for {@link CommandLinePathFactory}. */
35+
@RunWith(JUnit4.class)
36+
public class CommandLinePathFactoryTest {
37+
private static final Joiner PATH_JOINER = Joiner.on(File.pathSeparator);
38+
39+
private FileSystem filesystem = null;
40+
41+
@Before
42+
public void prepareFilesystem() throws Exception {
43+
filesystem = new InMemoryFileSystem(DigestHashFunction.SHA256);
44+
}
45+
46+
private void createExecutable(String path) throws Exception {
47+
Preconditions.checkNotNull(path);
48+
49+
createExecutable(filesystem.getPath(path));
50+
}
51+
52+
private void createExecutable(Path path) throws Exception {
53+
Preconditions.checkNotNull(path);
54+
55+
path.getParentDirectory().createDirectoryAndParents();
56+
try (OutputStream stream = path.getOutputStream()) {
57+
// Just create an empty file, nothing to do.
58+
}
59+
path.setExecutable(true);
60+
}
61+
62+
@Test
63+
public void emptyPathIsRejected() {
64+
CommandLinePathFactory factory = new CommandLinePathFactory(filesystem, ImmutableMap.of());
65+
66+
assertThrows(IllegalArgumentException.class, () -> factory.create(ImmutableMap.of(), ""));
67+
}
68+
69+
@Test
70+
public void createFromAbsolutePath() throws Exception {
71+
CommandLinePathFactory factory = new CommandLinePathFactory(filesystem, ImmutableMap.of());
72+
73+
assertThat(factory.create(ImmutableMap.of(), "/absolute/path/1"))
74+
.isEqualTo(filesystem.getPath("/absolute/path/1"));
75+
assertThat(factory.create(ImmutableMap.of(), "/absolute/path/2"))
76+
.isEqualTo(filesystem.getPath("/absolute/path/2"));
77+
}
78+
79+
@Test
80+
public void createWithNamedRoot() throws Exception {
81+
CommandLinePathFactory factory =
82+
new CommandLinePathFactory(
83+
filesystem,
84+
ImmutableMap.of(
85+
"workspace", filesystem.getPath("/path/to/workspace"),
86+
"output_base", filesystem.getPath("/path/to/output/base")));
87+
88+
assertThat(factory.create(ImmutableMap.of(), "/absolute/path/1"))
89+
.isEqualTo(filesystem.getPath("/absolute/path/1"));
90+
assertThat(factory.create(ImmutableMap.of(), "/absolute/path/2"))
91+
.isEqualTo(filesystem.getPath("/absolute/path/2"));
92+
93+
assertThat(factory.create(ImmutableMap.of(), "%workspace%/foo"))
94+
.isEqualTo(filesystem.getPath("/path/to/workspace/foo"));
95+
assertThat(factory.create(ImmutableMap.of(), "%workspace%/foo/bar"))
96+
.isEqualTo(filesystem.getPath("/path/to/workspace/foo/bar"));
97+
98+
assertThat(factory.create(ImmutableMap.of(), "%output_base%/foo"))
99+
.isEqualTo(filesystem.getPath("/path/to/output/base/foo"));
100+
assertThat(factory.create(ImmutableMap.of(), "%output_base%/foo/bar"))
101+
.isEqualTo(filesystem.getPath("/path/to/output/base/foo/bar"));
102+
}
103+
104+
@Test
105+
public void pathLeakingOutsideOfRoot() {
106+
CommandLinePathFactory factory =
107+
new CommandLinePathFactory(
108+
filesystem, ImmutableMap.of("a", filesystem.getPath("/path/to/a")));
109+
110+
assertThrows(
111+
IllegalArgumentException.class, () -> factory.create(ImmutableMap.of(), "%a%/../foo"));
112+
assertThrows(
113+
IllegalArgumentException.class, () -> factory.create(ImmutableMap.of(), "%a%/b/../.."));
114+
}
115+
116+
@Test
117+
public void unknownRoot() {
118+
CommandLinePathFactory factory =
119+
new CommandLinePathFactory(
120+
filesystem, ImmutableMap.of("a", filesystem.getPath("/path/to/a")));
121+
122+
assertThrows(
123+
IllegalArgumentException.class, () -> factory.create(ImmutableMap.of(), "%workspace%/foo"));
124+
assertThrows(
125+
IllegalArgumentException.class,
126+
() -> factory.create(ImmutableMap.of(), "%output_base%/foo"));
127+
}
128+
129+
@Test
130+
public void rootWithDoubleSlash() {
131+
CommandLinePathFactory factory =
132+
new CommandLinePathFactory(
133+
filesystem, ImmutableMap.of("a", filesystem.getPath("/path/to/a")));
134+
135+
assertThrows(
136+
IllegalArgumentException.class, () -> factory.create(ImmutableMap.of(), "%a%//foo"));
137+
}
138+
139+
@Test
140+
public void relativePathWithMultipleSegments() {
141+
CommandLinePathFactory factory = new CommandLinePathFactory(filesystem, ImmutableMap.of());
142+
143+
assertThrows(IllegalArgumentException.class, () -> factory.create(ImmutableMap.of(), "a/b"));
144+
assertThrows(
145+
IllegalArgumentException.class, () -> factory.create(ImmutableMap.of(), "a/b/c/d"));
146+
}
147+
148+
@Test
149+
public void pathLookup() throws Exception {
150+
CommandLinePathFactory factory = new CommandLinePathFactory(filesystem, ImmutableMap.of());
151+
152+
createExecutable("/bin/true");
153+
createExecutable("/bin/false");
154+
createExecutable("/usr/bin/foo-bar.exe");
155+
createExecutable("/usr/local/bin/baz");
156+
createExecutable("/home/yannic/bin/abc");
157+
createExecutable("/home/yannic/bin/true");
158+
159+
ImmutableMap<String, String> env =
160+
ImmutableMap.of(
161+
"PATH", PATH_JOINER.join("/bin", "/usr/bin", "/usr/local/bin", "/home/yannic/bin"));
162+
assertThat(factory.create(env, "true")).isEqualTo(filesystem.getPath("/bin/true"));
163+
assertThat(factory.create(env, "false")).isEqualTo(filesystem.getPath("/bin/false"));
164+
assertThat(factory.create(env, "foo-bar.exe"))
165+
.isEqualTo(filesystem.getPath("/usr/bin/foo-bar.exe"));
166+
assertThat(factory.create(env, "baz")).isEqualTo(filesystem.getPath("/usr/local/bin/baz"));
167+
assertThat(factory.create(env, "abc")).isEqualTo(filesystem.getPath("/home/yannic/bin/abc"));
168+
169+
// `.exe` is required.
170+
assertThrows(FileNotFoundException.class, () -> factory.create(env, "foo-bar"));
171+
}
172+
173+
@Test
174+
public void pathLookupWithUndefinedPath() {
175+
CommandLinePathFactory factory = new CommandLinePathFactory(filesystem, ImmutableMap.of());
176+
177+
assertThrows(FileNotFoundException.class, () -> factory.create(ImmutableMap.of(), "a"));
178+
assertThrows(FileNotFoundException.class, () -> factory.create(ImmutableMap.of(), "foo"));
179+
}
180+
181+
@Test
182+
public void pathLookupWithNonExistingDirectoryOnPath() {
183+
CommandLinePathFactory factory = new CommandLinePathFactory(filesystem, ImmutableMap.of());
184+
185+
assertThrows(
186+
FileNotFoundException.class,
187+
() -> factory.create(ImmutableMap.of("PATH", "/does/not/exist"), "a"));
188+
}
189+
190+
@Test
191+
public void pathLookupWithExistingAndNonExistingDirectoryOnPath() throws Exception {
192+
CommandLinePathFactory factory = new CommandLinePathFactory(filesystem, ImmutableMap.of());
193+
194+
createExecutable("/bin/foo");
195+
createExecutable("/usr/bin/bar");
196+
assertThrows(
197+
FileNotFoundException.class,
198+
() ->
199+
factory.create(
200+
ImmutableMap.of("PATH", PATH_JOINER.join("/bin", "/does/not/exist", "/usr/bin")),
201+
"a"));
202+
}
203+
}

0 commit comments

Comments
 (0)