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}