fuzztest-rust | support replaying corpus for inf

- Introduce a Duration enum with Indefinitely and Fixed variants in fuzztest_options.
- Replace FuzzFor with Duration across fuzztest_options, fuzztest, and cargo-fuzztest.
- Implement FromStr and Display on Duration to parse "inf", "infinite", and human-readable durations.
- Update replay_corpus_for option to support indefinite replay.
- In Centipede argument builder, omit time limit flag when replaying or fuzzing indefinitely.
- Add unit tests for Duration parsing, formatting, Centipede arguments, and cargo-fuzztest commands.

PiperOrigin-RevId: 966118187
diff --git a/rust/cargo_fuzztest/src/lib.rs b/rust/cargo_fuzztest/src/lib.rs
index b7c57eb..e2af090 100644
--- a/rust/cargo_fuzztest/src/lib.rs
+++ b/rust/cargo_fuzztest/src/lib.rs
@@ -18,8 +18,8 @@
 use anyhow::{Context, Result};
 use clap::Parser;
 pub use fuzztest_options::{
-    ExecutionMode, FuzzFor, FuzzOptions, FuzzTestOptions, ListCrashIdsOptions, ReplayCorpusOptions,
-    ReplayCrashOptions, TimeBudgetType,
+    ExecutionMode, FuzzOptions, FuzzTestOptions, ListCrashIdsOptions, ReplayCorpusOptions,
+    ReplayCrashOptions, RunDuration, TimeBudgetType,
 };
 use std::env;
 use std::ffi::OsString;
@@ -95,7 +95,10 @@
                 if self.test_path.is_some() {
                     self.check_centipede_binary_path_is_set()?;
                     ExecutionMode::Fuzz(FuzzOptions {
-                        fuzz_for: self.fuzztest_options.fuzz_for.unwrap_or(FuzzFor::Indefinitely),
+                        fuzz_for: self
+                            .fuzztest_options
+                            .fuzz_for
+                            .unwrap_or(RunDuration::Indefinitely),
                         jobs: self.fuzztest_options.jobs,
                     })
                 } else {
@@ -246,14 +249,7 @@
 
             ExecutionMode::Fuzz(fuzz_options) => {
                 let FuzzOptions { fuzz_for, jobs } = fuzz_options;
-                match fuzz_for {
-                    FuzzFor::Indefinitely => {
-                        cmd.env("FUZZTEST_FUZZ_FOR", "inf");
-                    }
-                    FuzzFor::Duration(duration) => {
-                        cmd.env("FUZZTEST_FUZZ_FOR", duration.to_string());
-                    }
-                }
+                cmd.env("FUZZTEST_FUZZ_FOR", fuzz_for.to_string());
                 if let Some(jobs) = jobs {
                     cmd.env("FUZZTEST_JOBS", jobs.to_string());
                 }
@@ -610,7 +606,60 @@
         assert_eq!(
             mode,
             ExecutionMode::ReplayCorpus(ReplayCorpusOptions {
-                replay_corpus_for: "10s".parse().unwrap(),
+                replay_corpus_for: "10s".parse().expect("valid duration string"),
+                time_budget_type: TimeBudgetType::PerTest,
+                jobs: None,
+            })
+        );
+    }
+
+    #[gtest]
+    fn test_cli_option_parsing_replay_corpus_for_inf() {
+        let parsed = CargoFuzzTestOptions::try_parse_from([
+            "cargo-fuzztest",
+            "--replay-corpus-for",
+            "inf",
+            "--corpus-db",
+            "/tmp/corpus_db",
+            "--centipede-binary-path",
+            "/custom/centipede",
+        ])
+        .expect("valid replay-corpus-for inf should parse successfully");
+
+        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"));
+
+        let mode = parsed.execution_mode().expect("valid execution mode");
+        assert_eq!(
+            mode,
+            ExecutionMode::ReplayCorpus(ReplayCorpusOptions {
+                replay_corpus_for: RunDuration::Indefinitely,
+                time_budget_type: TimeBudgetType::PerTest,
+                jobs: None,
+            })
+        );
+    }
+
+    #[gtest]
+    fn test_cli_option_parsing_replay_corpus_for_infinity() {
+        let parsed = CargoFuzzTestOptions::try_parse_from([
+            "cargo-fuzztest",
+            "--replay-corpus-for",
+            "infinity",
+            "--corpus-db",
+            "/tmp/corpus_db",
+            "--centipede-binary-path",
+            "/custom/centipede",
+        ])
+        .expect("valid replay-corpus-for infinity should parse successfully");
+
+        assert_eq!(parsed.fuzztest_options.replay_corpus_for, Some(RunDuration::Indefinitely));
+        let mode = parsed.execution_mode().expect("valid execution mode");
+        assert_eq!(
+            mode,
+            ExecutionMode::ReplayCorpus(ReplayCorpusOptions {
+                replay_corpus_for: RunDuration::Indefinitely,
                 time_budget_type: TimeBudgetType::PerTest,
                 jobs: None,
             })
@@ -794,4 +843,40 @@
             Some("/custom/centipede".to_string())
         )));
     }
+
+    #[gtest]
+    fn test_build_run_command_replay_corpus_indefinite() {
+        let options = CargoFuzzTestOptions {
+            fuzztest_options: FuzzTestOptions {
+                replay_corpus_for: Some(RunDuration::Indefinitely),
+                time_budget_type: TimeBudgetType::Total,
+                corpus_db: Some("/tmp/corpus_db".to_string()),
+                ..Default::default()
+            },
+            centipede_binary_path: Some("/custom/centipede".to_string()),
+            ..Default::default()
+        };
+        let runner = FuzztestRunner::new("x86_64-unknown-linux-gnu".to_string(), options);
+        let cmd =
+            runner.build_run_command(Path::new("/tmp/test_bin")).expect("should build run command");
+
+        let envs: Vec<(String, Option<String>)> = cmd
+            .get_envs()
+            .map(|(k, v)| {
+                (k.to_string_lossy().to_string(), v.map(|s| s.to_string_lossy().to_string()))
+            })
+            .collect();
+
+        assert!(envs.contains(&("FUZZTEST_REPLAY_CORPUS_FOR".to_string(), Some("inf".to_string()))));
+        assert!(
+            envs.contains(&("FUZZTEST_TIME_BUDGET_TYPE".to_string(), Some("total".to_string())))
+        );
+        assert!(
+            envs.contains(&("FUZZTEST_CORPUS_DB".to_string(), Some("/tmp/corpus_db".to_string())))
+        );
+        assert!(envs.contains(&(
+            "FUZZTEST_CENTIPEDE_BINARY_PATH".to_string(),
+            Some("/custom/centipede".to_string())
+        )));
+    }
 }
diff --git a/rust/cargo_fuzztest/tests/e2e_cli_test.rs b/rust/cargo_fuzztest/tests/e2e_cli_test.rs
index b77256f..04ed647 100644
--- a/rust/cargo_fuzztest/tests/e2e_cli_test.rs
+++ b/rust/cargo_fuzztest/tests/e2e_cli_test.rs
@@ -365,7 +365,7 @@
     // 2. Run cargo-fuzztest CLI with --replay-corpus-for and --time-budget-type total.
     let mut cmd = setup_cargo_fuzztest_command(&sample_crate_path, temp_target_dir.path());
     cmd.arg("__fuzztest_mod__sample_fuzztest_target::sample_fuzztest_target")
-        .arg("--replay-corpus-for=3s")
+        .arg("--replay-corpus-for=4.5s")
         .arg("--time-budget-type=total")
         .arg("--corpus-db")
         .arg(temp_db_dir.path())
@@ -376,12 +376,15 @@
     let stderr_str = String::from_utf8_lossy(&output.stderr);
     let stdout_str = String::from_utf8_lossy(&output.stdout);
 
+    eprintln!("tmp:: stderr:\n{stderr_str}");
+    eprintln!("tmp:: stdout:\n{stdout_str}");
+
     expect_true!(output.status.success());
     expect_true!(
         stderr_str.contains(
-            "Replaying __fuzztest_mod__sample_fuzztest_target.sample_fuzztest_target for 1s"
+            "Replaying __fuzztest_mod__sample_fuzztest_target.sample_fuzztest_target for 1.5s"
         ) || stdout_str.contains(
-            "Replaying __fuzztest_mod__sample_fuzztest_target.sample_fuzztest_target for 1s"
+            "Replaying __fuzztest_mod__sample_fuzztest_target.sample_fuzztest_target for 1.5s"
         )
     );
 }
@@ -409,7 +412,7 @@
     let mut cmd = setup_cargo_fuzztest_command(&sample_crate_path, temp_target_dir.path());
     cmd.arg(test_target)
         .arg("--fuzz-for=5s")
-        .env_remove("CENTIPEDE_BINARY_PATH")
+        .env_remove("FUZZTEST_CENTIPEDE_BINARY_PATH")
         .arg("--centipede-binary-path")
         .arg(&centipede_bin)
         .arg("--corpus-db")
diff --git a/rust/cargo_fuzztest/tests/runner_test.rs b/rust/cargo_fuzztest/tests/runner_test.rs
index b865b1d..14bd430 100644
--- a/rust/cargo_fuzztest/tests/runner_test.rs
+++ b/rust/cargo_fuzztest/tests/runner_test.rs
@@ -2,7 +2,7 @@
 
 use cargo_fuzztest::{CargoFuzzTestOptions, FuzztestRunner};
 use common::get_sample_test_bin_path;
-use fuzztest_options::{FuzzFor, FuzzTestOptions, TimeBudgetType};
+use fuzztest_options::{FuzzTestOptions, RunDuration, TimeBudgetType};
 use googletest::prelude::*;
 
 #[gtest]
@@ -45,10 +45,8 @@
 #[gtest]
 fn test_runner_build_run_command_with_duration() {
     let binary_path = get_sample_test_bin_path("sample_fuzz_crate");
-    let fuzztest_options = FuzzTestOptions {
-        fuzz_for: Some(FuzzFor::Duration("5s".parse().unwrap())),
-        ..Default::default()
-    };
+    let fuzztest_options =
+        FuzzTestOptions { fuzz_for: Some("5s".parse().unwrap()), ..Default::default() };
     let options = CargoFuzzTestOptions {
         fuzztest_options,
         centipede_binary_path: Some("/custom/path/to/centipede".to_string()),
@@ -68,7 +66,7 @@
 fn test_runner_build_run_command_with_indefinitely() {
     let binary_path = get_sample_test_bin_path("sample_fuzz_crate");
     let fuzztest_options =
-        FuzzTestOptions { fuzz_for: Some(FuzzFor::Indefinitely), ..Default::default() };
+        FuzzTestOptions { fuzz_for: Some(RunDuration::Indefinitely), ..Default::default() };
     let options = CargoFuzzTestOptions {
         fuzztest_options,
         centipede_binary_path: Some("/custom/path/to/centipede".to_string()),
@@ -109,7 +107,7 @@
     let binary_path = get_sample_test_bin_path("sample_fuzz_crate");
     let fuzztest_options = FuzzTestOptions {
         jobs: Some(4),
-        fuzz_for: Some(FuzzFor::Duration("10s".parse().expect("static valid duration string"))),
+        fuzz_for: Some("10s".parse().expect("static valid duration string")),
         ..Default::default()
     };
     let options = CargoFuzzTestOptions {
@@ -289,6 +287,43 @@
 }
 
 #[gtest]
+fn test_runner_build_run_command_with_replay_corpus_indefinitely() {
+    let binary_path = get_sample_test_bin_path("sample_fuzz_crate");
+    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()),
+        ..Default::default()
+    };
+    let options = CargoFuzzTestOptions {
+        fuzztest_options,
+        centipede_binary_path: Some("/custom/path/to/centipede".to_string()),
+        ..Default::default()
+    };
+    let runner = FuzztestRunner::new("x86_64-unknown-linux-gnu".to_string(), options);
+    let cmd = runner.build_run_command(&binary_path).expect("valid run command");
+
+    let envs: Vec<(String, Option<String>)> = cmd
+        .get_envs()
+        .map(|(k, v)| (k.to_string_lossy().to_string(), v.map(|s| s.to_string_lossy().to_string())))
+        .collect();
+    expect_true!(
+        envs.contains(&("FUZZTEST_REPLAY_CORPUS_FOR".to_string(), Some("inf".to_string())))
+    );
+    expect_true!(
+        envs.contains(&("FUZZTEST_TIME_BUDGET_TYPE".to_string(), Some("total".to_string())))
+    );
+    expect_true!(envs.contains(&(
+        "FUZZTEST_CORPUS_DB".to_string(),
+        Some("/custom/path/to/corpus_db".to_string())
+    )));
+    expect_true!(envs.contains(&(
+        "FUZZTEST_CENTIPEDE_BINARY_PATH".to_string(),
+        Some("/custom/path/to/centipede".to_string())
+    )));
+}
+
+#[gtest]
 fn test_execution_mode_replay_corpus_missing_centipede_binary_path_errors() {
     let fuzztest_options = FuzzTestOptions {
         replay_corpus_for: Some("10s".parse().expect("valid duration")),
diff --git a/rust/options/src/lib.rs b/rust/options/src/lib.rs
index 2b4c18f..93f9cc4 100644
--- a/rust/options/src/lib.rs
+++ b/rust/options/src/lib.rs
@@ -18,8 +18,12 @@
 #![deny(clippy::absolute_paths)]
 #![deny(unused_imports)]
 
+use anyhow::Context;
 use clap::{Parser, ValueEnum};
-use humantime::Duration;
+use std::fmt;
+use std::fmt::Display;
+use std::fmt::Formatter;
+use std::str::FromStr;
 
 /// Time budget calculation type for replay corpus mode.
 #[derive(ValueEnum, Debug, Clone, Copy, PartialEq, Eq, Default)]
@@ -29,20 +33,6 @@
     Total,
 }
 
-/// Parses a fuzzing duration string from `FUZZTEST_FUZZ_FOR`.
-///
-/// Matches `"inf"` or `"infinity"` to [`FuzzFor::Indefinitely`]. All other values
-/// are parsed as standard human-readable durations (for example, `"5s"` or `"10m"`).
-fn parse_fuzz_for(s: &str) -> anyhow::Result<FuzzFor> {
-    let s_lower = s.trim().to_lowercase();
-    if s_lower == "inf" || s_lower == "infinity" {
-        Ok(FuzzFor::Indefinitely)
-    } else {
-        let duration = s.parse()?;
-        Ok(FuzzFor::Duration(duration))
-    }
-}
-
 /// Command-line and environment variable options parsed for the FuzzTest harness.
 #[derive(Parser, Debug, Clone, Default)]
 pub struct FuzzTestOptions {
@@ -54,8 +44,8 @@
     ///
     /// Accepts a human-readable duration (e.g., `5s`, `10m`, `1h`) or `inf` / `infinity`
     /// to fuzz indefinitely until a crash is found or it is stopped manually.
-    #[arg(env = "FUZZTEST_FUZZ_FOR", long, value_parser = parse_fuzz_for)]
-    pub fuzz_for: Option<FuzzFor>,
+    #[arg(env = "FUZZTEST_FUZZ_FOR", long)]
+    pub fuzz_for: Option<RunDuration>,
 
     /// If true, subprocess logs are printed after every batch. Note that crash logs are always
     /// printed regardless of this flag's value.
@@ -96,8 +86,11 @@
     pub replay_findings: bool,
 
     /// Replay the corpus for a specified duration.
+    ///
+    /// Accepts a human-readable duration (e.g., `5s`, `10m`, `1h`) or `inf` / `infinity`
+    /// to replay indefinitely until stopped manually.
     #[arg(env = "FUZZTEST_REPLAY_CORPUS_FOR", long, requires = "corpus_db")]
-    pub replay_corpus_for: Option<Duration>,
+    pub replay_corpus_for: Option<RunDuration>,
 
     /// Time budget calculation type for replay corpus mode.
     #[arg(env = "FUZZTEST_TIME_BUDGET_TYPE", long, value_enum, default_value_t = TimeBudgetType::PerTest)]
@@ -158,7 +151,7 @@
             return ExecutionMode::ReplayCorpus(ReplayCorpusOptions {
                 replay_corpus_for,
                 time_budget_type: options.time_budget_type,
-                jobs: options.jobs.clone(),
+                jobs: options.jobs,
             });
         }
 
@@ -172,10 +165,7 @@
 
         // Continuous fuzzing mode is selected if an explicit duration/budget (`fuzz_for`) is specified.
         if let Some(fuzz_for) = &options.fuzz_for {
-            return ExecutionMode::Fuzz(FuzzOptions {
-                fuzz_for: *fuzz_for,
-                jobs: options.jobs.clone(),
-            });
+            return ExecutionMode::Fuzz(FuzzOptions { fuzz_for: *fuzz_for, jobs: options.jobs });
         }
 
         ExecutionMode::SmokeTest
@@ -185,21 +175,46 @@
 /// Mode-specific options for continuous fuzzing.
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub struct FuzzOptions {
-    pub fuzz_for: FuzzFor,
+    pub fuzz_for: RunDuration,
 
     /// If `jobs` is `None`, we won't specify the number of jobs while invoking Centipede and it
     /// will use its own default value.
     pub jobs: Option<usize>,
 }
 
-/// The duration or limit for fuzzing.
+/// The duration or limit for fuzzing or replaying corpus.
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum FuzzFor {
-    /// Fuzz indefinitely until it is manually stopped or a crash is found.
+pub enum RunDuration {
+    /// Run indefinitely until manually stopped or a crash is found.
     Indefinitely,
 
-    /// Fuzz for a specific duration.
-    Duration(Duration),
+    /// Run for a specific fixed duration.
+    Fixed(humantime::Duration),
+}
+
+impl FromStr for RunDuration {
+    type Err = anyhow::Error;
+
+    fn from_str(s: &str) -> anyhow::Result<Self> {
+        let s_lower = s.trim().to_lowercase();
+        if s_lower == "inf" || s_lower == "infinity" {
+            Ok(RunDuration::Indefinitely)
+        } else {
+            let duration: humantime::Duration = s
+                .parse()
+                .with_context(|| format!("while attempting to parse duration string '{s}'"))?;
+            Ok(RunDuration::Fixed(duration))
+        }
+    }
+}
+
+impl Display for RunDuration {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        match self {
+            RunDuration::Indefinitely => write!(f, "inf"),
+            RunDuration::Fixed(duration) => write!(f, "{duration}"),
+        }
+    }
 }
 
 /// Mode-specific options for replaying a specific crashing input from the corpus database.
@@ -211,7 +226,7 @@
 /// Mode-specific options for replaying corpus for a duration.
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub struct ReplayCorpusOptions {
-    pub replay_corpus_for: Duration,
+    pub replay_corpus_for: RunDuration,
     pub time_budget_type: TimeBudgetType,
     /// If `jobs` is `None`, we won't specify the number of jobs while invoking Centipede and it
     /// will use its own default value.
@@ -231,6 +246,39 @@
     use std::ffi::OsString;
 
     #[gtest]
+    fn test_duration_from_str_inf() {
+        let duration: RunDuration = "inf".parse().expect("failed to parse 'inf'");
+        expect_that!(duration, eq(RunDuration::Indefinitely));
+    }
+
+    #[gtest]
+    fn test_duration_from_str_infinity() {
+        let duration: RunDuration = "infinity".parse().expect("failed to parse 'infinity'");
+        expect_that!(duration, eq(RunDuration::Indefinitely));
+    }
+
+    #[gtest]
+    fn test_duration_from_str_fixed() {
+        let duration: RunDuration = "10s".parse().expect("failed to parse '10s'");
+        let expected_fixed = humantime::Duration::from(std::time::Duration::from_secs(10));
+        expect_that!(duration, eq(RunDuration::Fixed(expected_fixed)));
+    }
+
+    #[gtest]
+    fn test_duration_from_str_invalid() {
+        let result: Result<RunDuration, _> = "invalid_duration".parse();
+        expect_true!(result.is_err());
+    }
+
+    #[gtest]
+    fn test_duration_display() {
+        expect_that!(RunDuration::Indefinitely.to_string(), eq("inf"));
+        let fixed =
+            RunDuration::Fixed(humantime::Duration::from(std::time::Duration::from_secs(10)));
+        expect_that!(fixed.to_string(), eq("10s"));
+    }
+
+    #[gtest]
     fn test_replay_id_requires_corpus_db() {
         // SAFETY: Testing environment parsing in single-threaded context.
         unsafe {
@@ -281,6 +329,7 @@
         let options = FuzzTestOptions::parse_from(std::iter::empty::<OsString>());
 
         expect_that!(options.jobs, eq(Some(4)));
+
         // Setting jobs alone should not enter fuzzing mode; it defaults to smoke test mode.
         expect_that!(ExecutionMode::from_fuzztest_options(&options), eq(&ExecutionMode::SmokeTest));
 
@@ -305,7 +354,7 @@
         expect_that!(
             ExecutionMode::from_fuzztest_options(&options),
             eq(&ExecutionMode::Fuzz(FuzzOptions {
-                fuzz_for: FuzzFor::Duration(expected_duration),
+                fuzz_for: RunDuration::Fixed(expected_duration),
                 jobs: Some(4),
             }))
         );
@@ -425,12 +474,61 @@
 
         let options = result
             .expect("parsing should succeed when both replay_corpus_for and corpus_db are present");
-        expect_that!(options.replay_corpus_for, eq(Some("10s".parse().unwrap())));
+        expect_that!(
+            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.time_budget_type, eq(TimeBudgetType::PerTest));
     }
 
     #[gtest]
+    fn test_replay_corpus_for_inf_env_succeeds() {
+        // SAFETY: Testing environment parsing in single-threaded context.
+        unsafe {
+            std::env::set_var("FUZZTEST_REPLAY_CORPUS_FOR", "inf");
+            std::env::set_var("FUZZTEST_CORPUS_DB", "/tmp/corpus_db");
+        }
+
+        let result = FuzzTestOptions::try_parse_from(std::iter::empty::<OsString>());
+
+        // SAFETY: Cleaning up environment variables.
+        unsafe {
+            std::env::remove_var("FUZZTEST_REPLAY_CORPUS_FOR");
+            std::env::remove_var("FUZZTEST_CORPUS_DB");
+        }
+
+        let options = result.expect(
+            "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")));
+    }
+
+    #[gtest]
+    fn test_replay_corpus_for_infinity_env_succeeds() {
+        // SAFETY: Testing environment parsing in single-threaded context.
+        unsafe {
+            std::env::set_var("FUZZTEST_REPLAY_CORPUS_FOR", "infinity");
+            std::env::set_var("FUZZTEST_CORPUS_DB", "/tmp/corpus_db");
+        }
+
+        let result = FuzzTestOptions::try_parse_from(std::iter::empty::<OsString>());
+
+        // SAFETY: Cleaning up environment variables.
+        unsafe {
+            std::env::remove_var("FUZZTEST_REPLAY_CORPUS_FOR");
+            std::env::remove_var("FUZZTEST_CORPUS_DB");
+        }
+
+        let options = result.expect(
+            "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")));
+    }
+
+    #[gtest]
     fn test_replay_corpus_for_with_total_time_budget() {
         // SAFETY: Testing environment parsing in single-threaded context.
         unsafe {
@@ -449,7 +547,34 @@
         }
 
         let options = result.expect("parsing should succeed with total time budget type");
-        expect_that!(options.replay_corpus_for, eq(Some("10s".parse().unwrap())));
+        expect_that!(
+            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.time_budget_type, eq(TimeBudgetType::Total));
+    }
+
+    #[gtest]
+    fn test_replay_corpus_for_inf_with_total_time_budget() {
+        // SAFETY: Testing environment parsing in single-threaded context.
+        unsafe {
+            std::env::set_var("FUZZTEST_REPLAY_CORPUS_FOR", "inf");
+            std::env::set_var("FUZZTEST_TIME_BUDGET_TYPE", "total");
+            std::env::set_var("FUZZTEST_CORPUS_DB", "/tmp/corpus_db");
+        }
+
+        let result = FuzzTestOptions::try_parse_from(std::iter::empty::<OsString>());
+
+        // SAFETY: Cleaning up environment variables.
+        unsafe {
+            std::env::remove_var("FUZZTEST_REPLAY_CORPUS_FOR");
+            std::env::remove_var("FUZZTEST_TIME_BUDGET_TYPE");
+            std::env::remove_var("FUZZTEST_CORPUS_DB");
+        }
+
+        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.time_budget_type, eq(TimeBudgetType::Total));
     }
diff --git a/rust/src/domains/arbitrary.rs b/rust/src/domains/arbitrary.rs
index 33abb2b..e2167bf 100644
--- a/rust/src/domains/arbitrary.rs
+++ b/rust/src/domains/arbitrary.rs
@@ -17,6 +17,7 @@
 use super::utility::shrink_towards;
 use super::Domain;
 use std::char;
+use std::fmt;
 use std::marker::PhantomData;
 
 use anyhow;
@@ -47,12 +48,12 @@
 
 impl<T> Clone for Arbitrary<T> {
     fn clone(&self) -> Self {
-        Self { _phantom: std::marker::PhantomData }
+        Self { _phantom: PhantomData }
     }
 }
 
-impl<T> std::fmt::Debug for Arbitrary<T> {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+impl<T> fmt::Debug for Arbitrary<T> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         f.debug_struct("Arbitrary").field("_phantom", &self._phantom).finish()
     }
 }
diff --git a/rust/src/domains/containers.rs b/rust/src/domains/containers.rs
index 3c34565..a15e28c 100644
--- a/rust/src/domains/containers.rs
+++ b/rust/src/domains/containers.rs
@@ -1,4 +1,5 @@
 use rand::RngExt;
+use std::fmt;
 
 use super::Domain;
 
@@ -99,8 +100,8 @@
     }
 }
 
-impl<T: std::fmt::Debug> std::fmt::Debug for VecOf<T> {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+impl<T: fmt::Debug> fmt::Debug for VecOf<T> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         f.debug_struct("VecOf")
             .field("inner", &self.inner)
             .field("min_len", &self.min_len)
@@ -201,7 +202,7 @@
 
     fn with_min_len(self, min_len: usize) -> Self {
         assert!(
-            self.max_len.map_or(true, |max| min_len <= max),
+            self.max_len.is_none_or(|max| min_len <= max),
             "Minimum length {} cannot be greater than the maximum length {}",
             min_len,
             self.max_len.unwrap()
diff --git a/rust/src/options.rs b/rust/src/options.rs
index 5b659da..41abaf5 100644
--- a/rust/src/options.rs
+++ b/rust/src/options.rs
@@ -25,8 +25,8 @@
 use tempfile::{NamedTempFile, TempDir};
 
 pub use fuzztest_options::{
-    ExecutionMode, FuzzFor, FuzzOptions, FuzzTestOptions, ListCrashIdsOptions, ReplayCorpusOptions,
-    ReplayCrashOptions, TimeBudgetType,
+    ExecutionMode, FuzzOptions, FuzzTestOptions, ReplayCorpusOptions, ReplayCrashOptions,
+    RunDuration, TimeBudgetType,
 };
 
 /// Returns a lazily-initialized static reference to the global `FuzzTestOptions`.
@@ -214,10 +214,10 @@
         match mode_opts {
             ExecutionMode::Fuzz(fuzz_opts) => {
                 match &fuzz_opts.fuzz_for {
-                    FuzzFor::Indefinitely => {
+                    RunDuration::Indefinitely => {
                         // not specifying `--stop_after` means to run indefinitely.
                     }
-                    FuzzFor::Duration(duration) => {
+                    RunDuration::Fixed(duration) => {
                         let duration_secs = duration.as_secs_f64();
                         if opt_corpusdb.is_some() {
                             add_arg(format!("--fuzztest_time_limit_per_test={duration_secs}s"))?;
@@ -242,22 +242,31 @@
                 add_arg(format!("--list_crash_ids_file={}", path.display()))?;
             }
             ExecutionMode::ReplayCorpus(replay_corpus_opts) => {
-                let time_limit = match replay_corpus_opts.time_budget_type {
-                    TimeBudgetType::PerTest => replay_corpus_opts.replay_corpus_for,
-                    TimeBudgetType::Total => {
-                        let num_tests = inventory::iter::<FuzzTestRegistration>().count();
-                        if num_tests == 0 {
-                            replay_corpus_opts.replay_corpus_for
-                        } else {
-                            (*replay_corpus_opts.replay_corpus_for.as_ref() / (num_tests as u32))
-                                .into()
-                        }
-                    }
-                };
                 add_arg("--fuzztest_only_replay=true".to_string())?;
                 add_arg("--fuzztest_replay_coverage_inputs=true".to_string())?;
                 add_arg("--load_shards_only=true".to_string())?;
-                add_arg(format!("--fuzztest_time_limit_per_test={time_limit}"))?;
+                match replay_corpus_opts.replay_corpus_for {
+                    RunDuration::Indefinitely => {
+                        // Not specifying `--fuzztest_time_limit_per_test` means to run indefinitely
+                    }
+                    RunDuration::Fixed(duration) => {
+                        let time_limit: humantime::Duration = match replay_corpus_opts
+                            .time_budget_type
+                        {
+                            TimeBudgetType::PerTest => duration,
+                            TimeBudgetType::Total => {
+                                let num_tests = inventory::iter::<FuzzTestRegistration>().count();
+                                if num_tests == 0 {
+                                    duration
+                                } else {
+                                    (*duration.as_ref() / (num_tests as u32)).into()
+                                }
+                            }
+                        };
+                        let time_limit_secs = time_limit.as_secs_f64();
+                        add_arg(format!("--fuzztest_time_limit_per_test={time_limit_secs}s"))?;
+                    }
+                }
                 if let Some(jobs) = &replay_corpus_opts.jobs {
                     add_arg(format!("--j={jobs}"))?;
                 }
@@ -397,12 +406,12 @@
 
         let options = FuzzTestOptions::parse_from(std::iter::empty::<OsString>());
 
-        expect_that!(options.fuzz_for, eq(Some(FuzzFor::Indefinitely)));
+        expect_that!(options.fuzz_for, eq(Some(RunDuration::Indefinitely)));
         let mode = ExecutionMode::from_fuzztest_options(&options);
         let ExecutionMode::Fuzz(fuzz_opts) = mode else {
             panic!("Expected ExecutionMode::Fuzz");
         };
-        expect_that!(fuzz_opts.fuzz_for, eq(FuzzFor::Indefinitely));
+        expect_that!(fuzz_opts.fuzz_for, eq(RunDuration::Indefinitely));
 
         // SAFETY: Cleaning up environment variables.
         unsafe {
@@ -413,7 +422,7 @@
     #[gtest]
     fn test_determine_execution_action_standalone_indefinite() {
         let options =
-            FuzzTestOptions { fuzz_for: Some(FuzzFor::Indefinitely), ..Default::default() };
+            FuzzTestOptions { fuzz_for: Some(RunDuration::Indefinitely), ..Default::default() };
         let action = determine_execution_action_internal(&options, "my_mod::my_test");
 
         let ExecutionAction::Standalone(args) = action else {
@@ -437,11 +446,8 @@
 
     #[gtest]
     fn test_determine_execution_action_standalone() {
-        let expected_duration = "10s".parse().unwrap();
-        let options = FuzzTestOptions {
-            fuzz_for: Some(FuzzFor::Duration(expected_duration)),
-            ..Default::default()
-        };
+        let expected_duration: RunDuration = "10s".parse().unwrap();
+        let options = FuzzTestOptions { fuzz_for: Some(expected_duration), ..Default::default() };
         let action = determine_execution_action_internal(&options, "my_mod::my_test");
 
         let ExecutionAction::Standalone(args) = action else {
@@ -471,10 +477,7 @@
     #[gtest]
     fn test_centipede_args_binary_identifier() {
         let expected_duration = "1s".parse().unwrap();
-        let options = FuzzTestOptions {
-            fuzz_for: Some(FuzzFor::Duration(expected_duration)),
-            ..Default::default()
-        };
+        let options = FuzzTestOptions { fuzz_for: Some(expected_duration), ..Default::default() };
         let action = determine_execution_action_internal(&options, "my_mod::my_test");
 
         let ExecutionAction::Standalone(args) = action else {
@@ -494,10 +497,8 @@
     #[gtest]
     fn test_centipede_args_jobs() {
         let expected_duration = "1s".parse().expect("failed to parse duration");
-        let options_no_jobs = FuzzTestOptions {
-            fuzz_for: Some(FuzzFor::Duration(expected_duration)),
-            ..Default::default()
-        };
+        let options_no_jobs =
+            FuzzTestOptions { fuzz_for: Some(expected_duration), ..Default::default() };
         let action_no_jobs =
             determine_execution_action_internal(&options_no_jobs, "my_mod::my_test");
         let ExecutionAction::Standalone(args_no_jobs) = action_no_jobs else {
@@ -508,7 +509,7 @@
         assert!(!args_str_no_jobs.iter().any(|s| s.starts_with("--j=")));
 
         let options_with_jobs = FuzzTestOptions {
-            fuzz_for: Some(FuzzFor::Duration(expected_duration)),
+            fuzz_for: Some(expected_duration),
             jobs: Some(4),
             ..Default::default()
         };
@@ -525,8 +526,7 @@
     #[gtest]
     fn test_default_env_diff_set() {
         let duration = "1s".parse().unwrap();
-        let options =
-            FuzzTestOptions { fuzz_for: Some(FuzzFor::Duration(duration)), ..Default::default() };
+        let options = FuzzTestOptions { fuzz_for: Some(duration), ..Default::default() };
         let action = determine_execution_action_internal(&options, "my_mod::my_test");
         let ExecutionAction::Standalone(args) = action else {
             panic!("Expected Standalone action");
@@ -641,6 +641,31 @@
     }
 
     #[gtest]
+    fn test_determine_execution_action_replay_corpus_indefinite() {
+        let options = FuzzTestOptions {
+            replay_corpus_for: Some(RunDuration::Indefinitely),
+            time_budget_type: TimeBudgetType::PerTest,
+            ..Default::default()
+        };
+        let action = determine_execution_action_internal(&options, "my_mod::my_test");
+
+        let ExecutionAction::Standalone(args) = action else {
+            panic!("Expected Standalone action");
+        };
+
+        let args_str: Vec<&str> =
+            args._c_strings.iter().map(|s| s.to_str().expect("invalid utf8")).collect();
+
+        assert!(args_str
+            .iter()
+            .any(|s| s.starts_with("--binary=") && s.contains("my_mod::my_test --exact")));
+        assert!(args_str.contains(&"--fuzztest_only_replay=true"));
+        assert!(args_str.contains(&"--fuzztest_replay_coverage_inputs=true"));
+        assert!(args_str.contains(&"--load_shards_only=true"));
+        assert!(!args_str.iter().any(|s| s.starts_with("--fuzztest_time_limit_per_test=")));
+    }
+
+    #[gtest]
     fn test_determine_execution_action_replay_corpus_total_budget() {
         let expected_duration = "10s".parse().expect("failed to parse duration");
         let options = FuzzTestOptions {
@@ -658,10 +683,13 @@
             args._c_strings.iter().map(|s| s.to_str().expect("invalid utf8")).collect();
 
         let num_tests = inventory::iter::<FuzzTestRegistration>().count();
+        let RunDuration::Fixed(fixed_duration) = expected_duration else {
+            panic!("expected Fixed duration");
+        };
         let expected_limit = if num_tests == 0 {
-            expected_duration
+            fixed_duration
         } else {
-            (*expected_duration.as_ref() / (num_tests as u32)).into()
+            (*fixed_duration.as_ref() / (num_tests as u32)).into()
         };
         let expected_limit_str = format!("--fuzztest_time_limit_per_test={}", expected_limit);
 
@@ -669,6 +697,26 @@
     }
 
     #[gtest]
+    fn test_determine_execution_action_replay_corpus_indefinite_total_budget() {
+        let options = FuzzTestOptions {
+            replay_corpus_for: Some(RunDuration::Indefinitely),
+            time_budget_type: TimeBudgetType::Total,
+            ..Default::default()
+        };
+        let action = determine_execution_action_internal(&options, "my_mod::my_test");
+
+        let ExecutionAction::Standalone(args) = action else {
+            panic!("Expected Standalone action");
+        };
+
+        let args_str: Vec<&str> =
+            args._c_strings.iter().map(|s| s.to_str().expect("invalid utf8")).collect();
+
+        assert!(args_str.contains(&"--fuzztest_only_replay=true"));
+        assert!(!args_str.iter().any(|s| s.starts_with("--fuzztest_time_limit_per_test=")));
+    }
+
+    #[gtest]
     fn test_get_corpusdb_and_workdir_default_creates_temp_workdir() -> Result<()> {
         let options = FuzzTestOptions::default();
         let (corpus_db, workdir_root, workdir) =
@@ -752,11 +800,8 @@
         // When corpus_db is not provided, fuzzing for a fixed duration should pass --stop_after
         // to Centipede so it stops fuzzing after the specified duration.
         let duration = "1s".parse().expect("fixed test string should parse as duration");
-        let options = FuzzTestOptions {
-            fuzz_for: Some(FuzzFor::Duration(duration)),
-            corpus_db: None,
-            ..Default::default()
-        };
+        let options =
+            FuzzTestOptions { fuzz_for: Some(duration), corpus_db: None, ..Default::default() };
         let action = determine_execution_action_internal(&options, "my_mod::my_test");
 
         let ExecutionAction::Standalone(args) = action else {
@@ -780,7 +825,7 @@
         // a corpus database.
         let duration = "1s".parse().expect("fixed test string should parse as duration");
         let options = FuzzTestOptions {
-            fuzz_for: Some(FuzzFor::Duration(duration)),
+            fuzz_for: Some(duration),
             corpus_db: Some("/tmp/corpus_db".to_string()),
             ..Default::default()
         };