Skip to main content

fs_ext4/
runtime.rs

1//! Environmental values used by mounted-filesystem mutations.
2//!
3//! A provider can be injected for deterministic tests or an embedding runtime.
4//! Formatting has its own UUID policy and is not changed by this interface;
5//! its random bytes come from [`fill_random`] here.
6//!
7//! THIS IS THE ONE PLACE `src/` READS THE CLOCK, THE PROCESS ID OR THE
8//! PLATFORM'S RANDOMNESS. On the browser build (wasm32-unknown-unknown) `std`
9//! compiles `SystemTime::now`, `Instant::now` and `std::process::id` and
10//! panics when they run, so each use here has a JavaScript path beside it.
11//! `tests/scripts/test-clock-calls.sh` refuses those calls anywhere else.
12use std::sync::atomic::{AtomicU32, Ordering};
13
14pub trait Runtime: Send + Sync {
15    /// Seconds since the Unix epoch, signed and 64-bit: ext4 stores
16    /// times past 2038 (to 2446) by extending the 32-bit base with the
17    /// epoch bits of each `*_extra` field, so a `u32` cannot carry them.
18    fn now_unix_seconds(&self) -> i64;
19    /// New inode generation; implementations must avoid immediate reuse.
20    fn next_inode_generation(&self) -> u32;
21}
22
23/// Native defaults retain the process-ID/counter and SystemTime behavior.
24/// Browser builds use JavaScript wall time and a random process-equivalent seed.
25pub struct SystemRuntime;
26static COUNTER: AtomicU32 = AtomicU32::new(1);
27
28impl Runtime for SystemRuntime {
29    fn now_unix_seconds(&self) -> i64 {
30        #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
31        {
32            (js_sys::Date::now() / 1000.0).floor() as i64
33        }
34        #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
35        {
36            let now = std::time::SystemTime::now();
37            match now.duration_since(std::time::UNIX_EPOCH) {
38                Ok(d) => i64::try_from(d.as_secs()).unwrap_or(i64::MAX),
39                // A clock set before 1970 is still a time ext4 can store.
40                Err(e) => i64::try_from(e.duration().as_secs()).map_or(i64::MIN, |s| -s),
41            }
42        }
43    }
44    fn next_inode_generation(&self) -> u32 {
45        #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
46        let seed = {
47            // Randomize once for this WASM instance, then preserve the native
48            // monotonic wrapping counter semantics within the instance.
49            static SEED: std::sync::OnceLock<u32> = std::sync::OnceLock::new();
50            *SEED.get_or_init(|| (js_sys::Math::random() * 4294967296.0) as u32)
51        };
52        #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
53        let seed = std::process::id();
54        seed.wrapping_add(COUNTER.fetch_add(1, Ordering::Relaxed))
55    }
56}
57
58/// Fill `out` with random bytes for identifiers such as a volume UUID. Not a
59/// source of key material.
60///
61/// Native: `/dev/urandom`, falling back to a generator seeded from the time
62/// and the process ID when that cannot be read. Browser build: the host's
63/// `crypto.getRandomValues`, which every browser and Node 19 or later has,
64/// falling back to `Math.random` for a host without it.
65pub(crate) fn fill_random(out: &mut [u8]) {
66    #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
67    {
68        if !fill_from_web_crypto(out) {
69            for b in out.iter_mut() {
70                *b = (js_sys::Math::random() * 256.0) as u8;
71            }
72        }
73    }
74    #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
75    {
76        if let Ok(mut f) = std::fs::File::open("/dev/urandom") {
77            use std::io::Read;
78            if f.read_exact(out).is_ok() {
79                return;
80            }
81        }
82        // Not cryptographic, and it does not need to be: this names a volume.
83        let mut state = std::time::SystemTime::now()
84            .duration_since(std::time::UNIX_EPOCH)
85            .map(|d| d.as_nanos() as u64)
86            .unwrap_or(0xDEADBEEF)
87            ^ (std::process::id() as u64).wrapping_mul(0x9E3779B97F4A7C15);
88        for b in out.iter_mut() {
89            state = state
90                .wrapping_mul(6364136223846793005)
91                .wrapping_add(1442695040888963407);
92            *b = (state >> 56) as u8;
93        }
94    }
95}
96
97/// `globalThis.crypto.getRandomValues(out)`, or `false` when the host has no
98/// such function or it throws.
99#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
100fn fill_from_web_crypto(out: &mut [u8]) -> bool {
101    use js_sys::wasm_bindgen::{JsCast, JsValue};
102    use js_sys::{Function, Reflect, Uint8Array};
103
104    let Ok(crypto) = Reflect::get(&js_sys::global(), &JsValue::from_str("crypto")) else {
105        return false;
106    };
107    if !crypto.is_object() {
108        return false;
109    }
110    let Ok(get) = Reflect::get(&crypto, &JsValue::from_str("getRandomValues")) else {
111        return false;
112    };
113    let Ok(get) = get.dyn_into::<Function>() else {
114        return false;
115    };
116    let bytes = Uint8Array::new_with_length(out.len() as u32);
117    let arg: &JsValue = bytes.as_ref();
118    if get.call1(&crypto, arg).is_err() {
119        return false;
120    }
121    bytes.copy_to(out);
122    true
123}
124
125/// Time since [`Stopwatch::start`], for rate-limiting work such as progress
126/// callbacks. Never a timestamp.
127///
128/// Native: `std::time::Instant`, monotonic. Browser build: the host's
129/// `Date.now()`, because `Instant::now` panics there (#295); a wall clock can
130/// step backwards, which reads as no time elapsed rather than as a panic.
131pub(crate) struct Stopwatch {
132    #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
133    started_ms: f64,
134    #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
135    started: std::time::Instant,
136}
137
138impl Stopwatch {
139    pub(crate) fn start() -> Self {
140        Self {
141            #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
142            started_ms: js_sys::Date::now(),
143            #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
144            started: std::time::Instant::now(),
145        }
146    }
147
148    pub(crate) fn elapsed(&self) -> std::time::Duration {
149        #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
150        {
151            let ms = (js_sys::Date::now() - self.started_ms).max(0.0);
152            std::time::Duration::from_secs_f64(ms / 1000.0)
153        }
154        #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
155        {
156            self.started.elapsed()
157        }
158    }
159}