Skip to main content

limnifs_core/
ec_params.rs

1//! EC params section (spec §5.6, `bit-level/43-ec-params.md`).
2//!
3//! Configures Reed-Solomon erasure coding for the image's slabs. The
4//! section is OPTIONAL — present iff the EC feature flag (`0x0001`)
5//! is declared in the feature flags section (§5.2).
6
7use crate::cursor::ManifestCursor;
8use crate::error::CoreError;
9use limnifs_format::SlabId;
10
11/// Current layout version of this section.
12pub const EC_PARAMS_SECTION_VERSION: u8 = 1;
13
14/// Default GF(2^8) generator polynomial (the canonical AES polynomial).
15pub const DEFAULT_EC_POLYNOMIAL: u16 = 0x011D;
16
17/// GF(2^8) shard count upper bound.
18pub const MAX_SHARDS: u8 = 255;
19
20/// Width of the fixed prefix of this section (`version` + `k` + `m` + `polynomial` + `override_count`).
21const PREFIX_LEN: usize = 1 + 1 + 1 + 2 + 4;
22
23/// Width of a single override entry (40-byte `SlabId` + `k` + `m`).
24const OVERRIDE_ENTRY_LEN: usize = 40 + 1 + 1;
25
26/// One per-slab (k, m) override.
27#[derive(Clone, Copy, Debug, Eq, PartialEq)]
28pub struct EcOverride {
29    pub slab_id: SlabId,
30    pub k: u8,
31    pub m: u8,
32}
33
34/// Parsed EC params section.
35#[derive(Clone, Debug, Eq, PartialEq)]
36pub struct EcParams {
37    pub k: u8,
38    pub m: u8,
39    pub polynomial: u16,
40    pub overrides: Vec<EcOverride>,
41}
42
43impl EcParams {
44    #[must_use]
45    pub fn default_params(k: u8, m: u8) -> Self {
46        Self {
47            k,
48            m,
49            polynomial: DEFAULT_EC_POLYNOMIAL,
50            overrides: Vec::new(),
51        }
52    }
53}
54
55/// Parse the EC params section from the cursor's current position.
56///
57/// # Errors
58///
59/// - [`CoreError::UnsupportedFeature`] if `section_version` is not 1
60///   or `polynomial` is not the supported value.
61/// - [`CoreError::Corrupt`] if `k < 1`, `m < 1`, `k + m > 255`, if
62///   any override's `(k, m)` violates the same constraints, or if a
63///   `slab_id` appears in multiple overrides.
64/// - [`CoreError::TooShort`] if the cursor has fewer bytes than the
65///   declared structure requires.
66pub fn parse_ec_params(cursor: &mut ManifestCursor<'_>) -> Result<EcParams, CoreError> {
67    let section_version = cursor.read_u8()?;
68    if section_version != EC_PARAMS_SECTION_VERSION {
69        return Err(CoreError::UnsupportedFeature {
70            feature: format!(
71                "ec_params section version {section_version} (supported: {EC_PARAMS_SECTION_VERSION})"
72            ),
73        });
74    }
75    let k = cursor.read_u8()?;
76    let m = cursor.read_u8()?;
77    validate_shard_counts(k, m, "default")?;
78    let polynomial = cursor.read_u16_le()?;
79    if polynomial != DEFAULT_EC_POLYNOMIAL {
80        return Err(CoreError::UnsupportedFeature {
81            feature: format!(
82                "ec_params polynomial 0x{polynomial:04X} (supported: 0x{DEFAULT_EC_POLYNOMIAL:04X})"
83            ),
84        });
85    }
86    let raw_count = cursor.read_u32_le()?;
87    let override_count = usize::try_from(raw_count).map_err(|_| CoreError::Corrupt {
88        reason: format!("ec_params override_count {raw_count} exceeds usize"),
89    })?;
90    // DoS check: each override needs at least OVERRIDE_ENTRY_LEN bytes.
91    let min_size = override_count
92        .checked_mul(OVERRIDE_ENTRY_LEN)
93        .ok_or_else(|| CoreError::Corrupt {
94            reason: format!("ec_params override_count {override_count} overflows usize"),
95        })?;
96    if cursor.remaining_len() < min_size {
97        return Err(CoreError::TooShort {
98            have: cursor.remaining_len(),
99            need: min_size,
100        });
101    }
102    let mut overrides = Vec::with_capacity(override_count);
103    for index in 0..override_count {
104        let ordinal = cursor.read_u64_le()?;
105        let hash_bytes = cursor.read_n(32)?;
106        let mut hash = [0u8; 32];
107        hash.copy_from_slice(hash_bytes);
108        let slab_id = SlabId::new(ordinal, hash);
109        let ok = cursor.read_u8()?;
110        let om = cursor.read_u8()?;
111        validate_shard_counts(
112            ok,
113            om,
114            format!("override {index} for slab ordinal {ordinal}").as_str(),
115        )?;
116        if overrides
117            .iter()
118            .any(|existing: &EcOverride| existing.slab_id == slab_id)
119        {
120            return Err(CoreError::Corrupt {
121                reason: format!(
122                    "ec_params override {index}: duplicate slab_id (ordinal {ordinal})"
123                ),
124            });
125        }
126        overrides.push(EcOverride {
127            slab_id,
128            k: ok,
129            m: om,
130        });
131    }
132    let _ = PREFIX_LEN; // documented constant; nothing to emit
133    Ok(EcParams {
134        k,
135        m,
136        polynomial,
137        overrides,
138    })
139}
140
141fn validate_shard_counts(k: u8, m: u8, label: &str) -> Result<(), CoreError> {
142    if k == 0 {
143        return Err(CoreError::Corrupt {
144            reason: format!("ec_params {label}: k must be >= 1, got 0"),
145        });
146    }
147    if m == 0 {
148        return Err(CoreError::Corrupt {
149            reason: format!("ec_params {label}: m must be >= 1, got 0"),
150        });
151    }
152    let total = u16::from(k) + u16::from(m);
153    if total > u16::from(MAX_SHARDS) {
154        return Err(CoreError::Corrupt {
155            reason: format!(
156                "ec_params {label}: k + m = {total} exceeds GF(2^8) limit ({MAX_SHARDS})"
157            ),
158        });
159    }
160    Ok(())
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    fn make_ec_params_bytes(
168        version: u8,
169        k: u8,
170        m: u8,
171        polynomial: u16,
172        overrides: &[(u64, [u8; 32], u8, u8)],
173    ) -> Vec<u8> {
174        let mut bytes = Vec::new();
175        bytes.push(version);
176        bytes.push(k);
177        bytes.push(m);
178        bytes.extend_from_slice(&polynomial.to_le_bytes());
179        let count = u32::try_from(overrides.len()).expect("count fits u32");
180        bytes.extend_from_slice(&count.to_le_bytes());
181        for (ordinal, hash, ok, om) in overrides {
182            bytes.extend_from_slice(&ordinal.to_le_bytes());
183            bytes.extend_from_slice(hash);
184            bytes.push(*ok);
185            bytes.push(*om);
186        }
187        bytes
188    }
189
190    fn sample_hash(byte: u8) -> [u8; 32] {
191        let mut h = [0u8; 32];
192        h[0] = byte;
193        h
194    }
195
196    #[test]
197    fn parses_default_only() {
198        let bytes =
199            make_ec_params_bytes(EC_PARAMS_SECTION_VERSION, 4, 2, DEFAULT_EC_POLYNOMIAL, &[]);
200        let mut cursor = ManifestCursor::new(&bytes);
201        let parsed = parse_ec_params(&mut cursor).expect("default parses");
202        assert_eq!(parsed.k, 4);
203        assert_eq!(parsed.m, 2);
204        assert_eq!(parsed.polynomial, DEFAULT_EC_POLYNOMIAL);
205        assert!(parsed.overrides.is_empty());
206        assert_eq!(cursor.position(), bytes.len());
207    }
208
209    #[test]
210    fn parses_with_one_override() {
211        let bytes = make_ec_params_bytes(
212            EC_PARAMS_SECTION_VERSION,
213            4,
214            2,
215            DEFAULT_EC_POLYNOMIAL,
216            &[(7, sample_hash(0xAA), 8, 4)],
217        );
218        let mut cursor = ManifestCursor::new(&bytes);
219        let parsed = parse_ec_params(&mut cursor).expect("override parses");
220        assert_eq!(parsed.overrides.len(), 1);
221        assert_eq!(parsed.overrides[0].slab_id.ordinal, 7);
222        assert_eq!(parsed.overrides[0].k, 8);
223        assert_eq!(parsed.overrides[0].m, 4);
224    }
225
226    #[test]
227    fn parses_with_multiple_overrides() {
228        let bytes = make_ec_params_bytes(
229            EC_PARAMS_SECTION_VERSION,
230            4,
231            2,
232            DEFAULT_EC_POLYNOMIAL,
233            &[
234                (0, sample_hash(0x01), 6, 3),
235                (1, sample_hash(0x02), 8, 4),
236                (2, sample_hash(0x03), 16, 8),
237            ],
238        );
239        let mut cursor = ManifestCursor::new(&bytes);
240        let parsed = parse_ec_params(&mut cursor).expect("multi parses");
241        assert_eq!(parsed.overrides.len(), 3);
242    }
243
244    #[test]
245    fn rejects_unknown_section_version() {
246        let bytes = make_ec_params_bytes(7, 4, 2, DEFAULT_EC_POLYNOMIAL, &[]);
247        let mut cursor = ManifestCursor::new(&bytes);
248        match parse_ec_params(&mut cursor) {
249            Err(CoreError::UnsupportedFeature { feature }) => {
250                assert!(feature.contains("version 7"), "got: {feature}");
251            }
252            other => panic!("expected UnsupportedFeature, got {other:?}"),
253        }
254    }
255
256    #[test]
257    fn rejects_zero_k() {
258        let bytes =
259            make_ec_params_bytes(EC_PARAMS_SECTION_VERSION, 0, 2, DEFAULT_EC_POLYNOMIAL, &[]);
260        let mut cursor = ManifestCursor::new(&bytes);
261        match parse_ec_params(&mut cursor) {
262            Err(CoreError::Corrupt { reason }) => {
263                assert!(reason.contains("k must be >= 1"), "got: {reason}");
264            }
265            other => panic!("expected Corrupt, got {other:?}"),
266        }
267    }
268
269    #[test]
270    fn rejects_zero_m() {
271        let bytes =
272            make_ec_params_bytes(EC_PARAMS_SECTION_VERSION, 4, 0, DEFAULT_EC_POLYNOMIAL, &[]);
273        let mut cursor = ManifestCursor::new(&bytes);
274        match parse_ec_params(&mut cursor) {
275            Err(CoreError::Corrupt { reason }) => {
276                assert!(reason.contains("m must be >= 1"), "got: {reason}");
277            }
278            other => panic!("expected Corrupt, got {other:?}"),
279        }
280    }
281
282    #[test]
283    fn rejects_shard_count_above_gf_limit() {
284        // k = 200, m = 100 -> total 300 > 255.
285        let bytes = make_ec_params_bytes(
286            EC_PARAMS_SECTION_VERSION,
287            200,
288            100,
289            DEFAULT_EC_POLYNOMIAL,
290            &[],
291        );
292        let mut cursor = ManifestCursor::new(&bytes);
293        match parse_ec_params(&mut cursor) {
294            Err(CoreError::Corrupt { reason }) => {
295                assert!(reason.contains("GF(2^8)"), "got: {reason}");
296            }
297            other => panic!("expected Corrupt, got {other:?}"),
298        }
299    }
300
301    #[test]
302    fn rejects_unknown_polynomial() {
303        let bytes = make_ec_params_bytes(EC_PARAMS_SECTION_VERSION, 4, 2, 0x002B, &[]);
304        let mut cursor = ManifestCursor::new(&bytes);
305        match parse_ec_params(&mut cursor) {
306            Err(CoreError::UnsupportedFeature { feature }) => {
307                assert!(feature.contains("polynomial"), "got: {feature}");
308                assert!(feature.contains("0x002B"));
309            }
310            other => panic!("expected UnsupportedFeature, got {other:?}"),
311        }
312    }
313
314    #[test]
315    fn rejects_duplicate_override_slab_id() {
316        let bytes = make_ec_params_bytes(
317            EC_PARAMS_SECTION_VERSION,
318            4,
319            2,
320            DEFAULT_EC_POLYNOMIAL,
321            &[(5, sample_hash(0xAA), 6, 3), (5, sample_hash(0xAA), 8, 4)],
322        );
323        let mut cursor = ManifestCursor::new(&bytes);
324        match parse_ec_params(&mut cursor) {
325            Err(CoreError::Corrupt { reason }) => {
326                assert!(reason.contains("duplicate"), "got: {reason}");
327                assert!(reason.contains("ordinal 5"));
328            }
329            other => panic!("expected Corrupt, got {other:?}"),
330        }
331    }
332
333    #[test]
334    fn rejects_override_with_bad_shard_counts() {
335        let bytes = make_ec_params_bytes(
336            EC_PARAMS_SECTION_VERSION,
337            4,
338            2,
339            DEFAULT_EC_POLYNOMIAL,
340            &[(7, sample_hash(0xAA), 0, 4)], // override k = 0
341        );
342        let mut cursor = ManifestCursor::new(&bytes);
343        match parse_ec_params(&mut cursor) {
344            Err(CoreError::Corrupt { reason }) => {
345                assert!(reason.contains("override 0"), "got: {reason}");
346                assert!(reason.contains("k must be >= 1"));
347            }
348            other => panic!("expected Corrupt, got {other:?}"),
349        }
350    }
351
352    #[test]
353    fn rejects_truncated_prefix() {
354        // Section version + partial k.
355        let bytes = [EC_PARAMS_SECTION_VERSION];
356        let mut cursor = ManifestCursor::new(&bytes);
357        match parse_ec_params(&mut cursor) {
358            Err(CoreError::TooShort { .. }) => {}
359            other => panic!("expected TooShort, got {other:?}"),
360        }
361    }
362
363    #[test]
364    fn rejects_override_count_overrunning_buffer() {
365        let mut bytes = Vec::new();
366        bytes.push(EC_PARAMS_SECTION_VERSION);
367        bytes.push(4);
368        bytes.push(2);
369        bytes.extend_from_slice(&DEFAULT_EC_POLYNOMIAL.to_le_bytes());
370        bytes.extend_from_slice(&10u32.to_le_bytes()); // claim 10 overrides, none provided
371        let mut cursor = ManifestCursor::new(&bytes);
372        match parse_ec_params(&mut cursor) {
373            Err(CoreError::TooShort { have, need }) => {
374                assert_eq!(have, 0);
375                assert_eq!(need, 10 * OVERRIDE_ENTRY_LEN);
376            }
377            other => panic!("expected TooShort, got {other:?}"),
378        }
379    }
380
381    #[test]
382    fn default_params_helper_uses_default_polynomial() {
383        let params = EcParams::default_params(4, 2);
384        assert_eq!(params.polynomial, DEFAULT_EC_POLYNOMIAL);
385        assert!(params.overrides.is_empty());
386    }
387
388    #[test]
389    fn max_shards_constant_is_255() {
390        // GF(2^8) has 256 elements; one is the zero symbol, so 255 usable shards.
391        assert_eq!(MAX_SHARDS, 255);
392    }
393}