bashkit 0.20.1

Awesomely fast virtual sandbox with bash and file system
Documentation
//! Cross-platform `Instant`/`SystemTime`.
//!
//! `std::time::Instant`/`SystemTime` panic unconditionally on
//! wasm32-unknown-unknown ("time not implemented on this platform" — no OS
//! clock, no extension point to satisfy; see
//! `library/std/src/sys/time/unsupported.rs` in the Rust source). `web-time`
//! is a drop-in replacement backed by `Performance.now()`/`Date.now()` via
//! web-sys on that target. Every other target reads `std::time` directly,
//! including the WASI targets (wasip1, wasip2, wasip1-threads), which have real
//! clocks. Use this module's `Instant`/`SystemTime`/`UNIX_EPOCH` instead of
//! `std::time`'s directly anywhere wall-clock time is read.
//!
//! `web-time` is only correct when the wasm host *is* a JS host, so it sits
//! behind the `wasm_js` feature. Without it, `wasm32-unknown-unknown` takes
//! [`host_clock`], where the embedder supplies the clock (and the timer, by
//! spinning on it). See that module for the contract.

use std::future::Future;
use std::time::Duration;

#[cfg(all(
    target_arch = "wasm32",
    target_os = "unknown",
    not(feature = "wasm_js")
))]
pub mod host_clock;

#[cfg(all(
    target_arch = "wasm32",
    target_os = "unknown",
    not(feature = "wasm_js")
))]
pub use host_clock::{Instant, SystemTime, UNIX_EPOCH};

#[cfg(all(target_arch = "wasm32", target_os = "unknown", feature = "wasm_js"))]
pub use web_time::{Instant, SystemTime, UNIX_EPOCH};

// WASI targets (wasip1, wasip2, wasip1-threads) have real clocks in std, and
// no tokio timer driver worth routing `Instant` through.
#[cfg(all(target_arch = "wasm32", not(target_os = "unknown")))]
pub use std::time::{Instant, SystemTime, UNIX_EPOCH};

#[cfg(not(target_arch = "wasm32"))]
pub use std::time::{SystemTime, UNIX_EPOCH};

// Tokio's instant follows the runtime's paused/advanced monotonic clock. This
// makes shell deadlines and `time` deterministic under virtual-time runtimes.
#[cfg(not(target_arch = "wasm32"))]
pub use tokio::time::Instant;

/// Portable timer future. JS-host wasm uses `setTimeout`; native targets keep
/// tokio's runtime timer. The wasm future is wrapped as `Send` because the
/// target is deliberately single-threaded (the same invariant as JS builtins).
pub async fn sleep(duration: Duration) {
    #[cfg(all(
        target_arch = "wasm32",
        target_os = "unknown",
        not(feature = "wasm_js")
    ))]
    {
        // No timers and no other thread to make progress: spin the host clock.
        // Blocking is the point — the single-poll driver these embedders use
        // (`now_or_never`) can never come back to a pending timer future.
        let deadline = Instant::now() + duration;
        while Instant::now() < deadline {
            std::hint::spin_loop();
        }
    }

    #[cfg(all(target_arch = "wasm32", target_os = "unknown", feature = "wasm_js"))]
    send_wrapper::SendWrapper::new(gloo_timers::future::TimeoutFuture::new(duration_millis(
        duration,
    )))
    .await;

    #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
    tokio::time::sleep(duration).await;
}

#[derive(Debug)]
pub struct TimeoutElapsed;

/// Race a future against a host wall-clock deadline on every supported target.
pub async fn timeout<F: Future>(
    duration: Duration,
    future: F,
) -> Result<F::Output, TimeoutElapsed> {
    #[cfg(all(
        target_arch = "wasm32",
        target_os = "unknown",
        not(feature = "wasm_js")
    ))]
    {
        // Poll the future, then check the host clock. Without timers there is
        // nothing to race against, so a still-pending future past the deadline
        // is the timeout.
        use std::pin::pin;
        use std::task::Poll;

        let deadline = Instant::now() + duration;
        let mut future = pin!(future);
        return std::future::poll_fn(move |cx| {
            let result = future.as_mut().poll(cx);
            // A non-yielding operation may have consumed the remaining budget
            // before returning Ready, so deadline precedence must be checked
            // after every poll, not only after Pending.
            if Instant::now() >= deadline {
                Poll::Ready(Err(TimeoutElapsed))
            } else {
                result.map(Ok)
            }
        })
        .await;
    }

    #[cfg(all(target_arch = "wasm32", target_os = "unknown", feature = "wasm_js"))]
    {
        use futures_util::future::{Either, select};
        let timer = send_wrapper::SendWrapper::new(gloo_timers::future::TimeoutFuture::new(
            duration_millis(duration),
        ));
        futures_util::pin_mut!(future, timer);
        match select(future, timer).await {
            Either::Left((output, _)) => Ok(output),
            Either::Right(_) => Err(TimeoutElapsed),
        }
    }

    #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
    tokio::time::timeout(duration, future)
        .await
        .map_err(|_| TimeoutElapsed)
}

#[cfg(all(target_arch = "wasm32", target_os = "unknown", feature = "wasm_js"))]
fn duration_millis(duration: Duration) -> u32 {
    (duration.as_secs_f64() * 1000.0)
        .ceil()
        .clamp(0.0, u32::MAX as f64) as u32
}

/// Convert to a `chrono::DateTime<Utc>`.
///
/// `chrono` implements `From<std::time::SystemTime>` but not
/// `From<web_time::SystemTime>` — same API shape, different type, so the
/// blanket impl doesn't apply on wasm32. Goes through `duration_since`
/// instead, which both `SystemTime`s support identically.
pub(crate) fn to_chrono_utc(t: SystemTime) -> chrono::DateTime<chrono::Utc> {
    let epoch = || chrono::DateTime::from_timestamp(0, 0).expect("epoch is representable");
    match t.duration_since(UNIX_EPOCH) {
        Ok(dur) => chrono::DateTime::from_timestamp(dur.as_secs() as i64, dur.subsec_nanos())
            .unwrap_or_else(epoch),
        Err(e) => {
            // `dur` is how far *before* the epoch `t` is. chrono represents
            // negative timestamps as (floor_secs, nanos) where nanos is the
            // non-negative remainder added back — e.g. epoch - 500ms is
            // (-1, 500_000_000), not (0, 0). Preserve that decomposition
            // instead of truncating the subsecond part.
            let dur = e.duration();
            let (secs, nanos) = if dur.subsec_nanos() == 0 {
                (-(dur.as_secs() as i64), 0)
            } else {
                (
                    -(dur.as_secs() as i64) - 1,
                    1_000_000_000 - dur.subsec_nanos(),
                )
            };
            chrono::DateTime::from_timestamp(secs, nanos).unwrap_or_else(epoch)
        }
    }
}

/// Current wall-clock time as a `chrono::DateTime<Utc>`.
///
/// Use instead of `chrono::Utc::now()`: chrono reads the clock through
/// `std::time::SystemTime` (which panics on wasm32-unknown-unknown) or, with
/// its `wasmbind` feature, through the JS `Date` — neither works for a non-JS
/// wasm embedder. This routes through [`SystemTime`], so every target gets the
/// clock it actually has.
pub(crate) fn now_utc() -> chrono::DateTime<chrono::Utc> {
    to_chrono_utc(SystemTime::now())
}

/// Convert from a `chrono::DateTime`. Mirror of [`to_chrono_utc`] — see there
/// for why this can't just be a `From`/`Into` conversion.
pub(crate) fn from_chrono<Tz: chrono::TimeZone>(dt: chrono::DateTime<Tz>) -> SystemTime {
    let utc = dt.with_timezone(&chrono::Utc);
    let secs = utc.timestamp();
    let nanos = utc.timestamp_subsec_nanos();
    if secs >= 0 {
        UNIX_EPOCH + std::time::Duration::new(secs as u64, nanos)
    } else if nanos == 0 {
        UNIX_EPOCH - std::time::Duration::new((-secs) as u64, 0)
    } else {
        // Mirror of the decomposition in `to_chrono_utc`: `secs` is the
        // floor, so the actual distance before the epoch is one second
        // less than `-secs`, plus the complementary nanos.
        UNIX_EPOCH - std::time::Duration::new((-secs - 1) as u64, 1_000_000_000 - nanos)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn now_utc_tracks_the_system_clock() {
        let before = chrono::Utc::now();
        let sampled = now_utc();
        let after = chrono::Utc::now();
        assert!(
            sampled >= before && sampled <= after,
            "now_utc() {sampled} outside [{before}, {after}]"
        );
    }

    #[test]
    fn now_utc_is_monotonic_across_calls() {
        assert!(now_utc() <= now_utc());
    }

    #[test]
    fn to_chrono_utc_epoch() {
        let dt = to_chrono_utc(UNIX_EPOCH);
        assert_eq!((dt.timestamp(), dt.timestamp_subsec_nanos()), (0, 0));
    }

    #[test]
    fn to_chrono_utc_pre_epoch_whole_second() {
        let t = UNIX_EPOCH - std::time::Duration::from_secs(2);
        let dt = to_chrono_utc(t);
        assert_eq!((dt.timestamp(), dt.timestamp_subsec_nanos()), (-2, 0));
    }

    #[test]
    fn to_chrono_utc_pre_epoch_subsecond() {
        // Reviewer's case: 500ms before the epoch must round-trip as
        // 1969-12-31T23:59:59.500Z, i.e. (-1, 500_000_000) — not (0, 0).
        let t = UNIX_EPOCH - std::time::Duration::from_millis(500);
        let dt = to_chrono_utc(t);
        assert_eq!(
            (dt.timestamp(), dt.timestamp_subsec_nanos()),
            (-1, 500_000_000)
        );
        assert_eq!(dt.to_rfc3339(), "1969-12-31T23:59:59.500+00:00");
    }

    #[test]
    fn from_chrono_pre_epoch_subsecond() {
        let dt = chrono::DateTime::from_timestamp(-1, 500_000_000).unwrap();
        let t = from_chrono(dt);
        assert_eq!(
            t.duration_since(UNIX_EPOCH).unwrap_err().duration(),
            std::time::Duration::from_millis(500)
        );
    }

    #[test]
    fn round_trip_pre_epoch_subsecond() {
        let original = UNIX_EPOCH - std::time::Duration::from_millis(500);
        let round_tripped = from_chrono(to_chrono_utc(original));
        assert_eq!(round_tripped, original);
    }

    #[test]
    fn round_trip_post_epoch_subsecond() {
        let original = UNIX_EPOCH + std::time::Duration::from_millis(500);
        let round_tripped = from_chrono(to_chrono_utc(original));
        assert_eq!(round_tripped, original);
    }
}

/// Tracks time an execution spends blocked on interactive terminal input.
///
/// THREAT[TM-DOS-120]: see the threat model for why this is safe.
///
/// Important decision: the execution timeout (TM-DOS-057) bounds work the
/// sandbox does, not how long a human or agent takes to type. A `vi` session
/// waiting for the next keystroke burns no CPU, and the host controls when
/// input arrives, so that wait is excluded from the deadline. Everything else
/// (loops, `sleep`, pending callbacks) still counts.
#[cfg(feature = "terminal")]
#[derive(Clone, Default)]
pub(crate) struct InputWaitClock {
    inner: std::sync::Arc<std::sync::Mutex<InputWaitState>>,
}

#[cfg(feature = "terminal")]
#[derive(Default)]
struct InputWaitState {
    paused_since: Option<Instant>,
    total: Duration,
}

#[cfg(feature = "terminal")]
impl InputWaitClock {
    fn lock(&self) -> std::sync::MutexGuard<'_, InputWaitState> {
        self.inner
            .lock()
            .unwrap_or_else(std::sync::PoisonError::into_inner)
    }

    /// Start excluding time until the returned guard drops.
    pub(crate) fn pause(&self) -> InputWaitGuard {
        let mut state = self.lock();
        if state.paused_since.is_none() {
            state.paused_since = Some(Instant::now());
        }
        InputWaitGuard {
            clock: self.clone(),
        }
    }

    /// Total excluded time, including a wait still in progress.
    fn excluded(&self) -> Duration {
        let state = self.lock();
        state.total
            + state
                .paused_since
                .map(|since| {
                    Instant::now()
                        .checked_duration_since(since)
                        .unwrap_or_default()
                })
                .unwrap_or_default()
    }
}

/// Ends an input wait started by [`InputWaitClock::pause`].
#[cfg(feature = "terminal")]
pub(crate) struct InputWaitGuard {
    clock: InputWaitClock,
}

#[cfg(feature = "terminal")]
impl Drop for InputWaitGuard {
    fn drop(&mut self) {
        let mut state = self.clock.lock();
        if let Some(since) = state.paused_since.take() {
            state.total += Instant::now()
                .checked_duration_since(since)
                .unwrap_or_default();
        }
    }
}

/// [`timeout`], but time recorded on `clock` as input wait does not count.
///
/// Re-arms the portable timer with the remaining active budget each time it
/// fires, so it works on every target `timeout` supports. A long input wait
/// costs one wakeup per remaining-budget interval, never a busy loop.
#[cfg(feature = "terminal")]
pub(crate) async fn timeout_excluding_input_wait<F: Future>(
    duration: Duration,
    clock: &InputWaitClock,
    future: F,
) -> Result<F::Output, TimeoutElapsed> {
    let start = Instant::now();
    let excluded_at_start = clock.excluded();
    let mut future = std::pin::pin!(future);
    let mut remaining = duration;
    loop {
        if let Ok(output) = timeout(remaining, future.as_mut()).await {
            return Ok(output);
        }
        let elapsed = Instant::now()
            .checked_duration_since(start)
            .unwrap_or_default();
        let excluded = clock.excluded().saturating_sub(excluded_at_start);
        let active = elapsed.saturating_sub(excluded);
        remaining = duration.saturating_sub(active);
        if remaining.is_zero() {
            return Err(TimeoutElapsed);
        }
    }
}