ya_rand/
lib.rs

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