// Copyright 2015 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.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.ImmutableMap; import com.google.common.collect.Iterables; import com.google.devtools.build.lib.actions.UserExecException; 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 org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import java.io.IOException; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; 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 extends LinuxSandboxedStrategyTestCase { /** * 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 ImmutableMap userFriendlyMap(Map input) { ImmutableMap.Builder userFriendlyMap = ImmutableMap.builder(); for (Entry entry : input.entrySet()) { String key = entry.getKey().getPathString().replace(workspaceDir.getPathString(), ""); String value = entry.getValue().getPathString().replace(workspaceDir.getPathString(), ""); userFriendlyMap.put(key, value); } return userFriendlyMap.build(); } /** * Takes a map 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 ImmutableMap userFriendlyMounts( Map linksAndFiles, List customMounts) throws IOException { return userFriendlyMap(mounts(linksAndFiles, customMounts)); } private ImmutableMap mounts( Map linksAndFiles, List customMounts) throws IOException { createTreeStructure(linksAndFiles); ImmutableMap.Builder mounts = ImmutableMap.builder(); for (String customMount : customMounts) { Path customMountPath = workspaceDir.getRelative(customMount); mounts.put(customMountPath, customMountPath); } return LinuxSandboxedStrategy.validateMounts( LinuxSandboxedStrategy.withResolvedSymlinks( LinuxSandboxedStrategy.withRecursedDirs(mounts.build()))); } /** * Takes a map 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 Map userFriendlyMounts(Map linksAndFiles) throws IOException { return userFriendlyMap(mounts(linksAndFiles)); } private Map mounts(Map linksAndFiles) throws IOException { return mounts( linksAndFiles, ImmutableList.of(Iterables.getFirst(linksAndFiles.keySet(), null))); } /** * Returns a map of mount entries for a list files, which can be used to assert that all * expected mounts have been made by the LinuxSandboxedStrategy. */ private ImmutableMap userFriendlyAsserts(List asserts) { return userFriendlyMap(asserts(asserts)); } private ImmutableMap asserts(List asserts) { ImmutableMap.Builder pathifiedAsserts = ImmutableMap.builder(); for (String fileName : asserts) { Path inputPath = workspaceDir.getRelative(fileName); pathifiedAsserts.put(inputPath, inputPath); } return pathifiedAsserts.build(); } private void createTreeStructure(Map linksAndFiles) throws IOException { for (Entry entry : linksAndFiles.entrySet()) { Path filePath = workspaceDir.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); } } } @Test public void testResolvesRelativeFileToFileSymlinkInSameDir() throws IOException { Map testFiles = new LinkedHashMap<>(); 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)).isEqualTo(userFriendlyAsserts(assertMounts)); } @Test public void testResolvesRelativeFileToFileSymlinkInSubDir() throws IOException { Map testFiles = ImmutableMap.of( "symlink.txt", "x/goal.txt", "x/goal.txt", ""); List assertMounts = ImmutableList.of("symlink.txt", "x/goal.txt"); assertThat(userFriendlyMounts(testFiles)).isEqualTo(userFriendlyAsserts(assertMounts)); } @Test public void testResolvesRelativeFileToFileSymlinkInParentDir() throws IOException { Map testFiles = ImmutableMap.of( "x/symlink.txt", "../goal.txt", "goal.txt", ""); List assertMounts = ImmutableList.of("x/symlink.txt", "goal.txt"); assertThat(userFriendlyMounts(testFiles)).isEqualTo(userFriendlyAsserts(assertMounts)); } @Test public void testRecursesSubDirs() throws IOException { ImmutableList inputFile = ImmutableList.of("a/b"); Map testFiles = ImmutableMap.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)) .isEqualTo(userFriendlyAsserts(assertMounts)); } /** * Test that the algorithm correctly identifies and refuses symlink loops. */ @Test public void testCatchesSymlinkLoop() throws IOException { try { mounts( ImmutableMap.of( "a", "b", "b", "a")); fail(); } catch (IOException e) { assertThat(e) .hasMessage( String.format( "%s (Too many levels of symbolic links)", workspaceDir.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 testCatchesIllegalSymlink() throws IOException { try { mounts( ImmutableMap.of( "b", "a/c", "a", "")); fail(); } catch (IOException e) { assertThat(e) .hasMessage( String.format( "%s (Not a directory)", workspaceDir.getRelative("a/c").getPathString())); } } @Test public void testParseManifestFile() throws IOException, UserExecException { Path targetDir = workspaceDir.getRelative("runfiles"); targetDir.createDirectory(); Path testFile = workspaceDir.getRelative("testfile"); FileSystemUtils.createEmptyFile(testFile); Path manifestFile = workspaceDir.getRelative("MANIFEST"); FileSystemUtils.writeContent( manifestFile, Charset.defaultCharset(), String.format("x/testfile %s\nx/emptyfile \n", testFile.getPathString())); Map mounts = LinuxSandboxedStrategy.parseManifestFile(targetDir, manifestFile.getPathFile(), false, ""); assertThat(userFriendlyMap(mounts)) .isEqualTo( userFriendlyMap( ImmutableMap.of( fileSystem.getPath("/runfiles/x/testfile"), testFile, fileSystem.getPath("/runfiles/x/emptyfile"), fileSystem.getPath("/dev/null")))); } @Test public void testParseFilesetManifestFile() throws IOException, UserExecException { Path targetDir = workspaceDir.getRelative("fileset"); targetDir.createDirectory(); Path testFile = workspaceDir.getRelative("testfile"); FileSystemUtils.createEmptyFile(testFile); Path manifestFile = workspaceDir.getRelative("MANIFEST"); FileSystemUtils.writeContent( manifestFile, Charset.defaultCharset(), String.format("workspace/x/testfile %s\n0\n", testFile.getPathString())); Map mounts = LinuxSandboxedStrategy.parseManifestFile( targetDir, manifestFile.getPathFile(), true, "workspace"); assertThat(userFriendlyMap(mounts)) .isEqualTo( userFriendlyMap(ImmutableMap.of(fileSystem.getPath("/fileset/x/testfile"), testFile))); } }