fuzztest-rust | resolve relative corpus_db paths in test environments

- Change corpus_db field in FuzzTestOptions from String to PathBuf.
- Add parse_corpus_db_path to resolve relative corpus_db paths relative to TEST_SRCDIR when running inside Bazel test sandboxes.
- Disallow empty corpus_db paths during CLI and environment parsing.

PiperOrigin-RevId: 967278954
diff --git a/rust/cargo_fuzztest/src/lib.rs b/rust/cargo_fuzztest/src/lib.rs
index e2af090..353e26f 100644
--- a/rust/cargo_fuzztest/src/lib.rs
+++ b/rust/cargo_fuzztest/src/lib.rs
@@ -447,7 +447,7 @@
         .expect("valid replay options should parse successfully");
 
         assert_eq!(parsed.fuzztest_options.replay_id.as_deref(), Some("crash_12345"));
-        assert_eq!(parsed.fuzztest_options.corpus_db.as_deref(), Some("/tmp/corpus_db"));
+        assert_eq!(parsed.fuzztest_options.corpus_db.as_deref(), Some(Path::new("/tmp/corpus_db")));
 
         let mode = parsed.execution_mode().expect("valid execution mode");
         assert_eq!(
@@ -475,7 +475,7 @@
         let options = CargoFuzzTestOptions {
             fuzztest_options: FuzzTestOptions {
                 replay_id: Some("crash_12345".to_string()),
-                corpus_db: Some("/tmp/corpus_db".to_string()),
+                corpus_db: Some("/tmp/corpus_db".into()),
                 ..Default::default()
             },
             ..Default::default()
@@ -490,7 +490,7 @@
         let options = CargoFuzzTestOptions {
             fuzztest_options: FuzzTestOptions {
                 replay_id: Some("crash_12345".to_string()),
-                corpus_db: Some("/tmp/corpus_db".to_string()),
+                corpus_db: Some("/tmp/corpus_db".into()),
                 ..Default::default()
             },
             centipede_binary_path: Some("/custom/centipede".to_string()),
@@ -531,7 +531,7 @@
         .expect("valid replay-findings options should parse successfully");
 
         assert!(parsed.fuzztest_options.replay_findings);
-        assert_eq!(parsed.fuzztest_options.corpus_db.as_deref(), Some("/tmp/corpus_db"));
+        assert_eq!(parsed.fuzztest_options.corpus_db.as_deref(), Some(Path::new("/tmp/corpus_db")));
 
         let mode = parsed.execution_mode().expect("valid execution mode");
         assert_eq!(mode, ExecutionMode::ReplayAllCrashes);
@@ -542,7 +542,7 @@
         let options = CargoFuzzTestOptions {
             fuzztest_options: FuzzTestOptions {
                 replay_findings: true,
-                corpus_db: Some("/tmp/corpus_db".to_string()),
+                corpus_db: Some("/tmp/corpus_db".into()),
                 ..Default::default()
             },
             ..Default::default()
@@ -557,7 +557,7 @@
         let options = CargoFuzzTestOptions {
             fuzztest_options: FuzzTestOptions {
                 replay_findings: true,
-                corpus_db: Some("/tmp/corpus_db".to_string()),
+                corpus_db: Some("/tmp/corpus_db".into()),
                 ..Default::default()
             },
             centipede_binary_path: Some("/custom/centipede".to_string()),
@@ -600,7 +600,7 @@
 
         assert_eq!(parsed.fuzztest_options.replay_corpus_for, Some("10s".parse().unwrap()));
         assert_eq!(parsed.fuzztest_options.time_budget_type, TimeBudgetType::PerTest);
-        assert_eq!(parsed.fuzztest_options.corpus_db.as_deref(), Some("/tmp/corpus_db"));
+        assert_eq!(parsed.fuzztest_options.corpus_db.as_deref(), Some(Path::new("/tmp/corpus_db")));
 
         let mode = parsed.execution_mode().expect("valid execution mode");
         assert_eq!(
@@ -628,7 +628,7 @@
 
         assert_eq!(parsed.fuzztest_options.replay_corpus_for, Some(RunDuration::Indefinitely));
         assert_eq!(parsed.fuzztest_options.time_budget_type, TimeBudgetType::PerTest);
-        assert_eq!(parsed.fuzztest_options.corpus_db.as_deref(), Some("/tmp/corpus_db"));
+        assert_eq!(parsed.fuzztest_options.corpus_db.as_deref(), Some(Path::new("/tmp/corpus_db")));
 
         let mode = parsed.execution_mode().expect("valid execution mode");
         assert_eq!(
@@ -700,7 +700,7 @@
         let options = CargoFuzzTestOptions {
             fuzztest_options: FuzzTestOptions {
                 replay_corpus_for: Some("10s".parse().unwrap()),
-                corpus_db: Some("/tmp/corpus_db".to_string()),
+                corpus_db: Some("/tmp/corpus_db".into()),
                 ..Default::default()
             },
             ..Default::default()
@@ -716,7 +716,7 @@
             fuzztest_options: FuzzTestOptions {
                 replay_corpus_for: Some("10s".parse().unwrap()),
                 time_budget_type: TimeBudgetType::Total,
-                corpus_db: Some("/tmp/corpus_db".to_string()),
+                corpus_db: Some("/tmp/corpus_db".into()),
                 ..Default::default()
             },
             centipede_binary_path: Some("/custom/centipede".to_string()),
@@ -765,7 +765,7 @@
             parsed.fuzztest_options.list_crash_ids_file.as_deref(),
             Some("/tmp/crashes.txt")
         );
-        assert_eq!(parsed.fuzztest_options.corpus_db.as_deref(), Some("/tmp/corpus_db"));
+        assert_eq!(parsed.fuzztest_options.corpus_db.as_deref(), Some(Path::new("/tmp/corpus_db")));
 
         let mode = parsed.execution_mode().expect("valid execution mode");
         assert_eq!(
@@ -797,7 +797,7 @@
             fuzztest_options: FuzzTestOptions {
                 list_crash_ids: true,
                 list_crash_ids_file: Some("/tmp/crashes.txt".to_string()),
-                corpus_db: Some("/tmp/corpus_db".to_string()),
+                corpus_db: Some("/tmp/corpus_db".into()),
                 ..Default::default()
             },
             ..Default::default()
@@ -813,7 +813,7 @@
             fuzztest_options: FuzzTestOptions {
                 list_crash_ids: true,
                 list_crash_ids_file: Some("/tmp/crashes.txt".to_string()),
-                corpus_db: Some("/tmp/corpus_db".to_string()),
+                corpus_db: Some("/tmp/corpus_db".into()),
                 ..Default::default()
             },
             centipede_binary_path: Some("/custom/centipede".to_string()),
@@ -850,7 +850,7 @@
             fuzztest_options: FuzzTestOptions {
                 replay_corpus_for: Some(RunDuration::Indefinitely),
                 time_budget_type: TimeBudgetType::Total,
-                corpus_db: Some("/tmp/corpus_db".to_string()),
+                corpus_db: Some("/tmp/corpus_db".into()),
                 ..Default::default()
             },
             centipede_binary_path: Some("/custom/centipede".to_string()),
diff --git a/rust/cargo_fuzztest/tests/runner_test.rs b/rust/cargo_fuzztest/tests/runner_test.rs
index 14bd430..a7c81c9 100644
--- a/rust/cargo_fuzztest/tests/runner_test.rs
+++ b/rust/cargo_fuzztest/tests/runner_test.rs
@@ -147,7 +147,7 @@
     let binary_path = get_sample_test_bin_path("sample_fuzz_crate");
     let fuzztest_options = FuzzTestOptions {
         replay_id: Some("crash_12345".to_string()),
-        corpus_db: Some("/custom/path/to/corpus_db".to_string()),
+        corpus_db: Some("/custom/path/to/corpus_db".into()),
         ..Default::default()
     };
     let options = CargoFuzzTestOptions {
@@ -194,7 +194,7 @@
 fn test_execution_mode_replay_id_missing_centipede_binary_path_errors() {
     let fuzztest_options = FuzzTestOptions {
         replay_id: Some("crash_12345".to_string()),
-        corpus_db: Some("/custom/path/to/corpus_db".to_string()),
+        corpus_db: Some("/custom/path/to/corpus_db".into()),
         ..Default::default()
     };
     let options = CargoFuzzTestOptions { fuzztest_options, ..Default::default() };
@@ -209,7 +209,7 @@
     let binary_path = get_sample_test_bin_path("sample_fuzz_crate");
     let fuzztest_options = FuzzTestOptions {
         replay_findings: true,
-        corpus_db: Some("/custom/path/to/corpus_db".to_string()),
+        corpus_db: Some("/custom/path/to/corpus_db".into()),
         ..Default::default()
     };
     let options = CargoFuzzTestOptions {
@@ -239,7 +239,7 @@
 fn test_execution_mode_replay_findings_missing_centipede_binary_path_errors() {
     let fuzztest_options = FuzzTestOptions {
         replay_findings: true,
-        corpus_db: Some("/custom/path/to/corpus_db".to_string()),
+        corpus_db: Some("/custom/path/to/corpus_db".into()),
         ..Default::default()
     };
     let options = CargoFuzzTestOptions { fuzztest_options, ..Default::default() };
@@ -255,7 +255,7 @@
     let fuzztest_options = FuzzTestOptions {
         replay_corpus_for: Some("10s".parse().expect("valid duration")),
         time_budget_type: TimeBudgetType::Total,
-        corpus_db: Some("/custom/path/to/corpus_db".to_string()),
+        corpus_db: Some("/custom/path/to/corpus_db".into()),
         ..Default::default()
     };
     let options = CargoFuzzTestOptions {
@@ -292,7 +292,7 @@
     let fuzztest_options = FuzzTestOptions {
         replay_corpus_for: Some(RunDuration::Indefinitely),
         time_budget_type: TimeBudgetType::Total,
-        corpus_db: Some("/custom/path/to/corpus_db".to_string()),
+        corpus_db: Some("/custom/path/to/corpus_db".into()),
         ..Default::default()
     };
     let options = CargoFuzzTestOptions {
@@ -327,7 +327,7 @@
 fn test_execution_mode_replay_corpus_missing_centipede_binary_path_errors() {
     let fuzztest_options = FuzzTestOptions {
         replay_corpus_for: Some("10s".parse().expect("valid duration")),
-        corpus_db: Some("/custom/path/to/corpus_db".to_string()),
+        corpus_db: Some("/custom/path/to/corpus_db".into()),
         ..Default::default()
     };
     let options = CargoFuzzTestOptions { fuzztest_options, ..Default::default() };
@@ -359,7 +359,7 @@
     let fuzztest_options = FuzzTestOptions {
         list_crash_ids: true,
         list_crash_ids_file: Some("/custom/path/to/crash_ids.txt".to_string()),
-        corpus_db: Some("/custom/path/to/corpus_db".to_string()),
+        corpus_db: Some("/custom/path/to/corpus_db".into()),
         ..Default::default()
     };
     let options = CargoFuzzTestOptions {
@@ -412,7 +412,7 @@
     let fuzztest_options = FuzzTestOptions {
         list_crash_ids: true,
         list_crash_ids_file: Some("/custom/path/to/crash_ids.txt".to_string()),
-        corpus_db: Some("/custom/path/to/corpus_db".to_string()),
+        corpus_db: Some("/custom/path/to/corpus_db".into()),
         ..Default::default()
     };
     let options = CargoFuzzTestOptions { fuzztest_options, ..Default::default() };
diff --git a/rust/options/src/lib.rs b/rust/options/src/lib.rs
index 93f9cc4..5abe89c 100644
--- a/rust/options/src/lib.rs
+++ b/rust/options/src/lib.rs
@@ -20,11 +20,31 @@
 
 use anyhow::Context;
 use clap::{Parser, ValueEnum};
+use std::env;
 use std::fmt;
 use std::fmt::Display;
 use std::fmt::Formatter;
+use std::path::{Path, PathBuf};
 use std::str::FromStr;
 
+/// If a relative path is provided while executing inside a Bazel/Blaze test sandbox
+/// (detected via the `TEST_SRCDIR` environment variable), the path is automatically
+/// resolved relative to the runfiles root directory.
+fn parse_corpus_db_path(raw: &str) -> Result<PathBuf, String> {
+    if raw.is_empty() {
+        return Err("corpus_db path cannot be empty".to_string());
+    }
+
+    let path = PathBuf::from(raw);
+    if path.is_absolute() {
+        Ok(path)
+    } else if let Ok(test_src_dir) = env::var("TEST_SRCDIR") {
+        Ok(Path::new(&test_src_dir).join(path))
+    } else {
+        Ok(path)
+    }
+}
+
 /// Time budget calculation type for replay corpus mode.
 #[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Default)]
 pub enum TimeBudgetType {
@@ -100,8 +120,8 @@
     ///
     /// If set to non-empty, updates/queries the corpus database that contains coverage,
     /// regression, and crashing inputs for each test binary and fuzz test.
-    #[arg(env = "FUZZTEST_CORPUS_DB", long)]
-    pub corpus_db: Option<String>,
+    #[arg(env = "FUZZTEST_CORPUS_DB", long, value_parser = parse_corpus_db_path)]
+    pub corpus_db: Option<PathBuf>,
 }
 
 /// Strongly-typed domain execution mode for test runs.
@@ -316,7 +336,7 @@
         let options =
             result.expect("parsing should succeed when both replay_id and corpus_db are present");
         expect_that!(options.replay_id.as_deref(), eq(Some("my_crash_123")));
-        expect_that!(options.corpus_db.as_deref(), eq(Some("/tmp/corpus_db")));
+        expect_that!(options.corpus_db.as_deref(), eq(Some(Path::new("/tmp/corpus_db"))));
     }
 
     #[gtest]
@@ -419,8 +439,8 @@
     fn test_replay_findings_with_corpus_db_succeeds() {
         // SAFETY: Testing environment parsing in single-threaded context.
         unsafe {
-            std::env::set_var("FUZZTEST_REPLAY_FINDINGS", "true");
-            std::env::set_var("FUZZTEST_CORPUS_DB", "/tmp/corpus_db");
+            env::set_var("FUZZTEST_REPLAY_FINDINGS", "true");
+            env::set_var("FUZZTEST_CORPUS_DB", "/tmp/corpus_db");
         }
 
         let result = FuzzTestOptions::try_parse_from(std::iter::empty::<OsString>());
@@ -434,7 +454,7 @@
         let options = result
             .expect("parsing should succeed when both replay_findings and corpus_db are present");
         expect_that!(options.replay_findings, eq(true));
-        expect_that!(options.corpus_db.as_deref(), eq(Some("/tmp/corpus_db")));
+        expect_that!(options.corpus_db.as_deref(), eq(Some(Path::new("/tmp/corpus_db"))));
     }
 
     #[gtest]
@@ -478,7 +498,7 @@
             options.replay_corpus_for,
             eq(Some("10s".parse().expect("valid duration string")))
         );
-        expect_that!(options.corpus_db.as_deref(), eq(Some("/tmp/corpus_db")));
+        expect_that!(options.corpus_db.as_deref(), eq(Some(Path::new("/tmp/corpus_db"))));
         expect_that!(options.time_budget_type, eq(TimeBudgetType::PerTest));
     }
 
@@ -502,7 +522,7 @@
             "parsing should succeed when replay_corpus_for is inf and corpus_db is present",
         );
         expect_that!(options.replay_corpus_for, eq(Some(RunDuration::Indefinitely)));
-        expect_that!(options.corpus_db.as_deref(), eq(Some("/tmp/corpus_db")));
+        expect_that!(options.corpus_db.as_deref(), eq(Some(Path::new("/tmp/corpus_db"))));
     }
 
     #[gtest]
@@ -525,7 +545,7 @@
             "parsing should succeed when replay_corpus_for is infinity and corpus_db is present",
         );
         expect_that!(options.replay_corpus_for, eq(Some(RunDuration::Indefinitely)));
-        expect_that!(options.corpus_db.as_deref(), eq(Some("/tmp/corpus_db")));
+        expect_that!(options.corpus_db.as_deref(), eq(Some(Path::new("/tmp/corpus_db"))));
     }
 
     #[gtest]
@@ -551,7 +571,7 @@
             options.replay_corpus_for,
             eq(Some("10s".parse().expect("valid duration string")))
         );
-        expect_that!(options.corpus_db.as_deref(), eq(Some("/tmp/corpus_db")));
+        expect_that!(options.corpus_db.as_deref(), eq(Some(Path::new("/tmp/corpus_db"))));
         expect_that!(options.time_budget_type, eq(TimeBudgetType::Total));
     }
 
@@ -575,7 +595,7 @@
 
         let options = result.expect("parsing should succeed with total time budget type and inf");
         expect_that!(options.replay_corpus_for, eq(Some(RunDuration::Indefinitely)));
-        expect_that!(options.corpus_db.as_deref(), eq(Some("/tmp/corpus_db")));
+        expect_that!(options.corpus_db.as_deref(), eq(Some(Path::new("/tmp/corpus_db"))));
         expect_that!(options.time_budget_type, eq(TimeBudgetType::Total));
     }
 
@@ -665,7 +685,7 @@
         );
         expect_that!(options.list_crash_ids, eq(true));
         expect_that!(options.list_crash_ids_file.as_deref(), eq(Some("/tmp/crashes.txt")));
-        expect_that!(options.corpus_db.as_deref(), eq(Some("/tmp/corpus_db")));
+        expect_that!(options.corpus_db.as_deref(), eq(Some(Path::new("/tmp/corpus_db"))));
 
         let mode = ExecutionMode::from_fuzztest_options(&options);
         expect_that!(
diff --git a/rust/src/options.rs b/rust/src/options.rs
index 41abaf5..b5c61d2 100644
--- a/rust/src/options.rs
+++ b/rust/src/options.rs
@@ -340,11 +340,19 @@
 
     if let Some(corpus_db) = corpus_db {
         if let Some(workdir_root) = workdir_root {
-            Ok((Some(corpus_db), Some(workdir_root), None))
+            Ok((
+                Some(corpus_db.to_str().context("converting corpus_db to str")?),
+                Some(workdir_root),
+                None,
+            ))
         } else {
             let temp_dir =
                 TempDir::new().context("while attempting to create temporary working directory")?;
-            Ok((Some(corpus_db), None, Some(temp_dir)))
+            Ok((
+                Some(corpus_db.to_str().context("converting corpus_db to str")?),
+                None,
+                Some(temp_dir),
+            ))
         }
     } else {
         if workdir_root.is_some() {
@@ -549,7 +557,7 @@
     fn test_determine_execution_action_replay_crash_id() {
         let options = FuzzTestOptions {
             replay_id: Some("my_crash_123".to_string()),
-            corpus_db: Some("/tmp/corpus_db".to_string()),
+            corpus_db: Some("/tmp/corpus_db".into()),
             ..Default::default()
         };
         let action = determine_execution_action_internal(&options, "my_mod::my_test");
@@ -572,7 +580,7 @@
     fn test_determine_execution_action_replay_findings() {
         let options = FuzzTestOptions {
             replay_findings: true,
-            corpus_db: Some("/tmp/corpus_db".to_string()),
+            corpus_db: Some("/tmp/corpus_db".into()),
             ..Default::default()
         };
         let action = determine_execution_action_internal(&options, "my_mod::my_test");
@@ -730,7 +738,7 @@
     #[gtest]
     fn test_get_corpusdb_and_workdir_succeeds_with_corpusdb_and_workdir_root() -> Result<()> {
         let options = FuzzTestOptions {
-            corpus_db: Some("/tmp/db".to_string()),
+            corpus_db: Some("/tmp/db".into()),
             workdir_root: Some("/tmp/root".to_string()),
             ..Default::default()
         };
@@ -744,8 +752,7 @@
 
     #[gtest]
     fn test_get_corpusdb_and_workdir_succeeds_with_corpusdb_only() -> Result<()> {
-        let options =
-            FuzzTestOptions { corpus_db: Some("/tmp/db".to_string()), ..Default::default() };
+        let options = FuzzTestOptions { corpus_db: Some("/tmp/db".into()), ..Default::default() };
         let (corpus_db, workdir_root, workdir) =
             get_corpusdb_and_workdir_root_and_workdir(&options).or_fail()?;
         expect_that!(corpus_db, eq(Some("/tmp/db")));
@@ -792,7 +799,7 @@
         let options =
             result.expect("parsing should succeed when both replay_id and corpus_db are present");
         expect_that!(options.replay_id.as_deref(), eq(Some("my_crash_123")));
-        expect_that!(options.corpus_db.as_deref(), eq(Some("/tmp/corpus_db")));
+        expect_that!(options.corpus_db.as_deref(), eq(Some(Path::new("/tmp/corpus_db"))));
     }
 
     #[gtest]
@@ -826,7 +833,7 @@
         let duration = "1s".parse().expect("fixed test string should parse as duration");
         let options = FuzzTestOptions {
             fuzz_for: Some(duration),
-            corpus_db: Some("/tmp/corpus_db".to_string()),
+            corpus_db: Some("/tmp/corpus_db".into()),
             ..Default::default()
         };
         let action = determine_execution_action_internal(&options, "my_mod::my_test");
@@ -851,7 +858,7 @@
         let options = FuzzTestOptions {
             list_crash_ids: true,
             list_crash_ids_file: Some("/tmp/custom_crashes.txt".to_string()),
-            corpus_db: Some("/tmp/corpus_db".to_string()),
+            corpus_db: Some("/tmp/corpus_db".into()),
             ..Default::default()
         };
         let action = determine_execution_action_internal(&options, "my_mod::my_test");