Add full runfiles tree support (#149)

* Add full runfiles tree support

The runfiles tree of a fuzzing_binary is now packaged correctly for
OSS-Fuzz.

For a fuzz test foo, the runfiles are resolved relative to the
foo.runfiles directory in the output .tar and will be found by the Bazel
runfiles libraries.

This commit also adds a fuzz test that crashes if it can't find its
runfile.

* Optional: Add symlink to fuzz test binary to runfiles tree

* Address review comments
diff --git a/docs/BUILD b/docs/BUILD
index 9b63c93..2fc5a86 100644
--- a/docs/BUILD
+++ b/docs/BUILD
@@ -37,6 +37,7 @@
     name = "bazel_skylib",
     srcs = [
         "@bazel_skylib//lib:dicts",
+        "@bazel_skylib//lib:paths",
         "@bazel_skylib//rules:common_settings",
     ],
 )
@@ -54,6 +55,7 @@
         "//fuzzing/private:instrum_opts.bzl",
         "//fuzzing/private:java_utils.bzl",
         "//fuzzing/private:regression.bzl",
+        "//fuzzing/private:util.bzl",
         "//fuzzing/private/oss_fuzz:package.bzl",
         "@rules_fuzzing_oss_fuzz//:instrum.bzl",
     ],
diff --git a/examples/BUILD b/examples/BUILD
index 02e7e93..6f834e1 100644
--- a/examples/BUILD
+++ b/examples/BUILD
@@ -111,3 +111,14 @@
         "@re2",
     ],
 )
+
+cc_fuzz_test(
+    name = "runfiles_fuzz_test",
+    srcs = ["runfiles_fuzz_test.cc"],
+    data = [
+        ":corpus_0.txt",
+    ],
+    deps = [
+        "@bazel_tools//tools/cpp/runfiles",
+    ],
+)
diff --git a/examples/runfiles_fuzz_test.cc b/examples/runfiles_fuzz_test.cc
new file mode 100644
index 0000000..2383946
--- /dev/null
+++ b/examples/runfiles_fuzz_test.cc
@@ -0,0 +1,47 @@
+// Copyright 2021 Google LLC
+//
+// 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
+//
+//    https://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.
+
+// A fuzz target that exits if it doesn't find a declared runfile.
+
+#include <cstddef>
+#include <cstdint>
+
+#include <fstream>
+#include <iostream>
+#include <string>
+
+#include "tools/cpp/runfiles/runfiles.h"
+
+using ::bazel::tools::cpp::runfiles::Runfiles;
+
+namespace {
+  Runfiles *runfiles = nullptr;
+}
+
+extern "C" void LLVMFuzzerInitialize(int *argc, char ***argv) {
+  std::string error;
+  runfiles = Runfiles::Create((*argv)[0], &error);
+  if (runfiles == nullptr) {
+    std::cerr << error;
+    abort();
+  }
+}
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
+  std::string path = runfiles->Rlocation("rules_fuzzing/examples/corpus_0.txt");
+  if (path.empty()) abort();
+  std::ifstream in(path);
+  if (!in.good()) abort();
+  return 0;
+}
diff --git a/fuzzing/private/BUILD b/fuzzing/private/BUILD
index 6f59218..40dda3f 100644
--- a/fuzzing/private/BUILD
+++ b/fuzzing/private/BUILD
@@ -26,6 +26,7 @@
     "instrum_opts.bzl",
     "java_utils.bzl",
     "regression.bzl",
+    "util.bzl",
 ])
 
 # Config settings needed for prebuilt engines.
diff --git a/fuzzing/private/oss_fuzz/package.bzl b/fuzzing/private/oss_fuzz/package.bzl
index 16e3749..05510c8 100644
--- a/fuzzing/private/oss_fuzz/package.bzl
+++ b/fuzzing/private/oss_fuzz/package.bzl
@@ -15,26 +15,50 @@
 """Rule for packaging fuzz tests in the expected OSS-Fuzz format."""
 
 load("//fuzzing/private:binary.bzl", "FuzzingBinaryInfo")
+load("//fuzzing/private:util.bzl", "runfile_path")
 
 def _oss_fuzz_package_impl(ctx):
     output_archive = ctx.actions.declare_file(ctx.label.name + ".tar")
     binary_info = ctx.attr.binary[FuzzingBinaryInfo]
 
-    action_inputs = [binary_info.binary_file]
+    binary_runfiles = binary_info.binary_runfiles.files.to_list()
+    archive_inputs = binary_runfiles
+
+    runfiles_manifest = ctx.actions.declare_file(ctx.label.name + "_runfiles")
+    runfiles_manifest_content = "".join([
+        "{runfile_path} {real_path}\n".format(
+            real_path = runfile.path,
+            runfile_path = runfile_path(ctx, runfile),
+        )
+        # In order not to duplicate the fuzz test binary, it is excluded from
+        # the runfiles here. A symlink from the runfiles tree to the binary in
+        # the top-level directory is added further below.
+        for runfile in binary_runfiles
+        if runfile != binary_info.binary_file
+    ])
+    ctx.actions.write(runfiles_manifest, runfiles_manifest_content, False)
+    archive_inputs.append(runfiles_manifest)
+
     if binary_info.corpus_dir:
-        action_inputs.append(binary_info.corpus_dir)
+        archive_inputs.append(binary_info.corpus_dir)
     if binary_info.dictionary_file:
-        action_inputs.append(binary_info.dictionary_file)
+        archive_inputs.append(binary_info.dictionary_file)
     ctx.actions.run_shell(
         outputs = [output_archive],
-        inputs = action_inputs,
+        inputs = archive_inputs,
         command = """
+            set -e
             declare -r STAGING_DIR="$(mktemp --directory -t oss-fuzz-pkg.XXXXXXXXXX)"
             function cleanup() {{
                 rm -rf "$STAGING_DIR"
             }}
             trap cleanup EXIT
             ln -s "$(pwd)/{binary_path}" "$STAGING_DIR/{base_name}"
+            while IFS= read -r line; do
+              IFS=' ' read -r link target <<< "$line"
+              mkdir -p "$(dirname "$STAGING_DIR/{binary_runfiles_dir}/$link")"
+              ln -s "$(pwd)/$target" "$STAGING_DIR/{binary_runfiles_dir}/$link"
+            done <{runfiles_manifest_path}
             if [[ -n "{corpus_dir}" ]]; then
                 pushd "{corpus_dir}" >/dev/null
                 zip --quiet -r "$STAGING_DIR/{base_name}_seed_corpus.zip" ./*
@@ -47,13 +71,22 @@
                 ln -s "$(pwd)/{options_path}" "$STAGING_DIR/{base_name}.options"
             fi
             tar -chf "{output}" -C "$STAGING_DIR" .
+            # Add a relative symlink to the fuzz test binary to its runfiles.
+            declare -r BINARY_RUNFILES_PATH="$STAGING_DIR/{binary_runfiles_dir}/{binary_runfile_path}"
+            declare -r BINARY_RELATIVE_PATH="$(realpath -m -s --relative-to="$(dirname $BINARY_RUNFILES_PATH)" "$STAGING_DIR/{base_name}")"
+            mkdir -p "$(dirname "$BINARY_RUNFILES_PATH")"
+            ln -s "$BINARY_RELATIVE_PATH" "$BINARY_RUNFILES_PATH"
+            tar -rf "{output}" -C "$STAGING_DIR" "./{binary_runfiles_dir}/{binary_runfile_path}"
         """.format(
             base_name = ctx.attr.base_name,
             binary_path = binary_info.binary_file.path,
+            binary_runfile_path = runfile_path(ctx, binary_info.binary_file),
+            binary_runfiles_dir = ctx.attr.base_name + ".runfiles",
             corpus_dir = binary_info.corpus_dir.path if binary_info.corpus_dir else "",
             dictionary_path = binary_info.dictionary_file.path if binary_info.dictionary_file else "",
             options_path = binary_info.options_file.path if binary_info.options_file else "",
             output = output_archive.path,
+            runfiles_manifest_path = runfiles_manifest.path,
         ),
     )
     return [DefaultInfo(files = depset([output_archive]))]
@@ -62,9 +95,6 @@
     implementation = _oss_fuzz_package_impl,
     doc = """
 Packages a fuzz test in a TAR archive compatible with the OSS-Fuzz format.
-
-> NOTE: The current implementation does not yet support packaging the
-> binary runfiles.
 """,
     attrs = {
         "binary": attr.label(
diff --git a/fuzzing/private/util.bzl b/fuzzing/private/util.bzl
index f5a869f..4fb0db5 100644
--- a/fuzzing/private/util.bzl
+++ b/fuzzing/private/util.bzl
@@ -14,6 +14,8 @@
 
 """Miscellaneous utilities."""
 
+load("@bazel_skylib//lib:paths.bzl", "paths")
+
 def _generate_file_impl(ctx):
     ctx.actions.write(ctx.outputs.output, ctx.attr.contents)
 
@@ -33,3 +35,8 @@
         ),
     },
 )
+
+# Returns the path of a runfile that can be used to look up its absolute path
+# via the rlocation function provided by Bazel's runfiles libraries.
+def runfile_path(ctx, runfile):
+    return paths.normalize(ctx.workspace_name + "/" + runfile.short_path)