Skip to main content

shadowforge_lib/domain/reconstruction/
mod.rs

1//! K-of-N shard reassembly with manifest verification.
2//!
3//! Pure domain logic — no I/O, no file system, no async runtime.
4
5use crate::domain::errors::ReconstructionError;
6use crate::domain::types::Shard;
7
8/// Validate that enough shards are present for reconstruction.
9///
10/// # Errors
11/// Returns [`ReconstructionError::InsufficientCovers`] if `available < needed`.
12pub 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/// Reorder extracted shard data into shard slots based on embedded index.
26///
27/// Returns a `Vec<Option<Shard>>` of length `total_shards`, placing each
28/// shard at its declared index position. Duplicate indices are ignored.
29#[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/// Count how many shards are present (non-None) in a slot array.
42#[must_use]
43pub fn count_present(slots: &[Option<Shard>]) -> usize {
44    slots.iter().filter(|s| s.is_some()).count()
45}
46
47/// Serialize a shard to binary: `[index:1][total:1][hmac:32][data_len:4][data:N]`.
48#[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/// Deserialize a shard from binary produced by [`serialize_shard`].
66///
67/// # Errors
68/// Returns `None` if the buffer is too short or corrupted.
69#[must_use]
70pub fn deserialize_shard(data: &[u8]) -> Option<Shard> {
71    // Minimum: index(1) + total(1) + hmac(32) + data_len(4) = 38
72    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}