#!/usr/bin/env bash
#
# 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.
#
# Test hermetic Linux sandbox
#


# Load test environment
# Load the test setup defined in the parent directory
CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${CURRENT_DIR}/../integration_test_setup.sh" \
  || { echo "integration_test_setup.sh not found!" >&2; exit 1; }
source ${CURRENT_DIR}/../sandboxing_test_utils.sh \
  || { echo "sandboxing_test_utils.sh not found!" >&2; exit 1; }

cat >>$TEST_TMPDIR/bazelrc <<'EOF'
# Testing the sandboxed strategy requires using the sandboxed strategy. While it is the default,
# we want to make sure that this explicitly fails when the strategy is not available on the system
# running the test.
# The hermetic sandbox requires the Linux sandbox.
build --spawn_strategy=sandboxed
build --experimental_use_hermetic_linux_sandbox
build --sandbox_fake_username
EOF

# For the test to work we need to bind mount a couple of folders to
# get access to bash, ls, python etc. Depending on linux distribution
# these folders may vary. Mount all folders in the root directory '/'
# except the project directory, the directory containing the bazel
# workspace under test.
project_folder=`pwd | cut -d"/" -f 2`
for folder in /*/
do
  if [ -d "$folder" ] && [ "$folder" != "/$project_folder/" ]
  then
    if [[ -L $folder ]]
    then
      # Get resolved link
      linked_folder=`readlink -f $folder`
      echo "build --sandbox_add_mount_pair=/$linked_folder:$folder" >> $TEST_TMPDIR/bazelrc
    else
      echo "build --sandbox_add_mount_pair=$folder" >> $TEST_TMPDIR/bazelrc
    fi
  fi
done

function set_up {
  export BAZEL_GENFILES_DIR=$(bazel info bazel-genfiles 2>/dev/null)
  export BAZEL_BIN_DIR=$(bazel info bazel-bin 2>/dev/null)

  sed -i.bak '/sandbox_tmpfs_path/d' $TEST_TMPDIR/bazelrc

  add_rules_python "MODULE.bazel"
  mkdir -p examples/hermetic

  cat << 'EOF' > examples/hermetic/unknown_file.txt
text inside this file
EOF

  ABSOLUTE_PATH=$CURRENT_DIR/workspace/examples/hermetic/unknown_file.txt

  # In this case the ABSOLUTE_PATH will be expanded
  # and the absolute path will be written to script_absolute_path.sh
  cat << EOF > examples/hermetic/script_absolute_path.sh
#! /bin/sh
ls ${ABSOLUTE_PATH}
EOF

  chmod 777 examples/hermetic/script_absolute_path.sh

  cat << 'EOF' > examples/hermetic/script_symbolic_link.sh
#! /bin/sh
OUTSIDE_SANDBOX_DIR=$(dirname $(realpath $0))
cat $OUTSIDE_SANDBOX_DIR/unknown_file.txt
EOF

  chmod 777 examples/hermetic/script_symbolic_link.sh

  touch examples/hermetic/import_module.py

  cat << 'EOF' > examples/hermetic/py_module_test.py
import import_module
EOF

  cat << 'EOF' > examples/hermetic/BUILD
load("@rules_python//python:py_test.bzl", "py_test")
load(
  "test.bzl",
  "overwrite_via_symlink",
  "overwrite_file_from_declared_directory",
  "subdirectories_in_declared_directory",
  "other_artifacts",
)

overwrite_via_symlink(
  name = "overwrite_via_resolved_symlink",
  resolve_symlink = True
)

overwrite_via_symlink(
  name = "overwrite_via_unresolved_symlink",
  resolve_symlink = False
)

overwrite_file_from_declared_directory(
  name = "overwrite_file_from_declared_directory"
)

subdirectories_in_declared_directory(
  name = "subdirectories_in_declared_directory"
)

other_artifacts(
  name = "other_artifacts"
)

genrule(
  name = "absolute_path",
  srcs = ["script_absolute_path.sh"], # unknown_file.txt not referenced.
  outs = [ "absolute_path.txt" ],
  cmd = "./$(location :script_absolute_path.sh) > $@",
)

genrule(
  name = "symbolic_link",
  srcs = ["script_symbolic_link.sh"], # unknown_file.txt not referenced.
  outs = ["symbolic_link.txt"],
  cmd = "./$(location :script_symbolic_link.sh) > $@",
)

py_test(
  name = "py_module_test",
  srcs = ["py_module_test.py"],  # import_module.py not referenced.
  size = "small",
)

genrule(
  name = "input_file",
  outs = ["input_file.txt"],
  cmd = "echo original text input > $@",
)

genrule(
  name = "write_input_test",
  srcs = [":input_file"],
  outs = ["status.txt"],
  cmd = "(chmod 777 $(location :input_file) && \
         (echo overwrite text > $(location :input_file)) && \
         (echo success > $@)) || (echo fail > $@)",
)
EOF

  cat << 'EOF' > examples/hermetic/test.bzl

def _overwrite_via_symlink_impl(ctx):
    file = ctx.actions.declare_file(ctx.attr.name + ".file")
    if ctx.attr.resolve_symlink:
        symlink = ctx.actions.declare_file(ctx.attr.name + ".symlink")
    else:
        symlink = ctx.actions.declare_symlink(ctx.attr.name + ".symlink")

    ctx.actions.write(file, "")

    if ctx.attr.resolve_symlink:
        ctx.actions.symlink(
            output = symlink,
            target_file = file
        )
        # Symlink become resolved to RegularFileArtifactValue.
        needed_inputs = [symlink]
    else:
        ctx.actions.symlink(
            output = symlink,
            target_path = file.basename
        )
        # Symlink become UnresolvedSymlinkArtifactValue and would be
        # dangling unless also providing the actual file as input to sandbox.
        needed_inputs = [symlink, file]

    result_file = ctx.actions.declare_file(ctx.attr.name + ".result")

    # Try invalid write to the input file via the symlink
    ctx.actions.run_shell(
       command = "chmod u+w $1 && echo hello >> $1 && ls -lR > $2",
       arguments = [symlink.path, result_file.path],
       inputs = needed_inputs,
       outputs = [result_file],
    )

    return [DefaultInfo(files = depset([result_file]))]

overwrite_via_symlink = rule(
    attrs = {
        "resolve_symlink" : attr.bool(),
    },
    implementation = _overwrite_via_symlink_impl,
)


def _overwrite_file_from_declared_directory_impl(ctx):
    dir = ctx.actions.declare_directory(ctx.attr.name + ".dir")

    ctx.actions.run_shell(
        command = "mkdir -p $1/subdir && touch $1/subdir/file",
        arguments = [dir.path],
        outputs = [dir],
    )

    # Try invalid write to input file, with file as implicit input
    # from declared directory.
    result_file = ctx.actions.declare_file(ctx.attr.name + ".result")
    ctx.actions.run_shell(
       command = "chmod -R u+w $1 && echo hello >> $1/subdir/file && touch $2",
       arguments = [dir.path, result_file.path],
       inputs = [dir],
       outputs = [result_file],
    )

    return [DefaultInfo(files = depset([result_file]))]

overwrite_file_from_declared_directory = rule(
    implementation = _overwrite_file_from_declared_directory_impl,
)


def _subdirectories_in_declared_directory_impl(ctx):
    dir = ctx.actions.declare_directory(ctx.attr.name + ".dir")

    ctx.actions.run_shell(
        command = "mkdir -p %s/subdir1/subdir2" % dir.path,
        outputs = [dir],
    )

    result_file = ctx.actions.declare_file(ctx.attr.name + ".result")
    ctx.actions.run_shell(
        command = "ls -lRH %s > %s" % (dir.path, result_file.path),
        inputs = [dir],
        outputs = [result_file],
    )

    return [DefaultInfo(files = depset([result_file]))]

subdirectories_in_declared_directory = rule(
    implementation = _subdirectories_in_declared_directory_impl,
)


def _other_artifacts_impl(ctx):

    # Produce artifacts of other types

    file_artifact = ctx.actions.declare_file(ctx.attr.name + ".file_artifact")
    directory_artifact = ctx.actions.declare_directory(ctx.attr.name + ".directory_artifact")
    symlink_artifact = ctx.actions.declare_symlink(ctx.attr.name + ".symlink_artifact")

    ctx.actions.run_shell(
        command = "touch %s && mkdir -p %s && ln -s dangling %s" % (
            file_artifact.path, directory_artifact.path, symlink_artifact.path),
        outputs = [file_artifact, directory_artifact, symlink_artifact],
    )

    # Test other artifact types as input to hermetic sandbox.

    all_artifacts = [file_artifact,
                     directory_artifact,
                     symlink_artifact]
    input_paths_string = " ".join([a.path for a in all_artifacts])
    result_file = ctx.actions.declare_file(ctx.attr.name + ".result")
    ctx.actions.run_shell(
        command = "ls -lR %s > %s" % (input_paths_string, result_file.path),
        inputs = all_artifacts,
        outputs = [result_file],
    )

    return [DefaultInfo(files = depset([result_file]))]

other_artifacts = rule(
    implementation = _other_artifacts_impl,
)
EOF
}

# Test that the build can't escape the sandbox via absolute path.
function test_absolute_path() {
  bazel build examples/hermetic:absolute_path &> $TEST_log \
    && fail "Fail due to non hermetic sandbox: examples/hermetic:absolute_path" || true
  expect_log "ls:.* '\?.*/examples/hermetic/unknown_file.txt'\?: No such file or directory"
}

# Test that the build can't escape the sandbox by resolving symbolic link.
function test_symbolic_link() {
  bazel build examples/hermetic:symbolic_link &> $TEST_log \
    && fail "Fail due to non hermetic sandbox: examples/hermetic:symbolic_link" || true
  expect_log "cat: \/execroot\/_main\/examples\/hermetic\/unknown_file.txt: No such file or directory"
}

# Test that the sandbox discover if the bazel python rule miss dependencies.
function test_missing_python_deps() {
  bazel test examples/hermetic:py_module_test --test_output=all &> $TEST_TMPDIR/log \
    && fail "Fail due to non hermetic sandbox: examples/hermetic:py_module_test" || true

  expect_log "No module named '\?import_module'\?"
}

# Test that the intermediate corrupt input file gets re:evaluated
function test_writing_input_file() {
  # Write an input file, this should cause the hermetic sandbox to fail with an exception
  bazel build examples/hermetic:write_input_test &> $TEST_log  \
    && fail "Fail due to non hermetic sandbox: examples/hermetic:write_input_test" || true
  expect_log "input dependency .*examples/hermetic/input_file.txt was modified during execution."
  cat "${BAZEL_GENFILES_DIR}/examples/hermetic/input_file.txt" &> $TEST_log
  expect_log "overwrite text"

  # Build the input file again, this should not use the cache, but instead re:evaluate the file
  bazel build examples/hermetic:input_file &> $TEST_log \
    || fail "Fail due to non hermetic sandbox: examples/hermetic:input_file"
  [ -f "${BAZEL_GENFILES_DIR}/examples/hermetic/input_file.txt" ] \
    || fail "Genrule did not produce output: examples/hermetic:input_file"
  cat "${BAZEL_GENFILES_DIR}/examples/hermetic/input_file.txt" &> $TEST_log
  expect_log "original text input"
}

# Test that invalid write of input file is detected, when file is accessed via resolved symlink.
function test_overwrite_via_resolved_symlink() {
  bazel build examples/hermetic:overwrite_via_resolved_symlink &> $TEST_log  \
    && fail "Hermetic sandbox did not detect invalid write to input file"
  expect_log "input dependency .* was modified during execution."
}

# Test that invalid write of input file is detected, when file is accessed via unresolved symlink.
function test_overwrite_via_unresolved_symlink() {
  bazel build examples/hermetic:overwrite_via_unresolved_symlink &> $TEST_log  \
    && fail "Hermetic sandbox did not detect invalid write to input file"
  expect_log "input dependency .* was modified during execution."
}

# Test that invalid write of input file is detected, when file is found implicit via declared directory.
function test_overwrite_file_from_declared_directory() {
  bazel build examples/hermetic:overwrite_file_from_declared_directory &> $TEST_log  \
    && fail "Hermetic sandbox did not detect invalid write to input file"
  expect_log "input dependency .* was modified during execution."
}

# Test that the sandbox can handle deep directory trees from declared directory.
function test_subdirectories_in_declared_directory() {
  bazel build examples/hermetic:subdirectories_in_declared_directory &> $TEST_log
  cat bazel-bin/examples/hermetic/subdirectories_in_declared_directory.result
  assert_contains "dir/subdir1/subdir2" "bazel-bin/examples/hermetic/subdirectories_in_declared_directory.result"
}

# Test that the sandbox is able to handle various types of artifacts.
# Regression test for Issue #15340
function test_other_artifacts() {
  bazel build examples/hermetic:other_artifacts &> $TEST_log
  assert_contains ".file_artifact" "bazel-bin/examples/hermetic/other_artifacts.result"
  assert_contains ".symlink_artifact" "bazel-bin/examples/hermetic/other_artifacts.result"
  assert_contains ".directory_artifact" "bazel-bin/examples/hermetic/other_artifacts.result"
}

# The test shouldn't fail if the environment doesn't support running it.
check_sandbox_allowed || exit 0
is_linux || exit 0

run_suite "hermetic_sandbox"
