ya_rand/
lib.rs

1/*!
2# YA-Rand: Yet Another Rand
3
4Simple and fast pseudo/crypto random number generation.
5
6## Performance considerations
7
8The backing CRNG uses compile-time dispatch, so you'll only get the fastest implementation available to the
9machine if rustc knows what kind of machine to compile for.
10If you know the [x86 feature level] of the processor that will be running your binaries, tell rustc to
11target that feature level. On Windows, this means adding `RUSTFLAGS=-C target-cpu=<level>` to your system
12variables in System Properties -> Advanced -> Environment Variables. You can also manually toggle this for
13a single cmd-prompt instance using the [`set`] command. On Unix-based systems the process should be similar.
14If you're only going to run the final binary on your personal machine, replace `<level>` with `native`.
15
16If you happen to be building with a nightly toolchain, and for a machine supporting AVX512, the **nightly**
17feature provides an AVX512F implementation of the backing ChaCha algorithm.
18
19[x86 feature level]: https://en.wikipedia.org/wiki/X86-64#Microarchitecture_levels
20[`set`]: https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/set_1
21
22## But why?
23
24Because `rand` is very cool and extremely powerful, but kind of an enormous fucking pain in the ass
25to use, and it's far too large and involved for someone who just needs to flip a coin once
26every few minutes. But if you're doing some crazy black magic computational sorcery, it almost
27certainly has something you can use to complete your spell.
28
29Other crates, like `fastrand`, `tinyrand`, or `oorandom`, fall somewhere between "I'm not sure I trust
30the backing RNG" (state size is too small or algorithm is iffy) and "this API is literally just
31`rand` but far less powerful". I wanted something easy, but also fast and statistically robust.
32
33So here we are.
34
35## Usage
36
37Glob import the contents of the library and use [`new_rng`] to create new RNGs wherever
38you need them. Then call whatever method you require on those instances. All methods available
39are directly accessible through any generator instance via the dot operator, and are named
40in a way that should make it easy to quickly identify what you need. See below for a few examples.
41
42If you need cryptographic security, [`new_rng_secure`] will provide you with a [`SecureRng`] instance,
43suitable for use in secure contexts.
44
45"How do I access the thread-local RNG?" There isn't one, and unless Rust improves the performance and
46ergonomics of the TLS implementation, there probably won't ever be. Create a local instance when and
47where you need one and use it while you need it. If you need an RNG to stick around for a while, passing
48it between functions or storing it in structs is a perfectly valid solution.
49
50```
51use ya_rand::*;
52
53// **Correct** instantiation is very easy.
54// Seeds a PRNG instance using operating system entropy,
55// so you never have to worry about the quality of the
56// initial state.
57let mut rng = new_rng();
58
59// Generate a random number with a given upper bound.
60let max: u64 = 420;
61let val = rng.bound(max);
62assert!(val < max);
63
64// Generate a random number in a given range.
65let min: i64 = -69;
66let max: i64 = 69;
67let val = rng.range(min, max);
68assert!(min <= val && val < max);
69
70// Generate a random floating point value.
71let val = rng.f64();
72assert!(0.0 <= val && val < 1.0);
73
74// Generate a random ascii digit: '0'..='9' as a char.
75let digit = rng.ascii_digit();
76assert!(digit.is_ascii_digit());
77
78// Seeds a CRNG instance with OS entropy.
79let mut secure_rng = new_rng_secure();
80
81// We still have access to all the same methods...
82let val = rng.f64();
83assert!(0.0 <= val && val < 1.0);
84
85// ...but since the CRNG is secure, we also
86// get some nice extras.
87// Here, we generate a string of random hexidecimal
88// characters (base 16), with the shortest length guaranteed
89// to be secure.
90use ya_rand_encoding::*;
91let s = secure_rng.text::<Base16>(Base16::MIN_LEN).unwrap();
92assert!(s.len() == Base16::MIN_LEN);
93```
94
95## Features
96
97* **std** -
98    Enabled by default, but can be disabled for use in `no_std` environments. Enables normal/exponential
99    distributions, error type conversions for getrandom, and the **alloc** feature.
100* **alloc** -
101    Enabled by default. Normally enabled through **std**, but can be enabled on it's own for use in
102    `no_std` environments that provide allocation primitives. Enables random generation of secure
103    `String` values when using [`SecureRng`].
104* **inline** -
105    Marks all [`YARandGenerator::u64`] implementations with #\[inline\]. Should generally increase
106    runtime performance at the cost of binary size and compile time.
107    You'll have to test your specific use case to determine if this feature is worth it for you;
108    all the RNGs provided tend to be plenty fast without additional inlining.
109* **nightly** -
110    Enables AVX512F [`SecureRng`] implementation on targets that support it.
111
112## Details
113
114This crate uses the [xoshiro] family for pseudo-random number generators. These generators are
115very fast, of [very high statistical quality], and small. They aren't cryptographically secure,
116but most users don't need their RNG to be secure, they just need it to be random and fast. The default
117generator is xoshiro256++, which should provide a large enough period for most users. The xoshiro512++
118generator is also provided in case you need a longer period.
119
120[xoshiro]: https://prng.di.unimi.it/
121[very high statistical quality]: https://vigna.di.unimi.it/ftp/papers/ScrambledLinear.pdf
122
123All generators output a distinct `u64` value on each call, and the various methods used for transforming
124those outputs into more usable forms are all high-quality and well-understood. Placing an upper bound
125on these values uses [Lemire's method]. Both inclusive bounding and range-based bounding are applications
126of this method, with a few intermediary steps to adjust the bound and apply shifts as needed.
127This approach is unbiased and quite fast, but for very large bounds performance might degrade slightly,
128since the algorithm may need to sample the underlying RNG multiple times to get an unbiased result.
129But this is just a byproduct of how the underlying algorithm works, and isn't something you should ever be
130worried about when using the aforementioned methods, since these resamples are few and far between.
131If your bound happens to be a power of 2, always use [`YARandGenerator::bits`], since it's nothing more
132than a bit-shift of the original `u64` provided by the RNG, and will always be as fast as possible.
133
134Floating point values (besides the normal and exponential distributions) are uniformly distributed,
135with all the possible outputs being equidistant within the given interval. They are **not** maximally dense;
136if that's something you need, you'll have to generate those values yourself. This approach is very fast, and
137endorsed by both [Lemire] and [Vigna] (the author of the RNGs used in this crate). The normal distribution
138implementation uses the [Marsaglia polar method], returning pairs of independently sampled `f64` values.
139Exponential variates are generated using [this approach].
140
141[Lemire's method]: https://arxiv.org/abs/1805.10941
142[Lemire]: https://lemire.me/blog/2017/02/28/how-many-floating-point-numbers-are-in-the-interval-01/
143[Vigna]: https://prng.di.unimi.it/#remarks
144[Marsaglia polar method]: https://en.wikipedia.org/wiki/Marsaglia_polar_method
145[this approach]: https://en.wikipedia.org/wiki/Exponential_distribution#Random_variate_generation
146
147## Security
148
149If you're in the market for secure random number generation, this crate provides a secure generator backed
150by a highly optimized ChaCha8 implementation from the [`chachacha`] crate.
151It functions identically to the other provided RNGs, but with added functionality that wouldn't be safe to
152use on pseudo RNGs. Why only 8 rounds? Because people who are very passionate about cryptography are convinced
153that's enough, and I have zero reason to doubt them, nor any capacity to prove them wrong.
154See page 14 of the [`Too Much Crypto`] paper if you're interested in the justification.
155
156The security guarantees made to the user are identical to those made by ChaCha as an algorithm. It is up
157to you to determine if those guarantees meet the demands of your use case.
158
159I reserve the right to change the backing implementation at any time to another RNG which is at least as secure,
160without changing the API or bumping the major/minor version. Realistically, this just means I'm willing to bump
161this to ChaCha12 if ChaCha8 is ever compromised.
162
163[`Too Much Crypto`]: https://eprint.iacr.org/2019/1492
164
165## Safety
166
167Generators are seeded using entropy from the underlying OS, and have the potential to fail during creation.
168But in practice this is extraordinarily unlikely, and isn't something the end-user should ever worry about.
169Modern Windows versions (10 and newer) have a crypto subsystem that will never fail during runtime, and
170rustc can trivially remove the failure branch when compiling binaries for those systems.
171*/
172
173#![deny(missing_docs)]
174#![no_std]
175
176#[cfg(feature = "alloc")]
177extern crate alloc;
178
179#[cfg(feature = "alloc")]
180mod encoding;
181
182/// Module providing the [`Encoder`](crate::encoding::Encoder) trait and concrete implementations
183/// of the [RFC 4648](https://datatracker.ietf.org/doc/html/rfc4648) encoding schemes.
184#[cfg(feature = "alloc")]
185pub mod ya_rand_encoding {
186    pub use super::encoding::*;
187}
188
189mod rng;
190#[cfg(feature = "secure")]
191mod secure;
192mod util;
193mod xoshiro256pp;
194mod xoshiro512pp;
195
196pub use rng::{SecureYARandGenerator, SeedableYARandGenerator, YARandGenerator};
197#[cfg(feature = "secure")]
198pub use secure::SecureRng;
199pub use xoshiro256pp::Xoshiro256pp;
200pub use xoshiro512pp::Xoshiro512pp;
201
202/// The recommended generator for all non-cryptographic purposes.
203pub type ShiroRng = Xoshiro256pp;
204
205/// The recommended way to create new PRNG instances.
206///
207/// Identical to calling [`ShiroRng::new`] or [`Xoshiro256pp::new`].
208#[inline]
209pub fn new_rng() -> ShiroRng {
210    ShiroRng::new()
211}
212
213/// The recommended way to create new CRNG instances.
214///
215/// Identical to calling [`SecureRng::new`].
216#[cfg(feature = "secure")]
217#[inline]
218pub fn new_rng_secure() -> SecureRng {
219    SecureRng::new()
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225    use alloc::collections::BTreeSet;
226    use ya_rand_encoding::*;
227
228    const ITERATIONS: usize = 10007;
229    const ITERATIONS_LONG: usize = 1 << 24;
230
231    #[test]
232    pub fn ascii_alphabetic() {
233        let mut rng = new_rng();
234        let mut vals = BTreeSet::new();
235        for _ in 0..ITERATIONS {
236            let result = rng.ascii_alphabetic();
237            assert!(result.is_ascii_alphabetic());
238            vals.insert(result);
239        }
240        assert!(vals.len() == 52);
241    }
242
243    #[test]
244    pub fn ascii_uppercase() {
245        let mut rng = new_rng();
246        let mut vals = BTreeSet::new();
247        for _ in 0..ITERATIONS {
248            let result = rng.ascii_uppercase();
249            assert!(result.is_ascii_uppercase());
250            vals.insert(result);
251        }
252        assert!(vals.len() == 26);
253    }
254
255    #[test]
256    pub fn ascii_lowercase() {
257        let mut rng = new_rng();
258        let mut vals = BTreeSet::new();
259        for _ in 0..ITERATIONS {
260            let result = rng.ascii_lowercase();
261            assert!(result.is_ascii_lowercase());
262            vals.insert(result);
263        }
264        assert!(vals.len() == 26);
265    }
266
267    #[test]
268    pub fn ascii_alphanumeric() {
269        let mut rng = new_rng();
270        let mut vals = BTreeSet::new();
271        for _ in 0..ITERATIONS {
272            let result = rng.ascii_alphanumeric();
273            assert!(result.is_ascii_alphanumeric());
274            vals.insert(result);
275        }
276        assert!(vals.len() == 62);
277    }
278
279    #[test]
280    pub fn ascii_digit() {
281        let mut rng = new_rng();
282        let mut vals = BTreeSet::new();
283        for _ in 0..ITERATIONS {
284            let result = rng.ascii_digit();
285            assert!(result.is_ascii_digit());
286            vals.insert(result);
287        }
288        assert!(vals.len() == 10);
289    }
290
291    #[test]
292    fn text_base64() {
293        test_text::<Base64>();
294    }
295
296    #[test]
297    fn text_base64_url() {
298        test_text::<Base64URL>();
299    }
300
301    #[test]
302    fn text_base62() {
303        test_text::<Base62>();
304    }
305
306    #[test]
307    fn text_base32() {
308        test_text::<Base32>();
309    }
310
311    #[test]
312    fn text_base32_hex() {
313        test_text::<Base32Hex>();
314    }
315
316    #[test]
317    fn text_base16() {
318        test_text::<Base16>();
319    }
320
321    #[test]
322    fn text_base16_lowercase() {
323        test_text::<Base16Lowercase>();
324    }
325
326    fn test_text<E: Encoder>() {
327        let s = new_rng_secure().text::<E>(ITERATIONS).unwrap();
328        let distinct_bytes = s.bytes().collect::<BTreeSet<_>>();
329        let distinct_chars = s.chars().collect::<BTreeSet<_>>();
330
331        let lengths_are_equal = ITERATIONS == s.len()
332            && E::CHARSET.len() == distinct_bytes.len()
333            && E::CHARSET.len() == distinct_chars.len();
334        assert!(lengths_are_equal);
335
336        let contains_all_values = E::CHARSET.iter().all(|c| distinct_bytes.contains(c));
337        assert!(contains_all_values);
338    }
339
340    #[test]
341    fn wide_mul() {
342        const SHIFT: u32 = 48;
343        const EXPECTED_HIGH: u64 = 1 << ((SHIFT * 2) - u64::BITS);
344        const EXPECTED_LOW: u64 = 0;
345        let x = 1 << SHIFT;
346        let y = x;
347        // 2^48 * 2^48 = 2^96
348        let (high, low) = util::wide_mul(x, y);
349        assert!(high == EXPECTED_HIGH);
350        assert!(low == EXPECTED_LOW);
351    }
352
353    #[test]
354    fn f64() {
355        let mut rng = new_rng();
356        for _ in 0..ITERATIONS_LONG {
357            let val = rng.f64();
358            assert!(0.0 <= val && val < 1.0);
359        }
360    }
361
362    #[test]
363    fn f32() {
364        let mut rng = new_rng();
365        for _ in 0..ITERATIONS_LONG {
366            let val = rng.f32();
367            assert!(0.0 <= val && val < 1.0);
368        }
369    }
370
371    #[test]
372    fn f64_nonzero() {
373        let mut rng = new_rng();
374        for _ in 0..ITERATIONS_LONG {
375            let val = rng.f64_nonzero();
376            assert!(0.0 < val && val <= 1.0);
377        }
378    }
379
380    #[test]
381    fn f32_nonzero() {
382        let mut rng = new_rng();
383        for _ in 0..ITERATIONS_LONG {
384            let val = rng.f32_nonzero();
385            assert!(0.0 < val && val <= 1.0);
386        }
387    }
388
389    #[test]
390    fn f64_wide() {
391        let mut rng = new_rng();
392        for _ in 0..ITERATIONS_LONG {
393            let val = rng.f64_wide();
394            assert!(val.abs() < 1.0);
395        }
396    }
397
398    #[test]
399    fn f32_wide() {
400        let mut rng = new_rng();
401        for _ in 0..ITERATIONS_LONG {
402            let val = rng.f32_wide();
403            assert!(val.abs() < 1.0);
404        }
405    }
406}