umsh_journal_store/
seed.rs1use crate::record::{crc32, generation_is_newer};
14
15pub const SLOT_SIZE: usize = 64;
16pub const COMMIT_OFFSET: usize = SLOT_SIZE - 4;
17const CRC_OFFSET: usize = COMMIT_OFFSET - 4;
18const MAGIC: [u8; 4] = *b"URNG";
19const VERSION: u8 = 1;
20const SEED_OFFSET: usize = 12;
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub struct SeedRecord {
25 pub generation: u32,
26 pub seed: [u8; 32],
27}
28
29impl SeedRecord {
30 pub fn encode(&self) -> [u8; SLOT_SIZE] {
31 let mut out = [0xff; SLOT_SIZE];
32 out[..4].copy_from_slice(&MAGIC);
33 out[4] = VERSION;
34 out[5..8].fill(0);
35 out[8..12].copy_from_slice(&self.generation.to_le_bytes());
36 out[SEED_OFFSET..SEED_OFFSET + 32].copy_from_slice(&self.seed);
37 let crc = crc32(&out[..CRC_OFFSET]);
38 out[CRC_OFFSET..COMMIT_OFFSET].copy_from_slice(&crc.to_le_bytes());
39 out
40 }
41
42 pub fn decode(bytes: &[u8; SLOT_SIZE]) -> Option<Self> {
43 if bytes[COMMIT_OFFSET..] != [0, 0, 0, 0]
44 || bytes[..4] != MAGIC
45 || bytes[4] != VERSION
46 || crc32(&bytes[..CRC_OFFSET])
47 != u32::from_le_bytes(bytes[CRC_OFFSET..COMMIT_OFFSET].try_into().ok()?)
48 {
49 return None;
50 }
51 let seed: [u8; 32] = bytes[SEED_OFFSET..SEED_OFFSET + 32].try_into().ok()?;
52 if seed == [0; 32] {
56 return None;
57 }
58 Some(Self {
59 generation: u32::from_le_bytes(bytes[8..12].try_into().ok()?),
60 seed,
61 })
62 }
63}
64
65pub fn consider_record(
67 current: Option<(u32, SeedRecord)>,
68 address: u32,
69 bytes: &[u8; SLOT_SIZE],
70) -> Option<(u32, SeedRecord)> {
71 let Some(candidate) = SeedRecord::decode(bytes) else {
72 return current;
73 };
74 if current
75 .as_ref()
76 .is_none_or(|(_, record)| generation_is_newer(candidate.generation, record.generation))
77 {
78 Some((address, candidate))
79 } else {
80 current
81 }
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 fn sample() -> SeedRecord {
89 let mut seed = [0u8; 32];
90 for (index, byte) in seed.iter_mut().enumerate() {
91 *byte = index as u8 + 1;
92 }
93 SeedRecord {
94 generation: 7,
95 seed,
96 }
97 }
98
99 fn committed(record: &SeedRecord) -> [u8; SLOT_SIZE] {
100 let mut bytes = record.encode();
101 bytes[COMMIT_OFFSET..].fill(0);
102 bytes
103 }
104
105 #[test]
106 fn a_committed_record_round_trips() {
107 let record = sample();
108 assert_eq!(SeedRecord::decode(&committed(&record)), Some(record));
109 }
110
111 #[test]
112 fn an_uncommitted_record_is_ignored() {
113 assert_eq!(SeedRecord::decode(&sample().encode()), None);
116 }
117
118 #[test]
119 fn a_corrupt_body_is_ignored() {
120 let mut bytes = committed(&sample());
121 bytes[SEED_OFFSET] ^= 0x40;
122 assert_eq!(SeedRecord::decode(&bytes), None);
123 }
124
125 #[test]
126 fn an_all_zero_seed_is_refused() {
127 let mut zeroed = sample();
128 zeroed.seed = [0; 32];
129 assert_eq!(SeedRecord::decode(&committed(&zeroed)), None);
130 }
131
132 #[test]
133 fn the_newest_generation_wins_across_slots() {
134 let old = committed(&sample());
135 let new = committed(&SeedRecord {
136 generation: 8,
137 ..sample()
138 });
139 let latest = consider_record(None, 0x1000, &old);
140 let latest = consider_record(latest, 0x1040, &new);
141 assert_eq!(
142 latest.map(|(address, r)| (address, r.generation)),
143 Some((0x1040, 8))
144 );
145 let latest = consider_record(None, 0x1040, &new);
147 let latest = consider_record(latest, 0x1000, &old);
148 assert_eq!(latest.map(|(_, r)| r.generation), Some(8));
149 }
150
151 #[test]
152 fn a_torn_newer_record_leaves_the_older_one_current() {
153 let old = committed(&sample());
154 let torn = SeedRecord {
156 generation: 8,
157 ..sample()
158 }
159 .encode();
160 let latest = consider_record(None, 0x1000, &old);
161 let latest = consider_record(latest, 0x1040, &torn);
162 assert_eq!(latest.map(|(_, r)| r.generation), Some(7));
163 }
164
165 #[test]
166 fn generation_wraparound_prefers_the_wrapped_record() {
167 let pre_wrap = committed(&SeedRecord {
168 generation: u32::MAX,
169 ..sample()
170 });
171 let wrapped = committed(&SeedRecord {
172 generation: 0,
173 ..sample()
174 });
175 let latest = consider_record(None, 0x1000, &pre_wrap);
176 let latest = consider_record(latest, 0x1040, &wrapped);
177 assert_eq!(latest.map(|(_, r)| r.generation), Some(0));
178 }
179}