spinoso_random/lib.rs
1#![warn(clippy::all, clippy::pedantic, clippy::undocumented_unsafe_blocks)]
2#![allow(
3 clippy::let_underscore_untyped,
4 reason = "https://github.com/rust-lang/rust-clippy/pull/10442#issuecomment-1516570154"
5)]
6#![allow(
7 clippy::question_mark,
8 reason = "https://github.com/rust-lang/rust-clippy/issues/8281"
9)]
10#![allow(clippy::manual_let_else, reason = "manual_let_else was very buggy on release")]
11#![allow(clippy::missing_errors_doc, reason = "A lot of existing code fails this lint")]
12#![allow(
13 clippy::unnecessary_lazy_evaluations,
14 reason = "https://github.com/rust-lang/rust-clippy/issues/8109"
15)]
16#![cfg_attr(
17 test,
18 allow(clippy::non_ascii_literal, reason = "tests sometimes require UTF-8 string content")
19)]
20#![allow(unknown_lints)]
21#![warn(
22 missing_copy_implementations,
23 missing_debug_implementations,
24 missing_docs,
25 rust_2024_compatibility,
26 trivial_casts,
27 trivial_numeric_casts,
28 unused_qualifications,
29 variant_size_differences
30)]
31#![forbid(unsafe_code)]
32// Enable feature callouts in generated documentation:
33// https://doc.rust-lang.org/beta/unstable-book/language-features/doc-cfg.html
34//
35// This approach is borrowed from tokio.
36#![cfg_attr(docsrs, feature(doc_cfg))]
37#![cfg_attr(docsrs, feature(doc_alias))]
38
39//! An implementation of [Ruby's pseudo-random number generator][ruby-random],
40//! or PRNG.
41//!
42//! The PRNG produces a deterministic sequence of bits which approximate true
43//! randomness. The sequence may be represented by integers, floats, or binary
44//! strings.
45//!
46//! The generator may be initialized with either a system-generated or
47//! user-supplied seed value.
48//!
49//! PRNGs are currently implemented as a modified Mersenne Twister with a period
50//! of 2**19937-1.
51//!
52//! # Implementation notes
53//!
54//! This RNG reproduces the same random bytes and floats as MRI. It may differ
55//! when returning elements confined to a distribution.
56//!
57//! # Examples
58//!
59//! Generate integers:
60//!
61//! ```
62//! use spinoso_random::Random;
63//!
64//! let seed = [627457_u32, 697550, 16438, 41926];
65//! let mut random = Random::with_array_seed(seed);
66//! let rand = random.next_int32();
67//! ```
68//!
69//! Generate random numbers in a range:
70//!
71//! ```
72//! # #[cfg(feature = "rand-method")]
73//! # fn example() -> Result<(), spinoso_random::Error> {
74//! use spinoso_random::{rand, Max, Rand, Random};
75//!
76//! let mut random = Random::new()?;
77//! let max = Max::Integer(10);
78//! let mut rand = rand(&mut random, max)?;
79//! assert!(matches!(rand, Rand::Integer(x) if x < 10));
80//! # Ok(())
81//! # }
82//! # #[cfg(feature = "rand-method")]
83//! # example().unwrap();
84//! ```
85//!
86//! # `no_std`
87//!
88//! This crate is `no_std` compatible when built without the `std` feature. This
89//! crate does not depend on [`alloc`].
90//!
91//! # Crate features
92//!
93//! All features are enabled by default.
94//!
95//! - **rand-method** - Enables range sampling methods for the [`rand()`]
96//! function. Activating this feature also activates the **rand_core**
97//! feature. Dropping this feature removes the [`rand`] dependency.
98//! - **rand_core** - Enables implementations of [`RngCore`] on the [`Random`]
99//! type. Dropping this feature removes the [`rand_core`] dependency.
100//!
101#![cfg_attr(feature = "rand_core", doc = "[`RngCore`]: rand_core::RngCore")]
102#![cfg_attr(
103 not(feature = "rand_core"),
104 doc = "[`RngCore`]: https://docs.rs/rand_core/latest/rand_core/trait.RngCore.html"
105)]
106#![cfg_attr(feature = "rand_core", doc = "[`rand_core`]: ::rand_core")]
107#![cfg_attr(
108 not(feature = "rand_core"),
109 doc = "[`rand_core`]: https://docs.rs/rand_core/latest/rand_core/"
110)]
111#![cfg_attr(feature = "rand-method", doc = "[`rand`]: ::rand")]
112#![cfg_attr(not(feature = "rand-method"), doc = "[`rand`]: https://docs.rs/rand/latest/rand/")]
113#![cfg_attr(
114 not(feature = "rand-method"),
115 doc = "[`rand()`]: https://artichoke.github.io/artichoke/spinoso_random/fn.rand.html"
116)]
117//! [ruby-random]: https://ruby-doc.org/core-3.1.2/Random.html
118//! [`alloc`]: https://doc.rust-lang.org/alloc/
119
120#![no_std]
121
122// Ensure code blocks in `README.md` compile
123#[cfg(all(feature = "rand-method", doctest))]
124#[doc = include_str!("../README.md")]
125mod readme {}
126
127extern crate alloc;
128
129#[cfg(any(test, doctest))]
130extern crate std;
131
132use core::error;
133use core::fmt;
134
135#[cfg(feature = "rand-method")]
136mod rand;
137mod random;
138mod urandom;
139
140pub use random::{Random, new_seed, seed_to_key};
141pub use urandom::urandom;
142
143#[cfg(feature = "rand-method")]
144pub use self::rand::{Max, Rand, rand};
145
146/// Sum type of all errors possibly returned from `Random` functions.
147///
148/// Random functions in `spinoso-random` return errors in the following
149/// conditions:
150///
151/// - The platform source of cryptographic randomness is unavailable.
152/// - The platform source of cryptographic randomness does not have sufficient
153/// entropy to return the requested bytes.
154/// - Constraints for bounding random numbers are invalid.
155#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
156pub enum Error {
157 #[cfg_attr(
158 feature = "rand-method",
159 doc = "Error that indicates [`rand()`] was passed an invalid constraint."
160 )]
161 ///
162 /// See [`ArgumentError`].
163 Argument(ArgumentError),
164 /// Error that indicates that [`Random::new`] failed to generate a random
165 /// seed.
166 ///
167 /// See [`InitializeError`].
168 Initialize(InitializeError),
169 /// Error that indicates that [`new_seed`] failed to generate a random seed.
170 ///
171 /// See [`NewSeedError`].
172 NewSeed(NewSeedError),
173 /// Error that indicates that [`urandom()`] failed to generate the requested
174 /// random bytes from the platform source of randomness.
175 ///
176 /// See [`UrandomError`].
177 Urandom(UrandomError),
178}
179
180impl From<ArgumentError> for Error {
181 #[inline]
182 fn from(err: ArgumentError) -> Self {
183 Self::Argument(err)
184 }
185}
186
187impl From<InitializeError> for Error {
188 #[inline]
189 fn from(err: InitializeError) -> Self {
190 Self::Initialize(err)
191 }
192}
193
194impl From<NewSeedError> for Error {
195 #[inline]
196 fn from(err: NewSeedError) -> Self {
197 Self::NewSeed(err)
198 }
199}
200
201impl From<UrandomError> for Error {
202 #[inline]
203 fn from(err: UrandomError) -> Self {
204 Self::Urandom(err)
205 }
206}
207
208impl fmt::Display for Error {
209 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
210 f.write_str("Random error")
211 }
212}
213
214impl error::Error for Error {
215 fn source(&self) -> Option<&(dyn error::Error + 'static)> {
216 match self {
217 Self::Argument(err) => Some(err),
218 Self::Initialize(err) => Some(err),
219 Self::NewSeed(err) => Some(err),
220 Self::Urandom(err) => Some(err),
221 }
222 }
223}
224
225/// Error that indicates a `Random` random number generator failed to
226/// initialize.
227///
228/// When initializing an [`Random`] with a random seed, gathering entropy from
229/// the host system can fail.
230///
231/// This error corresponds to the [Ruby `RuntimeError` Exception class].
232///
233/// # Examples
234///
235/// ```
236/// use spinoso_random::InitializeError;
237///
238/// let err = InitializeError::new();
239/// assert_eq!(err.message(), "failed to get urandom");
240/// ```
241///
242/// [Ruby `RuntimeError` Exception class]: https://ruby-doc.org/core-3.1.2/RuntimeError.html
243#[derive(Default, Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
244pub struct InitializeError {
245 _private: (),
246}
247
248impl InitializeError {
249 /// Construct a new, default initialize error.
250 ///
251 /// # Examples
252 ///
253 /// ```
254 /// use spinoso_random::InitializeError;
255 ///
256 /// const ERR: InitializeError = InitializeError::new();
257 /// assert_eq!(ERR.message(), "failed to get urandom");
258 /// ```
259 #[inline]
260 #[must_use]
261 pub const fn new() -> Self {
262 Self { _private: () }
263 }
264
265 /// Retrieve the exception message associated with this initialization
266 /// error.
267 ///
268 /// # Examples
269 ///
270 /// ```
271 /// use spinoso_random::InitializeError;
272 ///
273 /// let err = InitializeError::new();
274 /// assert_eq!(err.message(), "failed to get urandom");
275 /// ```
276 #[inline]
277 #[must_use]
278 pub const fn message(self) -> &'static str {
279 "failed to get urandom"
280 }
281}
282
283impl fmt::Display for InitializeError {
284 #[inline]
285 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
286 f.write_str(self.message())
287 }
288}
289
290impl error::Error for InitializeError {}
291
292/// Error that indicates the system source of cryptographically secure
293/// randomness failed to read the requested bytes.
294///
295/// This can occur if the source is unknown or lacks sufficient entropy.
296///
297/// This error is returned by [`urandom()`]. See its documentation for more
298/// details.
299///
300/// This error corresponds to the [Ruby `RuntimeError` Exception class].
301///
302/// # Examples
303///
304/// ```
305/// use spinoso_random::UrandomError;
306///
307/// let err = UrandomError::new();
308/// assert_eq!(err.message(), "failed to get urandom");
309/// ```
310///
311/// [Ruby `RuntimeError` Exception class]: https://ruby-doc.org/core-3.1.2/RuntimeError.html
312#[derive(Default, Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
313pub struct UrandomError {
314 _private: (),
315}
316
317impl UrandomError {
318 /// Construct a new, default urandom error.
319 ///
320 /// # Examples
321 ///
322 /// ```
323 /// use spinoso_random::UrandomError;
324 ///
325 /// const ERR: UrandomError = UrandomError::new();
326 /// assert_eq!(ERR.message(), "failed to get urandom");
327 /// ```
328 #[inline]
329 #[must_use]
330 pub const fn new() -> Self {
331 Self { _private: () }
332 }
333
334 /// Retrieve the exception message associated with this urandom error.
335 ///
336 /// # Examples
337 ///
338 /// ```
339 /// use spinoso_random::UrandomError;
340 ///
341 /// let err = UrandomError::new();
342 /// assert_eq!(err.message(), "failed to get urandom");
343 /// ```
344 #[inline]
345 #[must_use]
346 pub const fn message(self) -> &'static str {
347 "failed to get urandom"
348 }
349}
350
351impl fmt::Display for UrandomError {
352 #[inline]
353 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
354 f.write_str(self.message())
355 }
356}
357
358impl error::Error for UrandomError {}
359
360impl From<getrandom::Error> for UrandomError {
361 #[inline]
362 fn from(_err: getrandom::Error) -> Self {
363 Self::new()
364 }
365}
366
367/// Error that indicates the system source of cryptographically secure
368/// randomness failed to read sufficient bytes to create a new seed.
369///
370/// This can occur if the source is unknown or lacks sufficient entropy.
371///
372/// This error is returned by [`new_seed`]. See its documentation for more
373/// details.
374///
375/// This error corresponds to the [Ruby `RuntimeError` Exception class].
376///
377/// # Examples
378///
379/// ```
380/// use spinoso_random::NewSeedError;
381///
382/// let err = NewSeedError::new();
383/// assert_eq!(err.message(), "failed to get urandom");
384/// ```
385///
386/// [Ruby `RuntimeError` Exception class]: https://ruby-doc.org/core-3.1.2/RuntimeError.html
387#[derive(Default, Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
388pub struct NewSeedError {
389 _private: (),
390}
391
392impl NewSeedError {
393 /// Construct a new, default new seed error.
394 ///
395 /// # Examples
396 ///
397 /// ```
398 /// use spinoso_random::NewSeedError;
399 ///
400 /// const ERR: NewSeedError = NewSeedError::new();
401 /// assert_eq!(ERR.message(), "failed to get urandom");
402 /// ```
403 #[inline]
404 #[must_use]
405 pub const fn new() -> Self {
406 Self { _private: () }
407 }
408
409 /// Retrieve the exception message associated with this new seed error.
410 ///
411 /// # Examples
412 ///
413 /// ```
414 /// use spinoso_random::NewSeedError;
415 ///
416 /// let err = NewSeedError::new();
417 /// assert_eq!(err.message(), "failed to get urandom");
418 /// ```
419 #[inline]
420 #[must_use]
421 pub const fn message(self) -> &'static str {
422 "failed to get urandom"
423 }
424}
425
426impl fmt::Display for NewSeedError {
427 #[inline]
428 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
429 f.write_str(self.message())
430 }
431}
432
433impl error::Error for NewSeedError {}
434
435impl From<getrandom::Error> for NewSeedError {
436 #[inline]
437 fn from(_err: getrandom::Error) -> Self {
438 Self::new()
439 }
440}
441
442/// Error that indicates a random number could not be generated with the given
443/// bounds.
444///
445#[cfg_attr(
446 feature = "rand-method",
447 doc = "This error is returned by [`rand()`]. See its documentation for more details."
448)]
449///
450/// This error corresponds to the [Ruby `ArgumentError` Exception class].
451///
452/// # Examples
453///
454/// ```
455/// use spinoso_random::ArgumentError;
456///
457/// let err = ArgumentError::new();
458/// assert_eq!(err.message(), "ArgumentError");
459/// ```
460///
461/// [Ruby `ArgumentError` Exception class]: https://ruby-doc.org/core-3.1.2/ArgumentError.html
462#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
463pub struct ArgumentError(ArgumentErrorInner);
464
465#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
466enum ArgumentErrorInner {
467 Default,
468 DomainError,
469 #[cfg(feature = "rand-method")]
470 #[cfg_attr(docsrs, doc(cfg(feature = "rand-method")))]
471 Rand(Max),
472}
473
474impl Default for ArgumentError {
475 fn default() -> Self {
476 Self::new()
477 }
478}
479
480impl ArgumentError {
481 /// Construct a new, default argument error.
482 ///
483 /// # Examples
484 ///
485 /// ```
486 /// use spinoso_random::ArgumentError;
487 ///
488 /// const ERR: ArgumentError = ArgumentError::new();
489 /// assert_eq!(ERR.message(), "ArgumentError");
490 /// ```
491 #[inline]
492 #[must_use]
493 pub const fn new() -> Self {
494 Self(ArgumentErrorInner::Default)
495 }
496
497 /// Construct a new domain error.
498 ///
499 /// # Examples
500 ///
501 /// ```
502 /// use spinoso_random::ArgumentError;
503 ///
504 /// const ERR: ArgumentError = ArgumentError::domain_error();
505 /// assert_eq!(ERR.message(), "Numerical argument out of domain");
506 /// ```
507 #[inline]
508 #[must_use]
509 pub const fn domain_error() -> Self {
510 Self(ArgumentErrorInner::DomainError)
511 }
512
513 /// Construct a new argument error from an invalid [`Max`] constraint.
514 ///
515 /// # Examples
516 ///
517 /// ```
518 /// use spinoso_random::{ArgumentError, Max};
519 ///
520 /// const ERR: ArgumentError = ArgumentError::with_rand_max(Max::Integer(-1));
521 /// assert_eq!(ERR.message(), "invalid argument");
522 /// ```
523 #[inline]
524 #[must_use]
525 #[cfg(feature = "rand-method")]
526 #[cfg_attr(docsrs, doc(cfg(feature = "rand-method")))]
527 pub const fn with_rand_max(max: Max) -> Self {
528 Self(ArgumentErrorInner::Rand(max))
529 }
530
531 /// Retrieve the exception message associated with this new seed error.
532 ///
533 #[cfg_attr(feature = "rand-method", doc = "# Implementation notes")]
534 #[cfg_attr(
535 feature = "rand-method",
536 doc = "Argument errors constructed with [`ArgumentError::with_rand_max`] return"
537 )]
538 #[cfg_attr(
539 feature = "rand-method",
540 doc = "an incomplete error message. Prefer to use the [`Display`] impl to"
541 )]
542 #[cfg_attr(feature = "rand-method", doc = "retrieve error messages from [`ArgumentError`].")]
543 ///
544 /// # Examples
545 ///
546 /// ```
547 /// use spinoso_random::ArgumentError;
548 ///
549 /// let err = ArgumentError::new();
550 /// assert_eq!(err.message(), "ArgumentError");
551 /// let err = ArgumentError::domain_error();
552 /// assert_eq!(err.message(), "Numerical argument out of domain");
553 /// ```
554 ///
555 /// [`Display`]: fmt::Display
556 #[inline]
557 #[must_use]
558 pub const fn message(self) -> &'static str {
559 match self.0 {
560 ArgumentErrorInner::Default => "ArgumentError",
561 ArgumentErrorInner::DomainError => "Numerical argument out of domain",
562 #[cfg(feature = "rand-method")]
563 ArgumentErrorInner::Rand(_) => "invalid argument",
564 }
565 }
566
567 /// Return whether this argument error is a domain error.
568 ///
569 /// Domain errors are typically reported as `Errno::EDOM` in MRI.
570 ///
571 /// # Examples
572 ///
573 /// ```
574 /// use spinoso_random::ArgumentError;
575 ///
576 /// let err = ArgumentError::domain_error();
577 /// assert!(err.is_domain_error());
578 /// let err = ArgumentError::new();
579 /// assert!(!err.is_domain_error());
580 /// ```
581 #[inline]
582 #[must_use]
583 pub const fn is_domain_error(self) -> bool {
584 matches!(self.0, ArgumentErrorInner::DomainError)
585 }
586}
587
588impl fmt::Display for ArgumentError {
589 #[inline]
590 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
591 match self.0 {
592 ArgumentErrorInner::Default | ArgumentErrorInner::DomainError => f.write_str(self.message()),
593 #[cfg(feature = "rand-method")]
594 ArgumentErrorInner::Rand(max) => write!(f, "invalid argument - {max}"),
595 }
596 }
597}
598
599impl error::Error for ArgumentError {}