gossan_engine/
schedule.rs1pub struct BlackrockPermutation {
18 range: u64,
19 half_bits: u32,
20 half_mask: u64,
21 seed: u64,
22 rounds: u32,
23}
24
25impl BlackrockPermutation {
26 #[must_use]
32 pub fn new(range: u64, seed: u64) -> Self {
33 assert!(range > 0, "range must be > 0");
34
35 let total_bits = 64 - (range - 1).leading_zeros();
38 let half_bits = (total_bits + 1) / 2;
39 let half_mask = (1u64 << half_bits) - 1;
40
41 Self {
42 range,
43 half_bits,
44 half_mask,
45 seed,
46 rounds: 6, }
48 }
49
50 #[must_use]
56 pub fn shuffle(&self, mut index: u64) -> u64 {
57 loop {
58 let permuted = self.feistel(index);
59 if permuted < self.range {
60 return permuted;
61 }
62 index = permuted;
64 }
65 }
66
67 #[must_use]
69 pub fn unshuffle(&self, mut permuted: u64) -> u64 {
70 loop {
71 let index = self.feistel_inverse(permuted);
72 if index < self.range {
73 return index;
74 }
75 permuted = index;
76 }
77 }
78
79 fn feistel(&self, input: u64) -> u64 {
80 let mut left = input >> self.half_bits;
81 let mut right = input & self.half_mask;
82
83 for round in 0..self.rounds {
84 let new_right = left ^ self.round_function(right, round);
85 left = right;
86 right = new_right & self.half_mask;
87 }
88
89 (left << self.half_bits) | right
90 }
91
92 fn feistel_inverse(&self, input: u64) -> u64 {
93 let mut left = input >> self.half_bits;
94 let mut right = input & self.half_mask;
95
96 for round in (0..self.rounds).rev() {
97 let new_left = right ^ self.round_function(left, round);
98 right = left;
99 left = new_left & self.half_mask;
100 }
101
102 (left << self.half_bits) | right
103 }
104
105 #[inline]
106 fn round_function(&self, value: u64, round: u32) -> u64 {
107 let mut h = value.wrapping_mul(0x9E37_79B9_7F4A_7C15);
109 h = h.wrapping_add(self.seed);
110 h = h.wrapping_add(round as u64);
111 h ^= h >> 17;
112 h = h.wrapping_mul(0xBF58_476D_1CE4_E5B9);
113 h ^= h >> 31;
114 h
115 }
116}
117
118pub struct ScanSchedule {
123 permutation: BlackrockPermutation,
124 num_ports: u64,
125 total: u64,
126 current: u64,
127}
128
129impl ScanSchedule {
130 #[must_use]
132 pub fn new(num_ips: u64, num_ports: u64, seed: u64) -> Self {
133 let total = num_ips.saturating_mul(num_ports);
134 let permutation = if total > 0 {
135 BlackrockPermutation::new(total, seed)
136 } else {
137 BlackrockPermutation::new(1, seed)
138 };
139
140 Self {
141 permutation,
142 num_ports,
143 total,
144 current: 0,
145 }
146 }
147
148 #[must_use]
150 pub fn total(&self) -> u64 {
151 self.total
152 }
153}
154
155impl Iterator for ScanSchedule {
156 type Item = (u64, u64); fn next(&mut self) -> Option<Self::Item> {
159 if self.current >= self.total {
160 return None;
161 }
162
163 let permuted = self.permutation.shuffle(self.current);
164 self.current += 1;
165
166 let ip_index = permuted / self.num_ports;
167 let port_index = permuted % self.num_ports;
168 Some((ip_index, port_index))
169 }
170
171 fn size_hint(&self) -> (usize, Option<usize>) {
172 let remaining = (self.total - self.current) as usize;
173 (remaining, Some(remaining))
174 }
175}
176
177impl ExactSizeIterator for ScanSchedule {}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use std::collections::HashSet;
183
184 #[test]
185 fn permutation_is_bijective_small() {
186 let range = 100u64;
187 let perm = BlackrockPermutation::new(range, 42);
188 let mut seen = HashSet::new();
189
190 for i in 0..range {
191 let out = perm.shuffle(i);
192 assert!(out < range, "output {out} >= range {range} for input {i}");
193 assert!(seen.insert(out), "duplicate output {out} for input {i}");
194 }
195 assert_eq!(seen.len(), range as usize);
196 }
197
198 #[test]
199 fn permutation_is_bijective_large() {
200 let range = 10_000u64;
201 let perm = BlackrockPermutation::new(range, 0xDEAD_BEEF);
202 let mut seen = HashSet::new();
203
204 for i in 0..range {
205 let out = perm.shuffle(i);
206 assert!(out < range);
207 seen.insert(out);
208 }
209 assert_eq!(seen.len(), range as usize);
210 }
211
212 #[test]
213 fn permutation_is_deterministic() {
214 let perm = BlackrockPermutation::new(1000, 42);
215 let a = perm.shuffle(500);
216 let b = perm.shuffle(500);
217 assert_eq!(a, b);
218 }
219
220 #[test]
221 fn permutation_differs_by_seed() {
222 let a = BlackrockPermutation::new(1000, 1);
223 let b = BlackrockPermutation::new(1000, 2);
224 let mismatches = (0..1000).filter(|&i| a.shuffle(i) != b.shuffle(i)).count();
226 assert!(mismatches > 900, "seeds should produce different orderings");
227 }
228
229 #[test]
230 fn permutation_roundtrip() {
231 let perm = BlackrockPermutation::new(500, 99);
232 for i in 0..500 {
233 let shuffled = perm.shuffle(i);
234 let unshuffled = perm.unshuffle(shuffled);
235 assert_eq!(unshuffled, i, "roundtrip failed for {i}");
236 }
237 }
238
239 #[test]
240 fn schedule_covers_all_targets() {
241 let num_ips = 10u64;
242 let num_ports = 5u64;
243 let schedule = ScanSchedule::new(num_ips, num_ports, 42);
244 let pairs: Vec<_> = schedule.collect();
245
246 assert_eq!(pairs.len(), 50);
247
248 let mut seen = HashSet::new();
249 for (ip, port) in &pairs {
250 assert!(*ip < num_ips, "ip {ip} >= {num_ips}");
251 assert!(*port < num_ports, "port {port} >= {num_ports}");
252 assert!(seen.insert((*ip, *port)), "duplicate ({ip}, {port})");
253 }
254 assert_eq!(seen.len(), 50);
255 }
256
257 #[test]
258 fn schedule_exact_size() {
259 let schedule = ScanSchedule::new(100, 20, 42);
260 assert_eq!(schedule.len(), 2000);
261 assert_eq!(schedule.total(), 2000);
262 }
263
264 #[test]
265 fn schedule_is_not_sequential() {
266 let schedule = ScanSchedule::new(100, 10, 42);
267 let first_ten: Vec<_> = schedule.take(10).collect();
268
269 let sequential = first_ten.windows(2).all(|w| w[0].0 <= w[1].0);
271 assert!(
272 !sequential,
273 "schedule should not be sequential: {first_ten:?}"
274 );
275 }
276}
277
278#[cfg(test)]
279mod proptests {
280 use super::*;
281 use proptest::prelude::*;
282
283 proptest! {
284 #[test]
285 fn permutation_stays_in_range(
286 range in 1u64..10_000,
287 seed in any::<u64>(),
288 index in 0u64..10_000,
289 ) {
290 let index = index % range;
291 let perm = BlackrockPermutation::new(range, seed);
292 let out = perm.shuffle(index);
293 prop_assert!(out < range, "output {out} >= range {range}");
294 }
295
296 #[test]
297 fn permutation_roundtrips(
298 range in 1u64..1_000,
299 seed in any::<u64>(),
300 index in 0u64..1_000,
301 ) {
302 let index = index % range;
303 let perm = BlackrockPermutation::new(range, seed);
304 let shuffled = perm.shuffle(index);
305 let back = perm.unshuffle(shuffled);
306 prop_assert_eq!(back, index);
307 }
308 }
309}