Skip to main content

tuwunel_core/utils/
rand.rs

1//! Random value generation and randomized truncation helpers.
2//!
3//! These helpers use the thread-local generator for strings, indexes, shuffles,
4//! durations, and event identifiers. Range arguments use half-open semantics.
5
6use std::{
7	iter::repeat_with,
8	ops::Range,
9	time::{Duration, SystemTime},
10};
11
12use arrayvec::ArrayString;
13use rand::{RngExt, rng, seq::SliceRandom};
14use ruma::OwnedEventId;
15
16/// Randomly permutes a slice in place.
17///
18/// The thread-local generator chooses the permutation without changing the
19/// slice's contents or length. Empty and single-element slices remain
20/// unchanged.
21pub fn shuffle<T>(vec: &mut [T]) {
22	let mut rng = rng();
23	vec.shuffle(&mut rng);
24}
25
26/// Chooses a uniformly random index below `len`.
27///
28/// A zero length returns `0` instead of sampling an empty range. For any
29/// nonzero length, the result lies in `0..len`.
30#[must_use]
31pub fn index(len: usize) -> usize {
32	match len {
33		| 0 => 0,
34		| len => rng().random_range(0..len),
35	}
36}
37
38/// Generates an alphanumeric ASCII string of the requested byte length.
39///
40/// Each character is sampled independently with the thread-local generator.
41/// Because the alphabet is ASCII, the character and byte lengths are equal.
42pub fn string(length: usize) -> String {
43	rng()
44		.sample_iter(&rand::distr::Alphanumeric)
45		.take(length)
46		.map(char::from)
47		.collect()
48}
49
50/// Generates a string of `length` characters sampled from `charset`.
51///
52/// Each byte becomes the Unicode scalar with the same numeric value, and
53/// samples are uniform with replacement. Sampling panics when `charset` is
54/// empty and a positive length is requested.
55#[must_use]
56pub fn string_from(charset: &[u8], length: usize) -> String {
57	let mut rng = rng();
58
59	repeat_with(|| char::from(charset[rng.random_range(0..charset.len())]))
60		.take(length)
61		.collect()
62}
63
64/// Generates an alphanumeric ASCII string that fills a fixed-capacity array.
65///
66/// Each sample occupies one byte, so the returned length and capacity are both
67/// `LENGTH`. The [`ArrayString`] stores the result without heap allocation.
68#[inline]
69pub fn string_array<const LENGTH: usize>() -> ArrayString<LENGTH> {
70	let mut ret = ArrayString::<LENGTH>::new();
71	rng()
72		.sample_iter(&rand::distr::Alphanumeric)
73		.take(LENGTH)
74		.map(char::from)
75		.for_each(|c| ret.push(c));
76
77	ret
78}
79
80/// Generates a Matrix event identifier from 32 random bytes.
81///
82/// The bytes use URL-safe base64 without padding, producing a 43-character
83/// localpart after the `$` sigil. The identifier has no server-name component.
84#[must_use]
85pub fn event_id() -> OwnedEventId {
86	use base64::{
87		Engine,
88		alphabet::URL_SAFE,
89		engine::{GeneralPurpose, general_purpose::NO_PAD},
90	};
91
92	let mut binary: [u8; 32] = [0; _];
93	rand::fill(&mut binary);
94
95	let mut encoded: [u8; 43] = [0; _];
96	GeneralPurpose::new(&URL_SAFE, NO_PAD)
97		.encode_slice(binary, &mut encoded)
98		.expect("Failed to encode binary to base64");
99
100	let event_id: &str = str::from_utf8(&encoded)
101		.expect("Failed to convert array of base64 bytes to valid utf8 str");
102
103	OwnedEventId::from_parts('$', event_id, None)
104		.expect("Failed to generate valid random event_id")
105}
106
107/// Truncates an owned string at a randomly selected character count.
108///
109/// The count is sampled from the half-open range and never splits a UTF-8
110/// scalar. A count at or beyond the string's character count leaves it intact;
111/// an invalid or empty range panics.
112#[must_use]
113pub fn truncate_string(mut str: String, range: Range<u64>) -> String {
114	let len = rng()
115		.random_range(range)
116		.try_into()
117		.unwrap_or(usize::MAX);
118
119	if let Some((i, _)) = str.char_indices().nth(len) {
120		str.truncate(i);
121	}
122
123	str
124}
125
126/// Borrows a prefix ending at a randomly selected character count.
127///
128/// The count is sampled from the half-open range and never splits a UTF-8
129/// scalar. A count at or beyond the string's character count returns the full
130/// input; an invalid or empty range panics.
131#[inline]
132#[must_use]
133pub fn truncate_str(str: &str, range: Range<u64>) -> &str {
134	let len = rng()
135		.random_range(range)
136		.try_into()
137		.unwrap_or(usize::MAX);
138
139	str.char_indices()
140		.nth(len)
141		.map(|(i, _)| str.split_at(i).0)
142		.unwrap_or(str)
143}
144
145/// Adds a random whole-second offset to the current [`SystemTime`].
146///
147/// The offset is sampled from the supplied half-open range. The function panics
148/// if the range is invalid or the addition exceeds [`SystemTime`].
149#[inline]
150#[must_use]
151pub fn time_from_now_secs(range: Range<u64>) -> SystemTime {
152	SystemTime::now()
153		.checked_add(secs(range))
154		.expect("range does not overflow SystemTime")
155}
156
157/// Generates a [`Duration`] with a random whole-second length.
158///
159/// The number of seconds is sampled uniformly from the supplied half-open
160/// range. An invalid or empty range panics.
161#[must_use]
162pub fn secs(range: Range<u64>) -> Duration {
163	let mut rng = rng();
164	Duration::from_secs(rng.random_range(range))
165}