| // Copyright 2024 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.remote; | |
| import static com.google.common.truth.Truth.assertThat; | |
| import static java.nio.charset.StandardCharsets.UTF_8; | |
| import static org.junit.Assert.assertThrows; | |
| import static org.junit.Assume.assumeTrue; | |
| import com.google.devtools.build.lib.util.OS; | |
| import com.google.devtools.build.lib.vfs.DigestHashFunction; | |
| import com.google.devtools.build.lib.vfs.FileSymlinkLoopException; | |
| import com.google.devtools.build.lib.vfs.FileSystem; | |
| import com.google.devtools.build.lib.vfs.FileSystem.NotASymlinkException; | |
| import com.google.devtools.build.lib.vfs.FileSystemUtils; | |
| import com.google.devtools.build.lib.vfs.Path; | |
| import com.google.devtools.build.lib.vfs.PathFragment; | |
| import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem; | |
| import java.io.FileNotFoundException; | |
| import java.io.IOException; | |
| import org.checkerframework.checker.nullness.qual.Nullable; | |
| import org.junit.Test; | |
| import org.junit.runner.RunWith; | |
| import org.junit.runners.JUnit4; | |
| /** Tests for {@link PathCanonicalizer}. */ | |
| @RunWith(JUnit4.class) | |
| public final class PathCanonicalizerTest { | |
| // Test outline: | |
| // 1. Set up the filesystem state by calling createSymlink, createNonSymlink or deleteTree. | |
| // 2. Call assertSuccess or assertFailure to check for successful resolution or failure. | |
| // On Windows, absolute paths start with a drive letter, e.g. C:/, instead of / as in Unix. | |
| // To avoid test duplication, when the tests run on Windows, Unix-style absolute paths passed to | |
| // the above methods will have a C: automatically prepended to them. | |
| private final FileSystem fs = new InMemoryFileSystem(DigestHashFunction.SHA256); | |
| private final PathCanonicalizer canonicalizer = new PathCanonicalizer(this::resolve); | |
| private @Nullable PathFragment resolve(PathFragment pathFragment) throws IOException { | |
| Path path = fs.getPath(pathFragment); | |
| try { | |
| return path.readSymbolicLink(); | |
| } catch (NotASymlinkException e) { | |
| return null; | |
| } | |
| } | |
| @Test | |
| public void testRoot() throws Exception { | |
| assertSuccess("/", "/"); | |
| } | |
| @Test | |
| public void testAlreadyCanonical() throws Exception { | |
| createNonSymlink("/a/b"); | |
| assertSuccess("/a/b", "/a/b"); | |
| } | |
| @Test | |
| public void testAbsoluteSymlinkToFile() throws Exception { | |
| createSymlink("/a/b", "/c/d"); | |
| createNonSymlink("/c/d"); | |
| assertSuccess("/a/b", "/c/d"); | |
| } | |
| @Test | |
| public void testAbsoluteSymlinkToDirectory() throws Exception { | |
| createSymlink("/a/b", "/d/e"); | |
| createNonSymlink("/d/e/c"); | |
| assertSuccess("/a/b/c", "/d/e/c"); | |
| } | |
| @Test | |
| public void testAbsoluteSymlinkToDifferentDrive() throws Exception { | |
| assumeTrue(OS.getCurrent() == OS.WINDOWS); | |
| createSymlink("C:/a/b", "D:/e/f"); | |
| createNonSymlink("D:/e/f"); | |
| assertSuccess("C:/a/b/c/d", "D:/e/f/c/d"); | |
| } | |
| @Test | |
| public void testRelativeSymlinkToFileInSameDirectory() throws Exception { | |
| createSymlink("/a/b", "c"); | |
| createNonSymlink("/a/c"); | |
| assertSuccess("/a/b", "/a/c"); | |
| } | |
| @Test | |
| public void testRelativeSymlinkToFileInDirectoryBelow() throws Exception { | |
| createSymlink("/a/b", "c/d"); | |
| createNonSymlink("/a/c/d"); | |
| assertSuccess("/a/b", "/a/c/d"); | |
| } | |
| @Test | |
| public void testRelativeSymlinkToFileInDirectoryAbove() throws Exception { | |
| createSymlink("/a/b/c", "../d/e"); | |
| createNonSymlink("/a/d/e"); | |
| assertSuccess("/a/b/c", "/a/d/e"); | |
| } | |
| @Test | |
| public void testRelativeSymlinkToRoot() throws Exception { | |
| createSymlink("/a/b/c", "../../d"); | |
| createNonSymlink("/d"); | |
| assertSuccess("/a/b/c", "/d"); | |
| } | |
| @Test | |
| public void testRelativeSymlinkWithTooManyUplevelReferences() throws Exception { | |
| createSymlink("/a/b", "../../d"); | |
| createNonSymlink("/d/c"); | |
| assertSuccess("/a/b/c", "/d/c"); | |
| } | |
| @Test | |
| public void testMultipleSymlinks() throws Exception { | |
| createSymlink("/a", "/b"); | |
| createSymlink("/b/c", "/d"); | |
| createSymlink("/d/e", "/f"); | |
| createNonSymlink("/f"); | |
| assertSuccess("/a/c/e", "/f"); | |
| } | |
| @Test | |
| public void testReplayCanonical() throws Exception { | |
| createNonSymlink("/a/b/c"); | |
| assertSuccess("/a/b/c", "/a/b/c"); | |
| assertSuccess("/a/b/c", "/a/b/c"); | |
| } | |
| @Test | |
| public void testReplaySymlink() throws Exception { | |
| createSymlink("/a/b", "/d"); | |
| createNonSymlink("/d/c"); | |
| assertSuccess("/a/b/c", "/d/c"); | |
| assertSuccess("/a/b/c", "/d/c"); | |
| } | |
| @Test | |
| public void testDistinguishPathsWithCommonPrefix() throws Exception { | |
| createSymlink("/a/b", "/d"); | |
| createNonSymlink("/d/c"); | |
| createNonSymlink("/a/e"); | |
| assertSuccess("/a/b/c", "/d/c"); | |
| assertSuccess("/a/e", "/a/e"); | |
| } | |
| @Test | |
| public void testDistinguishPathsWithDifferentDriveLetter() throws Exception { | |
| assumeTrue(OS.getCurrent() == OS.WINDOWS); | |
| createSymlink("C:/a/b", "D:/d"); | |
| createNonSymlink("D:/d/c"); | |
| createNonSymlink("C:/a/b/c"); | |
| assertSuccess("C:/a/b/c", "D:/d/c"); | |
| assertSuccess("D:/a/b/c", "D:/a/b/c"); | |
| } | |
| @Test | |
| public void testClearAndReplaceWithSymlink() throws Exception { | |
| createNonSymlink("/a/b/c"); | |
| assertSuccess("/a/b/c", "/a/b/c"); | |
| deleteTree("/a/b"); | |
| createSymlink("/a/b", "/d"); | |
| createNonSymlink("/d/c"); | |
| assertSuccess("/a/b/c", "/d/c"); | |
| } | |
| @Test | |
| public void testClearAndReplaceWithNonSymlink() throws Exception { | |
| createSymlink("/a/b", "/d"); | |
| createNonSymlink("/d/c"); | |
| assertSuccess("/a/b/c", "/d/c"); | |
| deleteTree("/a/b"); | |
| createNonSymlink("/a/b/c"); | |
| assertSuccess("/a/b/c", "/a/b/c"); | |
| } | |
| @Test | |
| public void testClearSymlinkAndDoNotReplace() throws Exception { | |
| createSymlink("/a/b", "/d"); | |
| createNonSymlink("/d/c"); | |
| assertSuccess("/a/b/c", "/d/c"); | |
| deleteTree("/a/b"); | |
| assertFailure(FileNotFoundException.class, "/a/b/c"); | |
| } | |
| @Test | |
| public void testClearNonSymlinkAndDoNotReplace() throws Exception { | |
| createNonSymlink("/a/b/c"); | |
| assertSuccess("/a/b/c", "/a/b/c"); | |
| deleteTree("/a/b"); | |
| assertFailure(FileNotFoundException.class, "/a/b/c"); | |
| } | |
| @Test | |
| public void testSymlinkSelfLoop() throws Exception { | |
| createSymlink("/a/b", "/a/b"); | |
| assertFailure(FileSymlinkLoopException.class, "/a/b"); | |
| } | |
| @Test | |
| public void testSymlinkMutualLoop() throws Exception { | |
| createSymlink("/a/b", "/c/d"); | |
| createSymlink("/c/d", "/a/b"); | |
| assertFailure(FileSymlinkLoopException.class, "/a/b"); | |
| } | |
| @Test | |
| public void testSymlinkChainTooLong() throws Exception { | |
| for (int i = 0; i < FileSystem.MAX_SYMLINKS + 1; i++) { | |
| createSymlink(String.format("/%s", i), String.format("/%s", i + 1)); | |
| } | |
| assertFailure(FileSymlinkLoopException.class, "/0"); | |
| } | |
| @Test | |
| public void testFileNotFound() throws Exception { | |
| assertFailure(FileNotFoundException.class, "/a/b"); | |
| createNonSymlink("/a/b"); | |
| assertSuccess("/a/b", "/a/b"); | |
| } | |
| @Test | |
| public void testEmpty() throws Exception { | |
| assertFailure(IllegalArgumentException.class, ""); | |
| } | |
| @Test | |
| public void testNonAbsolute() throws Exception { | |
| assertFailure(IllegalArgumentException.class, "a/b"); | |
| } | |
| private void createSymlink(String linkPathStr, String targetPathStr) throws Exception { | |
| Path linkPath = fs.getPath(pathFragment(linkPathStr)); | |
| linkPath.getParentDirectory().createDirectoryAndParents(); | |
| linkPath.createSymbolicLink(pathFragment(targetPathStr)); | |
| } | |
| private void createNonSymlink(String pathStr) throws Exception { | |
| Path path = fs.getPath(pathFragment(pathStr)); | |
| path.getParentDirectory().createDirectoryAndParents(); | |
| FileSystemUtils.writeContent(path, UTF_8, ""); | |
| } | |
| private void deleteTree(String pathStr) throws Exception { | |
| canonicalizer.clearPrefix(pathFragment(pathStr)); | |
| fs.getPath(pathFragment(pathStr)).deleteTree(); | |
| } | |
| private void assertSuccess(String input, String output) throws Exception { | |
| assertThat(canonicalizer.resolveSymbolicLinks(pathFragment(input))) | |
| .isEqualTo(pathFragment(output)); | |
| } | |
| private <T extends Throwable> void assertFailure(Class<T> exceptionClass, String input) | |
| throws Exception { | |
| assertThrows(exceptionClass, () -> canonicalizer.resolveSymbolicLinks(pathFragment(input))); | |
| } | |
| private static PathFragment pathFragment(String pathStr) { | |
| if (pathStr.startsWith("/") && OS.getCurrent() == OS.WINDOWS) { | |
| pathStr = "C:" + pathStr; | |
| } | |
| return PathFragment.create(pathStr); | |
| } | |
| } |