onlinecode/rng.rs
1//! # A random-number generator for Online Codes
2//!
3//! This implements required traits from the rand_core crate to get a
4//! full-featured set of calling methods.
5//!
6//! Design is simply to feed the current state of the RNG (a 160-bit
7//! number) into SHA-1.
8
9
10// New approach
11//
12// I was going to be compatible with Rng, but distributions are a step
13// too far. I want to focus on compatibility between Rust, C and Perl,
14// so I will rewrite everything to use RngCore::next_u32 as the only
15// rng call that I use. Everything else will be built on this...
16//
17// For a rand between 0 (included) and 1 (not included):
18//
19// return ((rng.next_u32() * u32::MAX) >> 32) as f64
20//
21// When converting a table of f64 floating points that are in the
22// range 0 <= float < 1 into [0 .. u32::MAX - 1]:
23//
24// return ((float as u64 * u32::MAX) >> 32) as u32
25//
26// Treat 1.0 in that case as u32::MAX (table sentinel)
27//
28// max_degree uses such a table, so it will call `rng.next_u32()` and
29// use exactly the same logic as before, only working with unsigned
30// 32-bit integers.
31//
32// floyd() needs integers in the range `0..j` (not including j). The
33// operation is:
34//
35// let pick : u32 = ((rng.next_u32() as u64 * j) >> 32).into()
36//
37// I think that I'll move every bit of code that uses random numbers
38// into the same module.
39//
40// For conformance testing, I'll generate lists of random numbers,
41// calls to max_degree (which uses the probability distribution table)
42// and calls to floyd (which generates ints in a range) and calculate
43// the SHA-1 sum of the output (in a canonical format). Then I can
44// simply check whether the hashes differ or not. That should be
45// enough to be confident that the implementations are interoperable.
46//
47// Given a choice between special handling of 0xffffffff (by
48// rerolling) and breaking backwards compatibility by using a better
49// scheme, I'll go with the better scheme.
50
51use crate::*;
52
53use rand::{Rng, SeedableRng};
54use rand_core::{RngCore, Error, impls};
55
56use sha1::{Sha1, Digest};
57
58// #[derive(Debug,Copy,Clone)]
59pub struct SHA1Rng {
60
61// state : <SHA1Rng as rand::SeedableRng>::Seed,
62 state : [u8; 20],
63
64 // with 160 bits of state, we could spin up a new digest every
65 // time we need a new random number, or we could consume 32 bits
66 // of it at a time, only producing a new hash when we have run out
67 // of bits. We can generate 5 32-bit values from 160 bits.
68
69 state_bytes : usize, // for SHA-1, 160 bits = 20 bytes
70 avail_bytes : usize, // count down to zero
71
72 always_hash : bool,
73}
74
75impl SHA1Rng {
76 // use fluent/builder-style setup of optional always_hash flag
77 // since we can't pass any extra arguments to
78 // SeedableRng::from_seed(). (Could also provide a generic
79 // constructor here)
80 pub fn always_hash(mut self) -> Self { self.always_hash = true; self }
81 pub fn never_hash(mut self) -> Self { self.always_hash = false; self }
82
83 // move to next internal state: state <- sha1(state)
84 pub fn next_hash(&mut self) {
85 let hash = sha1::Sha1::digest(&self.state);
86 self.state.copy_from_slice(&hash);
87 self.avail_bytes = self.state_bytes;
88 }
89
90 // convenience constructors
91 pub fn from_string(s : &String) -> Self {
92 //
93 let hash = sha1::Sha1::digest(s.as_bytes());
94 let mut output = [0u8; 20];
95 output.copy_from_slice(&hash);
96 Self::from_seed(output)
97 }
98 pub fn from_str(s : &str) -> Self {
99 //
100 let hash = sha1::Sha1::digest(s.as_bytes());
101 let mut output = [0u8; 20];
102 output.copy_from_slice(&hash);
103 Self::from_seed(output)
104 }
105
106 // TODO: implement floating-point rand that works like C/Perl
107 // probably involves implementing custom Distribution to replace
108 // Standard
109}
110
111impl SeedableRng for SHA1Rng {
112 type Seed = [u8; 20];
113
114 fn from_seed(seed: Self::Seed) -> Self {
115 SHA1Rng {
116 state : seed,
117 state_bytes : 20,
118 avail_bytes : 20,
119 always_hash : true
120 }
121 }
122}
123
124
125impl RngCore for SHA1Rng {
126 // prefer 32-bit numbers to avoid precision errors when
127 // calculating. They will be upgraded to u64/f64
128
129 // Notice that when avoiding hash, we return parts of the seed,
130 // and then hash, whereas always_hash hashes it first. I'll have
131 // to check this for compatibility with my Perl/C implementations.
132 fn next_u32(&mut self) -> u32 {
133 let mut val : u32;
134 loop {
135 if self.avail_bytes < 4 || self.always_hash {
136 self.next_hash();
137 }
138 // convert a (little endian) slice of state into a u32
139 let p = self.state_bytes - self.avail_bytes;
140 self.avail_bytes -= 4;
141 let array = [self.state[p], self.state[p+1],
142 self.state[p+2], self.state[p+3]];
143 val = u32::from_le_bytes(array);
144 // eprintln!("RNG returning val {}", val);
145 // half way towards making compatible with C/Perl versions
146 if val != 0xffffffff { return val }
147 }
148 }
149
150 fn next_u64(&mut self) -> u64 {
151 impls::next_u64_via_u32(self)
152 // ah... the following ends up returning 0 when calling
153 // gen_range(), no doubt due to it looking at the high bytes
154 // or something stupid like that.
155 //
156 // self.next_u32() as u64
157 }
158
159 fn fill_bytes(&mut self, dest: &mut [u8]) {
160 impls::fill_bytes_via_next(self, dest)
161 }
162
163 fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Error> {
164 Ok(self.fill_bytes(dest))
165 }
166}
167
168
169// Implement a Distribution
170use rand::distributions::Distribution;
171
172/// Usage
173///
174/// ```rust
175///
176/// use rand::distributions::Distribution;
177/// use rand::{Rng, SeedableRng};
178///
179/// use onlinecode::rng::{SHA1Rng, Compatible};
180///
181/// let zeros = [0u8; 20];
182/// let mut rng = SHA1Rng::from_seed(zeros).always_hash();
183///
184/// let mut pick = rng.sample(Compatible {});
185///
186/// println!("Got value {0:.20} from rng", pick);
187/// assert!(pick >= 0.0);
188/// assert!(pick < 1.0);
189///
190/// // Alternative way of calling:
191/// let distrib = Compatible {};
192/// pick = distrib.sample(&mut rng);
193///
194/// ```
195pub struct Compatible { }
196
197impl Distribution<f64> for Compatible {
198
199 fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> f64 {
200 // The normal Standard lops off some low bits. Here, I use a
201 // full 32-bit rand, convert it to an f64
202 let mut val : u32 = rng.gen();
203 if val == u32::MAX {
204 // this is the least bad way of dealing with the
205 // possibility that the RNG sends us 0xffffffff. It
206 // shouldn't, so a panic might be more in order, but I
207 // don't want (literally) random panics in the code.
208 eprintln!("Random number error; \"fixing\" it");
209 val = rng.gen();
210 // TODO: find a seed that should generate 0xffffffff as
211 // its random output. Use that as a test case.
212 }
213 let double : f64 = val.into();
214 double / (u32::MAX as f64)
215 }
216}
217
218#[cfg(test)]
219
220mod tests {
221
222 use rand::SeedableRng;
223 use rand::Rng;
224 use crate::rng::SHA1Rng;
225
226 #[test]
227 fn all_zeros_array() {
228 let zeros = [0u8; 20];
229 let rng = SHA1Rng::from_seed(zeros);
230 assert_eq!(rng.always_hash, true);
231 }
232
233 #[test]
234 fn build_always_hash() {
235 let zeros = [0u8; 20];
236 let rng = SHA1Rng::from_seed(zeros).always_hash();
237 assert_eq!(rng.always_hash, true);
238 }
239
240 #[test]
241 fn build_never_hash() {
242 let zeros = [0u8; 20];
243 let rng = SHA1Rng::from_seed(zeros).never_hash();
244 assert_eq!(rng.always_hash, false);
245 }
246
247 #[test]
248 fn get_some_zeros() {
249 let zeros = [0u8; 20];
250 let mut rng = SHA1Rng::from_seed(zeros).never_hash();
251
252 // should we be able to get 5 zeros before re-hashing?
253 let mut val : u32 = rng.gen();
254 assert_eq!(val, 0, "rand 0 == 0");
255 val = rng.gen();
256 assert_eq!(val, 0, "rand 1 == 0");
257 val = rng.gen();
258 assert_eq!(val, 0, "rand 2 == 0");
259 val = rng.gen();
260 assert_eq!(val, 0, "rand 3 == 0");
261 val = rng.gen();
262 assert_eq!(val, 0, "rand 4 == 0");
263 // don't know for certain that next value != 0, but good chance
264 val = rng.gen();
265 assert_ne!(val, 0, "rand 5 != 0");
266
267 }
268
269 #[test]
270 fn get_single_zero() {
271 let zeros = [0u8; 20];
272 let mut rng = SHA1Rng::from_seed(zeros).always_hash();
273
274 // should we be able to get 1 zero before re-hashing? No!
275 // Don't know for certain that value != 0, but good chance
276 let mut val : u32 = rng.gen();
277 assert_ne!(val, 0, "always hash first value != 0");
278 }
279
280
281
282
283}