shadowforge_lib/domain/reconstruction/
mod.rs1use crate::domain::errors::ReconstructionError;
6use crate::domain::types::Shard;
7
8pub const fn validate_shard_count(
13 available: usize,
14 needed: usize,
15) -> Result<(), ReconstructionError> {
16 if available < needed {
17 return Err(ReconstructionError::InsufficientCovers {
18 needed,
19 got: available,
20 });
21 }
22 Ok(())
23}
24
25#[must_use]
30pub fn arrange_shards(shards: Vec<Shard>, total_shards: u8) -> Vec<Option<Shard>> {
31 let mut slots: Vec<Option<Shard>> = (0..usize::from(total_shards)).map(|_| None).collect();
32 for shard in shards {
33 let idx = usize::from(shard.index);
34 if let Some(slot @ None) = slots.get_mut(idx) {
35 *slot = Some(shard);
36 }
37 }
38 slots
39}
40
41#[must_use]
43pub fn count_present(slots: &[Option<Shard>]) -> usize {
44 slots.iter().filter(|s| s.is_some()).count()
45}
46
47#[must_use]
49pub fn serialize_shard(shard: &Shard) -> Vec<u8> {
50 let data_len = shard.data.len();
51 let mut buf = Vec::with_capacity(1 + 1 + 32 + 4 + data_len);
52 buf.push(shard.index);
53 buf.push(shard.total);
54 buf.extend_from_slice(&shard.hmac_tag);
55 #[expect(
56 clippy::cast_possible_truncation,
57 reason = "shard data len bounded below u32::MAX"
58 )]
59 let len = data_len as u32;
60 buf.extend_from_slice(&len.to_le_bytes());
61 buf.extend_from_slice(&shard.data);
62 buf
63}
64
65#[must_use]
70pub fn deserialize_shard(data: &[u8]) -> Option<Shard> {
71 let index = *data.first()?;
73 let total = *data.get(1)?;
74 let mut hmac_tag = [0u8; 32];
75 hmac_tag.copy_from_slice(data.get(2..34)?);
76 let len_bytes: [u8; 4] = data.get(34..38)?.try_into().ok()?;
77 let data_len = u32::from_le_bytes(len_bytes) as usize;
78 let shard_data = data.get(38..38_usize.strict_add(data_len))?.to_vec();
79 Some(Shard {
80 index,
81 total,
82 data: shard_data,
83 hmac_tag,
84 })
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90
91 type TestResult = Result<(), Box<dyn std::error::Error>>;
92
93 fn make_shard(index: u8, total: u8, data: &[u8]) -> Shard {
94 Shard {
95 index,
96 total,
97 data: data.to_vec(),
98 hmac_tag: [0u8; 32],
99 }
100 }
101
102 #[test]
103 fn validate_shard_count_sufficient() {
104 assert!(validate_shard_count(5, 3).is_ok());
105 assert!(validate_shard_count(3, 3).is_ok());
106 }
107
108 #[test]
109 fn validate_shard_count_insufficient() {
110 let err = validate_shard_count(2, 3);
111 assert!(err.is_err());
112 }
113
114 #[test]
115 fn arrange_shards_correct_placement() -> TestResult {
116 let shards = vec![
117 make_shard(2, 4, b"c"),
118 make_shard(0, 4, b"a"),
119 make_shard(3, 4, b"d"),
120 ];
121 let slots = arrange_shards(shards, 4);
122 assert_eq!(slots.len(), 4);
123 assert!(slots.first().and_then(Option::as_ref).is_some());
124 assert!(slots.get(1).and_then(Option::as_ref).is_none());
125 assert!(slots.get(2).and_then(Option::as_ref).is_some());
126 assert!(slots.get(3).and_then(Option::as_ref).is_some());
127 assert_eq!(
128 slots
129 .first()
130 .and_then(Option::as_ref)
131 .ok_or("missing slot 0")?
132 .data,
133 b"a"
134 );
135 assert_eq!(
136 slots
137 .get(2)
138 .and_then(Option::as_ref)
139 .ok_or("missing slot 2")?
140 .data,
141 b"c"
142 );
143 Ok(())
144 }
145
146 #[test]
147 fn arrange_shards_duplicate_index_ignored() -> TestResult {
148 let shards = vec![make_shard(0, 2, b"first"), make_shard(0, 2, b"second")];
149 let slots = arrange_shards(shards, 2);
150 assert_eq!(
151 slots
152 .first()
153 .and_then(Option::as_ref)
154 .ok_or("missing slot 0")?
155 .data,
156 b"first"
157 );
158 Ok(())
159 }
160
161 #[test]
162 fn count_present_correct() {
163 let slots = vec![
164 Some(make_shard(0, 3, b"a")),
165 None,
166 Some(make_shard(2, 3, b"c")),
167 ];
168 assert_eq!(count_present(&slots), 2);
169 }
170}