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 {}