// Copyright 2014 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 java.nio.charset.StandardCharsets.UTF_8;

import com.google.common.collect.ImmutableMap;
import com.google.common.io.ByteStreams;
import com.google.devtools.build.lib.shell.Command;
import com.google.devtools.build.lib.shell.CommandException;
import com.google.devtools.build.lib.shell.KillableObserver;
import com.google.devtools.build.lib.shell.TimeoutKillableObserver;
import com.google.devtools.build.lib.util.OS;
import com.google.devtools.build.lib.vfs.Path;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Helper class for running the namespace sandbox. This runner prepares environment inside the
 * sandbox, handles sandbox output, performs cleanup and changes invocation if necessary.
 */
final class DarwinSandboxRunner extends SandboxRunner {
  private static final String SANDBOX_EXEC = "/usr/bin/sandbox-exec";

  private final Path sandboxExecRoot;
  private final Path argumentsFilePath;
  private final Set<Path> writableDirs;
  private final Set<Path> inaccessiblePaths;
  private final Path runUnderPath;

  DarwinSandboxRunner(
      Path sandboxPath,
      Path sandboxExecRoot,
      Set<Path> writableDirs,
      Set<Path> inaccessiblePaths,
      Path runUnderPath,
      boolean verboseFailures) {
    super(verboseFailures);
    this.sandboxExecRoot = sandboxExecRoot;
    this.argumentsFilePath = sandboxPath.getRelative("sandbox.sb");
    this.writableDirs = writableDirs;
    this.inaccessiblePaths = inaccessiblePaths;
    this.runUnderPath = runUnderPath;
  }

  static boolean isSupported() {
    // Check osx version, only >=10.11 is supported.
    // And we should check if sandbox still work when it gets 11.x
    String osxVersion = OS.getVersion();
    String[] parts = osxVersion.split("\\.");
    if (parts.length != 3) {
      // Currently the format is 10.11.x
      return false;
    }
    try {
      int v0 = Integer.parseInt(parts[0]);
      int v1 = Integer.parseInt(parts[1]);
      if (v0 != 10 || v1 < 11) {
        return false;
      }
    } catch (NumberFormatException e) {
      return false;
    }

    List<String> args = new ArrayList<>();
    args.add(SANDBOX_EXEC);
    args.add("-p");
    args.add("(version 1) (allow default)");
    args.add("/usr/bin/true");

    ImmutableMap<String, String> env = ImmutableMap.of();
    File cwd = new File("/usr/bin");

    Command cmd = new Command(args.toArray(new String[0]), env, cwd);
    try {
      cmd.execute(
          /* stdin */ new byte[] {},
          Command.NO_OBSERVER,
          ByteStreams.nullOutputStream(),
          ByteStreams.nullOutputStream(),
          /* killSubprocessOnInterrupt */ true);
    } catch (CommandException e) {
      return false;
    }

    return true;
  }

  @Override
  protected Command getCommand(
      List<String> arguments, Map<String, String> environment, int timeout, boolean allowNetwork)
      throws IOException {
    writeConfig(allowNetwork);

    List<String> commandLineArgs = new ArrayList<>();
    commandLineArgs.add(SANDBOX_EXEC);
    commandLineArgs.add("-f");
    commandLineArgs.add(argumentsFilePath.getPathString());
    commandLineArgs.addAll(arguments);
    return new Command(
        commandLineArgs.toArray(new String[0]), environment, sandboxExecRoot.getPathFile());
  }

  private void writeConfig(boolean allowNetwork) throws IOException {
    try (PrintWriter out =
        new PrintWriter(
            new BufferedWriter(
                new OutputStreamWriter(argumentsFilePath.getOutputStream(), UTF_8)))) {
      // Note: In Apple's sandbox configuration language, the *last* matching rule wins.
      out.println("(version 1)");
      out.println("(debug deny)");
      out.println("(allow default)");

      if (!allowNetwork) {
        out.println("(deny network*)");
      }

      out.println("(allow network* (local ip \"localhost:*\"))");
      out.println("(allow network* (remote ip \"localhost:*\"))");

      for (Path inaccessiblePath : inaccessiblePaths) {
        out.println("(deny file-read* (subpath \"" + inaccessiblePath + "\"))");
      }
      if (runUnderPath != null) {
        out.println("(allow file-read* (subpath \"" + runUnderPath + "\"))");
      }

      // Almost everything else is read-only.
      out.println("(deny file-write* (subpath \"/\"))");

      allowWriteSubpath(out, sandboxExecRoot);
      for (Path path : writableDirs) {
        allowWriteSubpath(out, path);
      }
    }
  }

  private void allowWriteSubpath(PrintWriter out, Path path) throws IOException {
    out.println("(allow file-write* (subpath \"" + path.getPathString() + "\"))");
    Path resolvedPath = path.resolveSymbolicLinks();
    if (!resolvedPath.equals(path)) {
      out.println("(allow file-write* (subpath \"" + resolvedPath.getPathString() + "\"))");
    }
  }

  @Override
  protected KillableObserver getCommandObserver(int timeout) {
    return (timeout >= 0) ? new TimeoutKillableObserver(timeout * 1000) : Command.NO_OBSERVER;
  }

  @Override
  protected int getSignalOnTimeout() {
    return 15; /* SIGTERM */
  }
}
