Skip to main content

offline_protocol_core/
time.rs

1//! The workspace's clock.
2//!
3//! Every crate in the workspace reads the time through this module and never
4//! through `std::time::Instant` or `std::time::SystemTime` directly. On every
5//! target but one, this module is `std::time` and nothing else: the types are
6//! the standard library's own, so a value crosses any API that names
7//! `std::time` unchanged.
8//!
9//! The exception is `wasm32-unknown-unknown`, the browser target. There the
10//! standard library compiles `Instant::now()` and `SystemTime::now()` and
11//! makes both panic when called, because the target defines no host clock. A
12//! green build therefore says nothing; the first tick of the engine aborts the
13//! module. On that target this module re-exports `web-time`, which reads
14//! `performance.now()` and `Date.now()` and has the same API.
15//!
16//! `clocks_are_read_through_core_time_only` in this crate walks every crate's
17//! sources and refuses a direct `std::time` clock import, because nothing else
18//! notices one until it runs in a browser.
19
20#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
21pub use std::time::{Instant, SystemTime, SystemTimeError, UNIX_EPOCH};
22
23#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
24pub use web_time::{Instant, SystemTime, SystemTimeError, UNIX_EPOCH};
25
26pub use core::time::Duration;
27
28#[cfg(test)]
29mod tests {
30    use super::*;
31
32    #[test]
33    fn the_clock_is_std_time_on_a_host() {
34        // A host must get the standard library's own types, so a value from
35        // here satisfies any API that names `std::time`.
36        let instant: std::time::Instant = Instant::now();
37        let wall: std::time::SystemTime = SystemTime::now();
38        assert!(instant.elapsed() < Duration::from_secs(60));
39        assert!(wall.duration_since(UNIX_EPOCH).is_ok());
40    }
41
42    /// The clock types `std::time` must not be reached for directly.
43    const FORBIDDEN: [&str; 3] = ["Instant", "SystemTime", "UNIX_EPOCH"];
44
45    /// Every place in `source` that names a `std::time` clock, in any of the
46    /// spellings Rust accepts: `std::time::Instant`, `std::time::{A, B}`,
47    /// `std::{time::Instant, ..}` at any depth, a renaming `as`, or the module
48    /// itself (`use std::time;`, `std::time::{self}`), which is the clock one
49    /// path segment later. Comments are dropped and whitespace around path
50    /// punctuation is removed first, so a group split over lines is seen
51    /// whole.
52    fn clock_imports(source: &str) -> Vec<String> {
53        let code: String = source
54            .lines()
55            .map(|line| line.split("//").next().unwrap_or(""))
56            .collect::<Vec<_>>()
57            .join(" ");
58        let code = tighten(&code);
59        let mut found = Vec::new();
60        let mut rest = code.as_str();
61        while let Some(start) = rest.find("std::") {
62            let tail = &rest[start + "std::".len()..];
63            // `core::time` and `alloc::...` end in `std::` only as part of a
64            // longer name; require a path boundary before `std`.
65            let boundary = rest[..start]
66                .chars()
67                .next_back()
68                .is_none_or(|c| !(c.is_alphanumeric() || c == '_'));
69            if boundary && names_a_clock(tail) {
70                let end = tail.find(';').unwrap_or(tail.len()).min(80);
71                found.push(format!("std::{}", &tail[..end]));
72            }
73            rest = tail;
74        }
75        found
76    }
77
78    /// Collapses whitespace, and drops it entirely next to `:`, `{`, `}`,
79    /// `,` and `;`, so `std::{\n    time::Instant,\n}` reads `std::{time::Instant,}`.
80    fn tighten(code: &str) -> String {
81        let collapsed: Vec<&str> = code.split_whitespace().collect();
82        let mut out = String::new();
83        for word in collapsed {
84            let glue = |c: char| matches!(c, ':' | '{' | '}' | ',' | ';');
85            let joins =
86                out.chars().next_back().is_some_and(glue) || word.chars().next().is_some_and(glue);
87            if !out.is_empty() && !joins {
88                out.push(' ');
89            }
90            out.push_str(word);
91        }
92        out
93    }
94
95    /// Whether the path after a `std::` names a clock.
96    fn names_a_clock(tail: &str) -> bool {
97        if let Some(rest) = tail.strip_prefix("time::") {
98            return match rest.strip_prefix('{') {
99                Some(group) => top_level_items(group).iter().any(|item| {
100                    let item = item.split(" as ").next().unwrap_or("").trim();
101                    item == "self" || FORBIDDEN.contains(&item)
102                }),
103                None => FORBIDDEN.contains(&ident(rest)),
104            };
105        }
106        if let Some(rest) = tail.strip_prefix("time") {
107            // The module itself: `std::time;`, `std::time as t`, or an item
108            // of a group, `std::{time, ..}`.
109            return rest.is_empty() || rest.starts_with([';', ',', '}', ' ']);
110        }
111        match tail.strip_prefix('{') {
112            Some(group) => top_level_items(group)
113                .iter()
114                .any(|item| names_a_clock(item)),
115            None => false,
116        }
117    }
118
119    /// The leading identifier of `text`.
120    fn ident(text: &str) -> &str {
121        let end = text
122            .find(|c: char| !(c.is_alphanumeric() || c == '_'))
123            .unwrap_or(text.len());
124        &text[..end]
125    }
126
127    /// The comma-separated items of a brace group whose opening brace is
128    /// already consumed, split at depth zero only, up to the matching brace.
129    fn top_level_items(group: &str) -> Vec<&str> {
130        let mut items = Vec::new();
131        let (mut depth, mut from) = (0usize, 0usize);
132        for (at, c) in group.char_indices() {
133            match c {
134                '{' => depth += 1,
135                '}' if depth == 0 => {
136                    items.push(&group[from..at]);
137                    return items;
138                }
139                '}' => depth -= 1,
140                ',' if depth == 0 => {
141                    items.push(&group[from..at]);
142                    from = at + 1;
143                }
144                _ => {}
145            }
146        }
147        items.push(&group[from..]);
148        items
149    }
150
151    #[test]
152    fn the_clock_guard_sees_every_spelling() {
153        for named in [
154            "let now = std::time::Instant::now();",
155            "use std::time::{Duration, SystemTime};",
156            "use std::time::{UNIX_EPOCH as EPOCH};",
157            "use std::time::{self};",
158            "use std::time;",
159            "use std::{collections::HashMap, time::Instant};",
160            "use std::{\n    sync::Arc,\n    time::{\n        Duration,\n        Instant,\n    },\n};",
161            "use std::{sync::{Arc, Mutex}, time};",
162        ] {
163            assert!(!clock_imports(named).is_empty(), "missed: {named}");
164        }
165        for clean in [
166            "use std::time::Duration;",
167            "use std::{collections::HashMap, time::Duration};",
168            "use core::time::Duration;",
169            "use offline_protocol_core::time::Instant;",
170            "// std::time::Instant in a comment",
171            "let timeout = std::time::Duration::from_secs(1);",
172        ] {
173            assert!(clock_imports(clean).is_empty(), "false alarm: {clean}");
174        }
175    }
176
177    /// Every crate takes its clock from this module. A direct
178    /// `std::time::Instant` or `std::time::SystemTime` compiles and passes on
179    /// every host, and panics on the first call in a browser.
180    #[test]
181    fn clocks_are_read_through_core_time_only() {
182        let crates = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
183        // A published .crate archive has no sibling crates. Skip rather than
184        // fail: the check belongs to the workspace, not the package.
185        if !crates.join("offline-protocol").is_dir() {
186            return;
187        }
188        let mut offenders = Vec::new();
189        // The crates a browser build compiles. The FFI crate and the benches
190        // never run there, and the leaf has its own clock contract (a time
191        // source is a provisioning obligation, ADR 0021).
192        let browser_crates = [
193            "offline-protocol-core",
194            "offline-protocol-sealed",
195            "offline-protocol-transport",
196            "offline-protocol-reliability",
197            "offline-protocol-mls",
198            "offline-protocol-services",
199            "offline-protocol-data",
200            "offline-protocol-router",
201            "offline-protocol",
202            "offline-protocol-web",
203        ];
204        let mut stack: Vec<_> = browser_crates
205            .iter()
206            .map(|name| crates.join(name).join("src"))
207            .collect();
208        while let Some(dir) = stack.pop() {
209            let Ok(entries) = std::fs::read_dir(&dir) else {
210                continue;
211            };
212            for entry in entries.flatten() {
213                let path = entry.path();
214                let name = entry.file_name();
215                let name = name.to_string_lossy();
216                if path.is_dir() {
217                    stack.push(path);
218                    continue;
219                }
220                if !name.ends_with(".rs") || path.ends_with("offline-protocol-core/src/time.rs") {
221                    continue;
222                }
223                let Ok(source) = std::fs::read_to_string(&path) else {
224                    continue;
225                };
226                for fragment in clock_imports(&source) {
227                    offenders.push(format!("{}: {fragment}", path.display()));
228                }
229            }
230        }
231        assert!(
232            offenders.is_empty(),
233            "read the clock through `offline_protocol_core::time`, not `std::time` \
234             (it panics on wasm32-unknown-unknown):\n{}",
235            offenders.join("\n")
236        );
237    }
238}