Skip to main content

hashiverse_lib/tools/
tools.rs

1//! # Grab-bag of tiny cross-platform helpers
2//!
3//! Utility functions that don't fit in any of the more focused modules:
4//!
5//! - **Async yielding** ([`yield_now`]) — maps to `tokio::task::yield_now` on native,
6//!   `gloo_timers` on wasm32-unknown, and `tokio::time::sleep(0)` on wasi.
7//! - **Randomness** ([`random_fill_bytes`], [`random_bytes`], [`random_u32`]) — OS RNG
8//!   helpers used by key generation and PoW salt selection.
9//! - **Base64 and hex parsing** — consistent helpers used wherever we need to emit or
10//!   accept textual byte blobs (key persistence, URLs, HTML attributes).
11//! - **Byte reversal** used by server-id PoW hash-to-id mapping.
12//! - **`LeadingAgreementBits`** typedef for the XOR-distance metric used by the DHT.
13//! - **Logging bootstrap** (`tracing_subscriber` initialisation with consistent
14//!   formatting across native and wasm).
15//! - **`Cancellable`-style async helpers** that plug into `CancellationToken`.
16//!
17//! Anything here that grows a meaningful amount of functionality should graduate to
18//! its own module.
19
20use crate::tools::json;
21use crate::tools::time::DurationMillis;
22use crate::tools::time_provider::time_provider::{RealTimeProvider, TimeProvider};
23use crate::tools::BytesGatherer;
24use base64::Engine;
25use bytes::Bytes;
26use log::info;
27use std::fmt;
28use std::future::Future;
29use std::sync::Arc;
30use tokio_util::sync::CancellationToken;
31use tracing_subscriber::fmt::time::FormatTime;
32use tracing_subscriber::layer::SubscriberExt;
33use tracing_subscriber::util::SubscriberInitExt;
34
35pub type LeadingAgreementBits = i32;
36
37pub async fn yield_now() {
38    #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
39    {
40        send_wrapper::SendWrapper::new(gloo_timers::future::TimeoutFuture::new(0)).await;
41    }
42    #[cfg(all(target_arch = "wasm32", target_os = "wasi"))]
43    {
44        tokio::time::sleep(std::time::Duration::from_millis(0u64)).await;
45    }
46    #[cfg(not(target_arch = "wasm32"))]
47    {
48        // On native platforms, use Tokio's optimized yield.
49        tokio::task::yield_now().await;
50    }
51}
52
53pub fn random_fill_bytes(dest: &mut [u8]) {
54    getrandom_4::fill(dest).expect("OS random number generator failed");
55}
56
57pub fn random_bytes(n: usize) -> Vec<u8> {
58    let mut dest = vec![0u8; n];
59    random_fill_bytes(&mut dest);
60    dest
61}
62
63pub fn reverse_bytes<const N: usize>(bytes: &[u8; N]) -> [u8; N] {
64    let mut result = [0u8; N];
65    for (i, &byte) in bytes.iter().rev().enumerate() {
66        result[i] = byte;
67    }
68    result
69}
70
71pub fn random_u32() -> u32 {
72    getrandom_4::u32().expect("OS random number generator failed")
73}
74
75#[cfg(target_pointer_width = "64")]
76pub fn random_usize() -> usize {
77    getrandom_4::u64().expect("OS random number generator failed") as usize
78}
79
80#[cfg(target_pointer_width = "32")]
81pub fn random_usize() -> usize {
82    getrandom_4::u32().expect("OS random number generator failed") as usize
83}
84
85pub fn random_usize_bounded(upper: usize) -> usize {
86    // Rejection sampling to avoid modulo bias.
87    // We accept values in [0, zone) where zone is the largest multiple of `upper`.
88    let zone = usize::MAX - (usize::MAX % upper);
89
90    loop {
91        let r = random_usize();
92        if r < zone {
93            return r % upper;
94        }
95    }
96}
97
98pub fn random_u8() -> u8 {
99    getrandom_4::u32().expect("OS random number generator failed") as u8
100}
101
102pub fn random_base64(length: usize) -> String {
103    let mut bytes = vec![0u8; length];
104    random_fill_bytes(&mut bytes);
105    encode_base64(bytes)
106}
107
108pub fn are_all_zeros<T: PartialEq + num_traits::Zero>(src: &[T]) -> bool {
109    src.iter().all(|b| *b == T::zero())
110}
111
112pub fn are_all_equal<T: PartialEq>(src1: &[T], src2: &[T]) -> bool {
113    if src1.len() != src2.len() {
114        return false;
115    }
116    src1.iter().zip(src2).all(|(a, b)| a == b)
117}
118
119pub fn count_leading_zero_bits(bytes: &[u8]) -> u8 {
120    let mut count = 0u64;
121
122    for &byte in bytes {
123        if byte == 0 {
124            count += 8;
125            continue;
126        }
127
128        // Count leading zeros in the non-zero byte
129        let mut mask = 0x80; // 10000000 in binary
130        while byte & mask == 0 {
131            count += 1;
132            mask >>= 1;
133        }
134
135        break; // Exit after processing the first non-zero byte
136    }
137
138    if count < 256 { count as u8 } else { 255 }
139}
140
141pub async fn cancellable_sleep_millis(time_provider: &dyn TimeProvider, millis: DurationMillis, cancellation_token: &CancellationToken) {
142    tokio::select! {
143        _ = time_provider.sleep_millis(millis) => {},
144        _ = cancellation_token.cancelled() => {},
145    }
146}
147
148pub fn format_vec<T: std::fmt::Display>(items: &[T]) -> String {
149    format!("[ {} ]", items.iter().map(|item| format!("{}", item)).collect::<Vec<_>>().join(", "))
150}
151
152pub fn leading_agreement_bits_xor(key1: &[u8], key2: &[u8]) -> LeadingAgreementBits {
153    let mut leading_bits_in_agreement: i32 = 0;
154
155    let min_len = std::cmp::min(key1.len(), key2.len());
156    for byte_idx in 0..min_len {
157        let xor = key1[byte_idx] ^ key2[byte_idx];
158
159        // Do we have differing bytes?
160        if xor != 0 {
161            leading_bits_in_agreement += xor.leading_zeros() as LeadingAgreementBits;
162            return leading_bits_in_agreement;
163        }
164        else {
165            leading_bits_in_agreement += 8;
166        }
167    }
168
169    leading_bits_in_agreement
170}
171
172pub fn encode_base64<T: AsRef<[u8]>>(input: T) -> String {
173    base64::engine::general_purpose::STANDARD.encode(&input)
174}
175
176pub fn decode_base64<T: AsRef<[u8]>>(input: T) -> anyhow::Result<Vec<u8>> {
177    Ok(base64::engine::general_purpose::STANDARD.decode(input)?)
178}
179
180pub fn usize_encode_le64(v: usize) -> [u8; 8] {
181    u64::to_le_bytes(v as u64)
182}
183
184pub fn usize_decode_le64(v_bytes: &[u8]) -> anyhow::Result<usize> {
185    let v = u64::from_le_bytes(v_bytes.try_into()?);
186    Ok(v as usize)
187}
188
189pub fn write_length_prefixed_json<T: serde::Serialize>(bytes_gatherer: &mut BytesGatherer, value: &T) -> anyhow::Result<()> {
190    let json_bytes = json::struct_to_bytes(value)?;
191    bytes_gatherer.put_u64(json_bytes.len() as u64);
192    bytes_gatherer.put_bytes(json_bytes);
193    Ok(())
194}
195pub fn read_length_prefixed_json<T: serde::de::DeserializeOwned>(bytes: &mut Bytes) -> anyhow::Result<T> {
196    use bytes::Buf;
197
198    if bytes.remaining() < 8 {
199        anyhow::bail!("Invalid buffer: missing json length");
200    }
201
202    let len = bytes.get_u64() as usize;
203
204    if bytes.remaining() < len {
205        anyhow::bail!("Invalid buffer: json data truncated");
206    }
207
208    let json_bytes = bytes.copy_to_bytes(len);
209    json::bytes_to_struct::<T>(&json_bytes)
210}
211
212pub fn random_element<T>(range: &[T]) -> &T {
213    let index = random_usize_bounded(range.len());
214    &range[index]
215}
216
217pub fn shuffle<T>(source: &mut [T]) {
218    // Fisher–Yates / Knuth shuffle (uniform)
219    for i in 1..source.len() {
220        let j = random_usize_bounded(i + 1);
221        source.swap(i, j);
222    }
223}
224
225pub struct CustomTimeFormatter {
226    time_provider: Arc<dyn TimeProvider>,
227}
228
229impl CustomTimeFormatter {
230    pub fn new(time_provider: Arc<dyn TimeProvider>) -> Self {
231        Self { time_provider }
232    }
233}
234
235impl FormatTime for CustomTimeFormatter {
236    fn format_time(&self, w: &mut tracing_subscriber::fmt::format::Writer<'_>) -> fmt::Result {
237        write!(w, "{}", self.time_provider.current_time_str())
238    }
239}
240
241pub fn configure_logging() {
242    configure_logging_with_time_provider("trace", Arc::new(RealTimeProvider))
243}
244
245pub fn configure_logging_with_time_provider(level: &str, time_provider: Arc<dyn TimeProvider>) {
246    // The filter
247    let filter = format!("{},hyper=off,warp=off,reqwest=off,rustls=off,h2=off,h2=off,html5ever=off,selectors=off,fjall=off,lsm_tree=off,sfa=off,hickory_resolver=off,hickory_proto=off", level);
248    let env_filter = tracing_subscriber::EnvFilter::new(&filter);
249
250    // Prepare the Standard Logging Layer
251    let fmt_layer = tracing_subscriber::fmt::layer().with_timer(CustomTimeFormatter::new(time_provider));
252
253    let registry = tracing_subscriber::registry();
254
255    // Prepare the Console Layer (Conditional) - we only enable this if the 'tokio_unstable' cfg is present and we are not WASM
256    #[cfg(all(tokio_unstable, not(target_arch = "wasm32")))]
257    registry.with(console_subscriber::spawn());
258
259    // Register everything
260    registry.with(fmt_layer).with(env_filter).init();
261
262    info!("Logging initialized");
263}
264
265#[cfg(not(target_arch = "wasm32"))]
266pub type TempDirHandle = tempfile::TempDir;
267
268#[cfg(not(target_arch = "wasm32"))]
269pub fn get_temp_dir() -> anyhow::Result<(TempDirHandle, String)> {
270    let mut base = std::env::temp_dir();
271    base.push("hashiverse-temp");
272
273    // Ensure the base directory exists
274    std::fs::create_dir_all(&base)?;
275
276    let temp_dir = tempfile::Builder::new().prefix("hashiverse-").tempdir_in(&base)?;
277    let temp_dir_path = temp_dir.path().to_str().unwrap().to_string();
278    Ok((temp_dir, temp_dir_path))
279}
280
281#[cfg(target_arch = "wasm32")]
282pub type TempDirHandle = ();
283
284#[cfg(target_arch = "wasm32")]
285pub fn get_temp_dir() -> anyhow::Result<(TempDirHandle, String)> {
286    Ok(((), "".to_string()))
287}
288
289pub fn from_hex_str<T, const T_BYTES: usize>(str: &str, ctor: impl FnOnce([u8; T_BYTES]) -> T) -> anyhow::Result<T> {
290    if str.len() != 2 * T_BYTES {
291        anyhow::bail!("Invalid hex string length: expected {} hex characters ({} bytes), got {} characters.", 2 * T_BYTES, T_BYTES, str.len(),);
292    }
293
294    // Try to decode the hex string
295    let decoded = hex::decode(str)?;
296
297    // Check if the decoded bytes are exactly xxx bytes
298    if decoded.len() != T_BYTES {
299        anyhow::bail!("Invalid hex string length: expected {} bytes, got {} bytes", T_BYTES, decoded.len());
300    }
301
302    // Convert Vec<u8> to [u8; xxx]
303    let mut decoded_bytes = [0u8; T_BYTES];
304    decoded_bytes.copy_from_slice(&decoded);
305
306    Ok(ctor(decoded_bytes))
307}
308
309/// Spawn a background async task, using the appropriate runtime for the current target.
310///
311/// Only the native path requires `Send`: `tokio::spawn` runs the task on the multi-threaded
312/// runtime. Both wasm targets are single-threaded and impose no `Send` bound — important because
313/// browser-backed futures (IndexedDB storage, `gloo-net` requests) and the `async_trait(?Send)`
314/// client traits are not `Send`. The browser (`wasm32-unknown-unknown`) drives the task on the JS
315/// event loop via `wasm_bindgen_futures`; the wasmtime test target (`wasm32-wasip1`) uses tokio's
316/// thread-local spawner.
317#[cfg(not(target_arch = "wasm32"))]
318pub fn spawn_background_task<F>(task: F)
319where
320    F: Future<Output = ()> + Send + 'static,
321{
322    tokio::spawn(task);
323}
324
325#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
326pub fn spawn_background_task<F>(task: F)
327where
328    F: Future<Output = ()> + 'static,
329{
330    wasm_bindgen_futures::spawn_local(task);
331}
332
333#[cfg(all(target_arch = "wasm32", not(target_os = "unknown")))]
334pub fn spawn_background_task<F>(task: F)
335where
336    F: Future<Output = ()> + 'static,
337{
338    tokio::task::spawn_local(task);
339}
340
341#[cfg(test)]
342mod tests {
343    #[tokio::test]
344    async fn xor_distance_bits_test() -> anyhow::Result<()> {
345        use crate::tools::tools::leading_agreement_bits_xor;
346
347        let tests = [
348            // Identical
349            ("0000", "0000", 16),
350            ("ffff", "ffff", 16),
351            ("1234", "1234", 16),
352            ("abcd", "abcd", 16),
353            // MSB
354            ("0000", "ffff", 0),
355            ("0000", "0fff", 4),
356            ("0000", "00ff", 8),
357            ("0000", "000f", 12),
358            // Units
359            ("0000", "efff", 0),
360            ("0000", "7fff", 1),
361            ("0000", "3fff", 2),
362            ("0000", "1fff", 3),
363            ("0000", "0fff", 4),
364            ("0000", "07ff", 5),
365            ("0000", "03ff", 6),
366            ("0000", "01ff", 7),
367            ("0000", "00ff", 8),
368            ("0000", "007f", 9),
369            ("0000", "003f", 10),
370            ("0000", "001f", 11),
371            ("0000", "000f", 12),
372            ("0000", "0007", 13),
373            ("0000", "0003", 14),
374            ("0000", "0001", 15),
375            // MSB + random
376            ("0000", "fff9", 0),
377            ("0000", "0ff9", 4),
378            ("0000", "00f9", 8),
379            // Different lengths
380            ("", "0000", 0),
381            ("00", "0000", 8),
382            ("0000", "000000", 16),
383        ];
384
385        for (a, b, expected) in tests {
386            let a_binary = hex::decode(a)?;
387            let b_binary = hex::decode(b)?;
388            {
389                let distance = leading_agreement_bits_xor(&a_binary, &b_binary);
390                assert_eq!(distance, expected, "Failed for {} and {}.  Got {} expected {}.", a, b, distance, expected);
391            }
392            {
393                let distance = leading_agreement_bits_xor(&b_binary, &a_binary);
394                assert_eq!(distance, expected, "Failed for {} and {}.  Got {} expected {}.", a, b, distance, expected);
395            }
396        }
397        Ok(())
398    }
399}
400