// Copyright 2015 Google Inc. 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.sandbox; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableSetMultimap; import com.google.common.collect.Iterables; import com.google.common.collect.LinkedHashMultimap; import com.google.common.collect.Multimap; import com.google.devtools.build.lib.testutil.TestUtils; import com.google.devtools.build.lib.vfs.FileSystem; 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.UnixFileSystem; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import java.io.File; import java.io.IOException; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; import java.util.Map.Entry; /** * Tests for {@code LinuxSandboxedStrategy}. * *

The general idea for each test is to provide a file tree consisting of symlinks, directories * and empty files and then handing that together with an arbitrary number of input files (what * would be specified in the "srcs" attribute, for example) to the LinuxSandboxedStrategy. * *

The algorithm that processes the mounts must then always find (and thus mount) the expected * tree of files given only the set of input files. */ @RunWith(JUnit4.class) public class LinuxSandboxedStrategyTest { private FileSystem testFS; private Path workingDir; private Path fakeSandboxDir; @Before public void setUp() throws Exception { testFS = new UnixFileSystem(); workingDir = testFS.getPath(new File(TestUtils.tmpDir()).getCanonicalPath()); fakeSandboxDir = workingDir.getRelative("sandbox"); fakeSandboxDir.createDirectory(); } @After public void tearDown() throws Exception { FileSystemUtils.deleteTreesBelow(workingDir); } private Path getSandboxPath(Path entry) { return fakeSandboxDir.getRelative(entry.asFragment().relativeTo("/")); } /** * Strips the working directory (which can be very long) from the file names in the input map, to * make assertion failures easier to read. */ private ImmutableSetMultimap userFriendlyMap(Multimap input) { ImmutableSetMultimap.Builder userFriendlyMap = ImmutableSetMultimap.builder(); for (Entry entry : input.entries()) { String key = entry.getKey().getPathString().replace(workingDir.getPathString(), ""); String value = entry.getValue().getPathString().replace(workingDir.getPathString(), ""); userFriendlyMap.put(key, value); } return userFriendlyMap.build(); } private void createTreeStructure(Multimap linksAndFiles) throws IOException { for (Entry entry : linksAndFiles.entries()) { Path filePath = workingDir.getRelative(entry.getKey()); String linkTarget = entry.getValue(); FileSystemUtils.createDirectoryAndParents(filePath.getParentDirectory()); if (!linkTarget.isEmpty()) { filePath.createSymbolicLink(new PathFragment(linkTarget)); } else if (filePath.getPathString().endsWith("/")) { filePath.createDirectory(); } else { FileSystemUtils.createEmptyFile(filePath); } } } /** * Takes a Multimap of file specifications, creates the necessary files / symlinks / dirs, * mounts files listed in customMount at their canonical location in the sandbox and returns the * output of {@code LinuxSandboxedStrategy#fixMounts} for it. */ private ImmutableSetMultimap mounts( Multimap linksAndFiles, List customMounts) throws IOException { createTreeStructure(linksAndFiles); ImmutableSetMultimap.Builder mounts = ImmutableSetMultimap.builder(); for (String customMount : customMounts) { Path customMountPath = workingDir.getRelative(customMount); mounts.put(customMountPath, getSandboxPath(customMountPath)); } return LinuxSandboxedStrategy.validateMounts( fakeSandboxDir, LinuxSandboxedStrategy.withResolvedSymlinks( fakeSandboxDir, LinuxSandboxedStrategy.withRecursedDirs(mounts.build()))); } private ImmutableSetMultimap userFriendlyMounts( Multimap linksAndFiles, List customMounts) throws IOException { return userFriendlyMap(mounts(linksAndFiles, customMounts)); } /** * Takes a Multimap of file specifications, creates the necessary files / symlinks / dirs, * mounts the first file of the specification at its canonical location in the sandbox and returns * the output of {@code LinuxSandboxedStrategy#fixMounts} for it. */ private ImmutableSetMultimap mounts(Multimap linksAndFiles) throws IOException { return mounts(linksAndFiles, ImmutableList.of(Iterables.getFirst(linksAndFiles.keys(), null))); } private ImmutableSetMultimap userFriendlyMounts( Multimap linksAndFiles) throws IOException { return userFriendlyMap(mounts(linksAndFiles)); } /** * Returns a Multimap of mount entries for a list files, which can be used to assert that all * expected mounts have been made by the LinuxSandboxedStrategy. */ private ImmutableSetMultimap asserts(List asserts) { ImmutableSetMultimap.Builder pathifiedAsserts = ImmutableSetMultimap.builder(); for (String fileName : asserts) { Path inputPath = workingDir.getRelative(fileName); pathifiedAsserts.put(inputPath, getSandboxPath(inputPath)); } return pathifiedAsserts.build(); } private ImmutableSetMultimap userFriendlyAsserts(List asserts) { return userFriendlyMap(asserts(asserts)); } @Test public void resolvesRelativeFileToFileSymlinkInSameDir() throws IOException { Multimap testFiles = LinkedHashMultimap.create(); testFiles.put("symlink.txt", "goal.txt"); testFiles.put("goal.txt", ""); List assertMounts = new ArrayList<>(); assertMounts.add("symlink.txt"); assertMounts.add("goal.txt"); assertThat(userFriendlyMounts(testFiles)).containsExactly(userFriendlyAsserts(assertMounts)); } @Test public void resolvesRelativeFileToFileSymlinkInSubDir() throws IOException { Multimap testFiles = ImmutableSetMultimap.of( "symlink.txt", "x/goal.txt", "x/goal.txt", ""); List assertMounts = ImmutableList.of("symlink.txt", "x/goal.txt"); assertThat(userFriendlyMounts(testFiles)).containsExactly(userFriendlyAsserts(assertMounts)); } @Test public void resolvesRelativeFileToFileSymlinkInParentDir() throws IOException { Multimap testFiles = ImmutableSetMultimap.of( "x/symlink.txt", "../goal.txt", "goal.txt", ""); List assertMounts = ImmutableList.of("x/symlink.txt", "goal.txt"); assertThat(userFriendlyMounts(testFiles)).containsExactly(userFriendlyAsserts(assertMounts)); } @Test public void recursesSubDirs() throws IOException { ImmutableList inputFile = ImmutableList.of("a/b"); Multimap testFiles = ImmutableSetMultimap.of( "a/b/x.txt", "", "a/b/y.txt", "z.txt", "a/b/z.txt", ""); List assertMounts = ImmutableList.of("a/b/x.txt", "a/b/y.txt", "a/b/z.txt"); assertThat(userFriendlyMounts(testFiles, inputFile)) .containsExactly(userFriendlyAsserts(assertMounts)); } /** * Test that the algorithm correctly identifies and refuses symlink loops. */ @Test public void catchesSymlinkLoop() throws IOException { try { mounts( ImmutableSetMultimap.of( "a", "b", "b", "a")); fail(); } catch (IOException e) { assertThat(e) .hasMessage( String.format( "%s (Too many levels of symbolic links)", workingDir.getRelative("a").getPathString())); } } /** * Test that the algorithm correctly detects and refuses symlinks whose subcomponents are not all * directories (e.g. "a -> dir/file/file"). */ @Test public void catchesIllegalSymlink() throws IOException { try { mounts( ImmutableSetMultimap.of( "b", "a/c", "a", "")); fail(); } catch (IOException e) { assertThat(e) .hasMessage( String.format("%s (Not a directory)", workingDir.getRelative("a/c").getPathString())); } } @Test public void testParseManifestFile() throws IOException { Path targetDir = workingDir.getRelative("runfiles"); targetDir.createDirectory(); Path testFile = workingDir.getRelative("testfile"); FileSystemUtils.createEmptyFile(testFile); Path manifestFile = workingDir.getRelative("MANIFEST"); FileSystemUtils.writeContent( manifestFile, Charset.defaultCharset(), String.format("x/testfile %s\nx/emptyfile \n", testFile.getPathString())); ImmutableSetMultimap mounts = LinuxSandboxedStrategy.parseManifestFile( fakeSandboxDir, targetDir, manifestFile.getPathFile()); assertThat(userFriendlyMap(mounts)) .containsExactly( userFriendlyMap( ImmutableMultimap.of( testFile, fakeSandboxDir.getRelative("runfiles/x/testfile"), testFS.getPath("/dev/null"), fakeSandboxDir.getRelative("runfiles/x/emptyfile")))); } }