#!/bin/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 sandboxing spawn strategy
#

# 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; }
function set_up {
  add_to_bazelrc "build --spawn_strategy=sandboxed"
  add_to_bazelrc "build --genrule_strategy=sandboxed"
}

function test_sandboxed_tooldir() {
  mkdir -p examples/genrule

  cat << 'EOF' > examples/genrule/BUILD
genrule(
   name = "tooldir",
   srcs = [],
   outs = ["tooldir.txt"],
   cmd = "ls -l external/bazel_tools/tools/genrule | tee $@ >&2; " +
       "cat external/bazel_tools/tools/genrule/genrule-setup.sh >&2",
)
EOF

  bazel build examples/genrule:tooldir &> $TEST_log \
    || fail "Hermetic genrule failed: examples/genrule:tooldir"
  [ -f "bazel-genfiles/examples/genrule/tooldir.txt" ] \
    || fail "Genrule did not produce output: examples/genrule:works"
  cat "bazel-genfiles/examples/genrule/tooldir.txt" > $TEST_log
  expect_log "genrule-setup.sh"
}

function test_sandbox_block_filesystem() {
  # The point of this test is to attempt to read something from the filesystem
  # that is blocked via --sandbox_block_path= and thus shouldn't be accessible.
  #
  # /var/log is an arbitrary choice of directory that should exist on all
  # Unix-like systems.
  local block_path
  case "$(uname -s)" in
    Darwin)
      # TODO(jmmv): sandbox-exec does not resolve symlinks, so attempting
      # to block /var/log does not work. Unsure if we should make this work
      # by resolving symlinks or documenting the expected behavior.
      block_path=/private/var/log
      ;;
    *)
      block_path=/var/log
      ;;
  esac

  mkdir pkg
  cat >pkg/BUILD <<EOF
genrule(
  name = "breaks",
  srcs = [ "a.txt" ],
  outs = [ "breaks.txt" ],
  cmd = "ls ${block_path} &> \$@",
)
EOF
  touch pkg/a.txt

  local output_file="bazel-genfiles/pkg/breaks.txt"

  bazel build --sandbox_block_path="${block_path}" pkg:breaks \
    &> $TEST_log \
    && fail "Non-hermetic genrule succeeded: examples/genrule:breaks" || true

  [ -f "$output_file" ] ||
    fail "Action did not produce output: $output_file"
  cat "${output_file}" >$TEST_log

  if [ "$(wc -l $output_file | awk '{print $1}')" -gt 1 ]; then
    fail "Output contained more than one line: $output_file"
  fi

  grep -E "(Operation not permitted|Permission denied)" $output_file ||
    fail "Output did not contain expected error message: $output_file"
}

# TODO(philwo) - this doesn't work on Ubuntu 14.04 due to "unshare" being too
# old and not understanding the --user flag.
function DISABLED_test_sandbox_different_nobody_uid() {
  cat /etc/passwd | sed 's/\(^nobody:[^:]*:\)[0-9]*:[0-9]*/\15000:16000/g' > \
      "${TEST_TMPDIR}/passwd"
  unshare --user --mount --map-root-user -- bash - \
      << EOF || fail "Hermetic genrule with different UID for nobody failed" \
set -e
set -u

mount --bind ${TEST_TMPDIR}/passwd /etc/passwd
bazel build examples/genrule:works &> ${TEST_log}
EOF
}

# Tests that a pseudoterminal can be opened in linux when --sandbox_explicit_pseudoterminal is active
function test_can_enable_pseudoterminals() {
  if [[ "$(uname -s)" != Linux ]]; then
    echo "Skipping test: flag intended for linux systems"
    return 0
  fi

  cat > test.py <<'EOF'
import pty
pty.openpty()
EOF
  cat > BUILD <<'EOF'
py_test(
  name = "test",
  srcs = ["test.py"],
)
EOF
  bazel test --sandbox_explicit_pseudoterminal --verbose_failures --sandbox_debug :test || fail "test did not pass"
}

function test_sandbox_debug() {
  if [[ "$PLATFORM" == "darwin" ]]; then
    # The process wrapper sandbox used in MacOS doesn't emit debug output.
    return 0
  fi

  cat > BUILD <<'EOF'
genrule(
  name = "broken",
  outs = ["bla.txt"],
  cmd = "exit 1",
)
EOF
  bazel build --verbose_failures :broken &> $TEST_log \
    && fail "build should have failed" || true
  expect_log "Use --sandbox_debug to see verbose messages from the sandbox and retain the sandbox build root for debugging"
  expect_log "Executing genrule //:broken failed"

  bazel build --verbose_failures --sandbox_debug :broken &> $TEST_log \
    && fail "build should have failed" || true
  expect_log "Executing genrule //:broken failed"
  expect_not_log "Use --sandbox_debug to see verbose messages from the sandbox and retain the sandbox build root for debugging"
  expect_log "child exited normally with code 1"

  bazel build --verbose_failures --sandbox_debug --experimental_use_hermetic_linux_sandbox :broken &> $TEST_log \
    && fail "build should have failed with hermetic sandbox" || true
  expect_log "child exited normally with code 1"

  bazel build --verbose_failures --sandbox_debug --incompatible_sandbox_hermetic_tmp :broken &> $TEST_log \
    && fail "build should have failed with hermetic sandbox /tmp" || true
  expect_log "child exited normally with code 1"
}

function test_sandbox_expands_tree_artifacts_in_runfiles_tree() {
  create_workspace_with_default_repos WORKSPACE

  cat > def.bzl <<'EOF'
def _mkdata_impl(ctx):
    out = ctx.actions.declare_directory(ctx.label.name + ".d")
    script = "mkdir -p {out}; touch {out}/file; ln -s file {out}/link".format(out = out.path)
    ctx.actions.run_shell(
        outputs = [out],
        command = script,
    )
    runfiles = ctx.runfiles(files = [out])
    return [DefaultInfo(
        files = depset([out]),
        runfiles = runfiles,
    )]

mkdata = rule(
    _mkdata_impl,
)
EOF

  cat > mkdata_test.sh <<'EOF'
#!/bin/bash

set -euo pipefail

test_dir="$1"
cd "$test_dir"
ls -l | cut -f1,9 -d' ' >&2

if [ ! -f file -o -L file ]; then
  echo "'file' is not a regular file" >&2
  exit 1
fi
EOF
  chmod +x mkdata_test.sh

  cat > BUILD <<'EOF'
load("//:def.bzl", "mkdata")

mkdata(name = "mkdata")

sh_test(
    name = "mkdata_test",
    srcs = ["mkdata_test.sh"],
    args = ["$(location :mkdata)"],
    data = [":mkdata"],
)
EOF

  bazel test --test_output=streamed //:mkdata_test &>$TEST_log && fail "expected test to fail" || true
  expect_log "'file' is not a regular file"
}

# Regression test for https://github.com/bazelbuild/bazel/issues/6262.
function test_create_tree_artifact_outputs() {
  create_workspace_with_default_repos WORKSPACE

  cat > def.bzl <<'EOF'
def _r(ctx):
    d = ctx.actions.declare_directory("%s_dir" % ctx.label.name)
    ctx.actions.run_shell(
        outputs = [d],
        command = "cd %s && pwd" % d.path,
    )
    return [DefaultInfo(files = depset([d]))]

r = rule(implementation = _r)
EOF

cat > BUILD <<'EOF'
load(":def.bzl", "r")

r(name = "a")
EOF

  bazel build --test_output=streamed :a &>$TEST_log || fail "expected build to succeed"
}

# Regression test for https://github.com/bazelbuild/bazel/issues/20032.
function test_read_only_tree_artifact() {
  create_workspace_with_default_repos WORKSPACE

  cat > def.bzl <<'EOF'
def _r(ctx):
  d = ctx.actions.declare_directory(ctx.label.name)
  ctx.actions.run_shell(
    outputs = [d],
    command = "touch $1/file.txt && chmod -w $1",
    arguments = [d.path],
  )
  return DefaultInfo(files = depset([d]))

r = rule(_r)
EOF

  cat > BUILD <<'EOF'
load(":def.bzl", "r")

r(name = "a")
EOF

  bazel build --test_output=streamed :a &>$TEST_log || fail "expected build to succeed"
}

function test_empty_tree_artifact_as_inputs() {
  # Test that when an empty tree artifact is the input, an empty directory is
  # created in the sandbox for action to read.
  create_workspace_with_default_repos WORKSPACE

  mkdir -p pkg

  cat > pkg/def.bzl <<'EOF'
def _r(ctx):
    empty_d = ctx.actions.declare_directory("%s/empty_dir" % ctx.label.name)
    ctx.actions.run_shell(
        outputs = [empty_d],
        command = "mkdir -p %s" % empty_d.path,
    )
    f = ctx.actions.declare_file("%s/file" % ctx.label.name)
    ctx.actions.run_shell(
        inputs = [empty_d],
        outputs = [f],
        command = "touch %s && cd %s && pwd" % (f.path, empty_d.path),
    )
    return [DefaultInfo(files = depset([f]))]

r = rule(implementation = _r)
EOF

cat > pkg/BUILD <<'EOF'
load(":def.bzl", "r")

r(name = "a")
EOF

  bazel build //pkg:a &>$TEST_log || fail "expected build to succeed"
}

function test_add_mount_pair_tmp_source() {
  if [[ "$PLATFORM" == "darwin" ]]; then
    # Tests Linux-specific functionality
    return 0
  fi

  create_workspace_with_default_repos WORKSPACE

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

  local mounted=$(mktemp -d "/tmp/bazel_mounted.XXXXXXXX")
  trap "rm -fr $mounted" EXIT
  echo GOOD > "$mounted/data.txt"

  mkdir -p pkg
  cat > pkg/BUILD <<EOF
genrule(
    name = "gen",
    outs = ["gen.txt"],
    # Verify that /tmp is still hermetic.
    cmd = """[ ! -e "${mounted}/data.txt" ] && cp /etc/data.txt \$@""",
)
EOF

  # This assumes the existence of /etc on the host system
  bazel build --sandbox_add_mount_pair="$mounted:/etc" //pkg:gen || fail "build failed"
  assert_contains GOOD bazel-bin/pkg/gen.txt
}

function test_add_mount_pair_tmp_target() {
  if [[ "$PLATFORM" == "darwin" ]]; then
    # Tests Linux-specific functionality
    return 0
  fi

  create_workspace_with_default_repos WORKSPACE

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

  local source_dir=$(mktemp -d "/tmp/bazel_mounted.XXXXXXXX")
  trap "rm -fr $source_dir" EXIT
  echo BAD > "$source_dir/data.txt"

  mkdir -p pkg
  cat > pkg/BUILD <<EOF
genrule(
    name = "gen",
    outs = ["gen.txt"],
    # Verify that /tmp is still hermetic.
    cmd = """[ ! -e "${source_dir}/data.txt" ] && ls "$source_dir" > \$@""",
)
EOF


  # This assumes the existence of /etc on the host system
  bazel build --sandbox_add_mount_pair="/etc:$source_dir" //pkg:gen || fail "build failed"
  assert_contains passwd bazel-bin/pkg/gen.txt
}

function test_add_mount_pair_tmp_target_and_source() {
  if [[ "$PLATFORM" == "darwin" ]]; then
    # Tests Linux-specific functionality
    return 0
  fi

  create_workspace_with_default_repos WORKSPACE

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

  local mounted=$(mktemp -d "/tmp/bazel_mounted.XXXXXXXX")
  trap "rm -fr $mounted" EXIT
  echo GOOD > "$mounted/data.txt"

  local tmp_file=$(mktemp "/tmp/bazel_tmp.XXXXXXXX")
  trap "rm $tmp_file" EXIT
  echo BAD > "$tmp_file"

  mkdir -p pkg
  cat > pkg/BUILD <<EOF
genrule(
    name = "gen",
    outs = ["gen.txt"],
    # Verify that /tmp is still hermetic.
    cmd = """[ ! -e "${tmp_file}" ] && cp "$mounted/data.txt" \$@""",
)
EOF

  bazel build --sandbox_add_mount_pair="$mounted" //pkg:gen || fail "build failed"
  assert_contains GOOD bazel-bin/pkg/gen.txt
}

function test_symlink_with_output_base_under_tmp() {
  if [[ "$PLATFORM" == "darwin" ]]; then
    # Tests Linux-specific functionality
    return 0
  fi

  create_workspace_with_default_repos WORKSPACE

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

  mkdir -p pkg
  cat > pkg/BUILD <<'EOF'
load(":r.bzl", "symlink_rule")

genrule(name="r1", srcs=[], outs=[":r1"], cmd="echo CONTENT > $@")
symlink_rule(name="r2", input=":r1")
genrule(name="r3", srcs=[":r2"], outs=[":r3"], cmd="cp $< $@")
EOF

  cat > pkg/r.bzl <<'EOF'
def _symlink_impl(ctx):
  output = ctx.actions.declare_file(ctx.label.name)
  ctx.actions.symlink(output = output, target_file = ctx.file.input)
  return [DefaultInfo(files = depset([output]))]

symlink_rule = rule(
  implementation = _symlink_impl,
  attrs = {"input": attr.label(allow_single_file=True)})
EOF

  local tmp_output_base=$(mktemp -d "/tmp/bazel_output_base.XXXXXXXX")
  trap "chmod -R u+w $tmp_output_base && rm -fr $tmp_output_base" EXIT

  bazel --output_base="$tmp_output_base" build //pkg:r3
  assert_contains CONTENT bazel-bin/pkg/r3
  bazel --output_base="$tmp_output_base" shutdown
}

function test_symlink_to_directory_with_output_base_under_tmp() {
  if [[ "$PLATFORM" == "darwin" ]]; then
    # Tests Linux-specific functionality
    return 0
  fi

  create_workspace_with_default_repos WORKSPACE

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

  mkdir -p pkg
  cat > pkg/BUILD <<'EOF'
load(":r.bzl", "symlink_rule", "tree_rule")

tree_rule(name="t1")
symlink_rule(name="t2", input=":t1")
genrule(name="t3", srcs=[":t2"], outs=[":t3"], cmd=";\n".join(
    ["cat $(location :t2)/{a/a,b/b} > $@"]))
EOF

  cat > pkg/r.bzl <<'EOF'
def _symlink_impl(ctx):
  output = ctx.actions.declare_directory(ctx.label.name)
  ctx.actions.symlink(output = output, target_file = ctx.file.input)
  return [DefaultInfo(files = depset([output]))]

symlink_rule = rule(
  implementation = _symlink_impl,
  attrs = {"input": attr.label(allow_single_file=True)})

def _tree_impl(ctx):
  output = ctx.actions.declare_directory(ctx.label.name)
  ctx.actions.run_shell(
    outputs = [output],
    command = "export TREE=%s && mkdir $TREE/a $TREE/b && echo -n A > $TREE/a/a && echo -n B > $TREE/b/b" % output.path)
  return [DefaultInfo(files = depset([output]))]

tree_rule = rule(
  implementation = _tree_impl,
  attrs = {})

EOF

  local tmp_output_base=$(mktemp -d "/tmp/bazel_output_base.XXXXXXXX")
  trap "chmod -R u+w $tmp_output_base && rm -fr $tmp_output_base" EXIT

  bazel --output_base="$tmp_output_base" build //pkg:t3
  assert_contains AB bazel-bin/pkg/t3
  bazel --output_base="$tmp_output_base" shutdown
}

function test_tmpfs_path_under_tmp() {
  if [[ "$PLATFORM" == "darwin" ]]; then
    # Tests Linux-specific functionality
    return 0
  fi

  create_workspace_with_default_repos WORKSPACE

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

  local tmp_file=$(mktemp "/tmp/bazel_tmp.XXXXXXXX")
  trap "rm $tmp_file" EXIT
  echo BAD > "$tmp_file"

  local tmpfs=$(mktemp -d "/tmp/bazel_tmpfs.XXXXXXXX")
  trap "rm -fr $tmpfs" EXIT
  echo BAD > "$tmpfs/data.txt"

  mkdir -p pkg
  cat > pkg/BUILD <<EOF
genrule(
    name = "gen",
    outs = ["gen.txt"],
    cmd = """
# Verify that /tmp is still hermetic and that the tmpfs under /tmp exists, but is empty.
[[ ! -e "${tmp_file}" ]]
[[ -d /tmp/tmpfs ]]
[[ ! -e /tmp/tmpfs/data.txt ]]
# Verify that the tmpfs on /etc exists and is empty.
[[ -d /etc ]]
[[ -z "\$\$(ls -A /etc)" ]]
touch \$@
""",
)
EOF

  # This assumes the existence of /etc on the host system
  bazel build --sandbox_tmpfs_path=/tmp/tmpfs --sandbox_tmpfs_path=/etc \
    //pkg:gen || fail "build failed"
}

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

run_suite "sandbox"
