Add support for .options files to fuzzing_binary (#148)

* Add support for .options files to fuzzing_binary

.options files can be used to supply custom options to sanitizers. While
they are rarely used for C++ fuzz targets (and thus not exposed via the
cc_fuzz_test macro for now), they are necessary to run JVM fuzz targets
correctly and will be used internally by the java_fuzz_test macro in a
follow-up commit.

* Address review comments
diff --git a/docs/BUILD b/docs/BUILD
index cb9f365..9b63c93 100644
--- a/docs/BUILD
+++ b/docs/BUILD
@@ -36,6 +36,7 @@
 bzl_library(
     name = "bazel_skylib",
     srcs = [
+        "@bazel_skylib//lib:dicts",
         "@bazel_skylib//rules:common_settings",
     ],
 )
diff --git a/fuzzing/private/binary.bzl b/fuzzing/private/binary.bzl
index 904f106..f157137 100644
--- a/fuzzing/private/binary.bzl
+++ b/fuzzing/private/binary.bzl
@@ -14,6 +14,7 @@
 
 """Defines a rule for creating an instrumented fuzzing executable."""
 
+load("@bazel_skylib//lib:dicts.bzl", "dicts")
 load("//fuzzing/private:engine.bzl", "FuzzingEngineInfo")
 load(
     "//fuzzing/private:instrum_opts.bzl",
@@ -35,7 +36,12 @@
         "binary_runfiles": "The runfiles of the fuzz test executable.",
         "corpus_dir": "The directory of the corpus files used as input seeds.",
         "dictionary_file": "The dictionary file to use in fuzzing runs.",
-        "engine_info": "The `FuzzingEngineInfo` provider of the fuzzing engine used in the fuzz test.",
+        "engine_info": "The `FuzzingEngineInfo` provider of the fuzzing " +
+                       "engine used in the fuzz test.",
+        "options_file": "A file containing fuzzing engine and sanitizer " +
+                        "options to use during execution. The file loosely " +
+                        "follows the INI format and currently only applies " +
+                        "to OSS-Fuzz.",
     },
 )
 
@@ -110,6 +116,8 @@
         other_runfiles.append(ctx.file.corpus)
     if ctx.file.dictionary:
         other_runfiles.append(ctx.file.dictionary)
+    if ctx.file.options:
+        other_runfiles.append(ctx.file.options)
     return [
         DefaultInfo(
             executable = output_file,
@@ -121,9 +129,32 @@
             corpus_dir = ctx.file.corpus,
             dictionary_file = ctx.file.dictionary,
             engine_info = ctx.attr.engine[FuzzingEngineInfo],
+            options_file = ctx.file.options,
         ),
     ]
 
+_common_fuzzing_binary_attrs = {
+    "engine": attr.label(
+        doc = "The specification of the fuzzing engine used in the binary.",
+        providers = [FuzzingEngineInfo],
+        mandatory = True,
+    ),
+    "corpus": attr.label(
+        doc = "A directory of corpus files used as input seeds.",
+        allow_single_file = True,
+    ),
+    "dictionary": attr.label(
+        doc = "A dictionary file to use in fuzzing runs.",
+        allow_single_file = True,
+    ),
+    "options": attr.label(
+        doc = "A file containing fuzzing engine and sanitizer options to use " +
+              "use during execution. The file loosely follows the INI " +
+              "format and currently only applies to OSS-Fuzz.",
+        allow_single_file = True,
+    ),
+}
+
 fuzzing_binary = rule(
     implementation = _fuzzing_binary_impl,
     doc = """
@@ -138,33 +169,20 @@
  * `@rules_fuzzing//fuzzing:cc_engine_sanitizer`
  * `@rules_fuzzing//fuzzing:cc_fuzzing_build_mode`
 """,
-    attrs = {
+    attrs = dicts.add(_common_fuzzing_binary_attrs, {
         "binary": attr.label(
             executable = True,
             doc = "The fuzz test executable to instrument.",
             cfg = fuzzing_binary_transition,
             mandatory = True,
         ),
-        "engine": attr.label(
-            doc = "The specification of the fuzzing engine used in the binary.",
-            providers = [FuzzingEngineInfo],
-            mandatory = True,
-        ),
-        "corpus": attr.label(
-            doc = "A directory of corpus files used as input seeds.",
-            allow_single_file = True,
-        ),
-        "dictionary": attr.label(
-            doc = "A dictionary file to use in fuzzing runs.",
-            allow_single_file = True,
-        ),
         "_allowlist_function_transition": attr.label(
             default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
         ),
         "_instrument_binary": attr.bool(
             default = True,
         ),
-    },
+    }),
     executable = True,
     provides = [FuzzingBinaryInfo],
 )
@@ -178,30 +196,17 @@
 be incorporated in the target configuration (e.g., on the command line or the
 .bazelrc configuration file).
 """,
-    attrs = {
+    attrs = dicts.add(_common_fuzzing_binary_attrs, {
         "binary": attr.label(
             executable = True,
             doc = "The instrumented fuzz test executable.",
             cfg = "target",
             mandatory = True,
         ),
-        "engine": attr.label(
-            doc = "The specification of the fuzzing engine used in the binary.",
-            providers = [FuzzingEngineInfo],
-            mandatory = True,
-        ),
-        "corpus": attr.label(
-            doc = "A directory of corpus files used as input seeds.",
-            allow_single_file = True,
-        ),
-        "dictionary": attr.label(
-            doc = "A dictionary file to use in fuzzing runs.",
-            allow_single_file = True,
-        ),
         "_instrument_binary": attr.bool(
             default = False,
         ),
-    },
+    }),
     executable = True,
     provides = [FuzzingBinaryInfo],
 )
diff --git a/fuzzing/private/oss_fuzz/package.bzl b/fuzzing/private/oss_fuzz/package.bzl
index 6184303..16e3749 100644
--- a/fuzzing/private/oss_fuzz/package.bzl
+++ b/fuzzing/private/oss_fuzz/package.bzl
@@ -43,12 +43,16 @@
             if [[ -n "{dictionary_path}" ]]; then
                 ln -s "$(pwd)/{dictionary_path}" "$STAGING_DIR/{base_name}.dict"
             fi
+            if [[ -n "{options_path}" ]]; then
+                ln -s "$(pwd)/{options_path}" "$STAGING_DIR/{base_name}.options"
+            fi
             tar -chf "{output}" -C "$STAGING_DIR" .
         """.format(
             base_name = ctx.attr.base_name,
             binary_path = binary_info.binary_file.path,
             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,
         ),
     )