offline_protocol_core/
time.rs1#[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 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 const FORBIDDEN: [&str; 3] = ["Instant", "SystemTime", "UNIX_EPOCH"];
44
45 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 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 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 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 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 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 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 #[test]
181 fn clocks_are_read_through_core_time_only() {
182 let crates = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
183 if !crates.join("offline-protocol").is_dir() {
186 return;
187 }
188 let mut offenders = Vec::new();
189 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}