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