Skip to main content

limnifs_core/
feature_flags.rs

1//! Feature flags section (spec §5.2, `bit-level/36-feature-flags.md`).
2//!
3//! One row of `(flag_id, required)` per optional feature the image
4//! relies on. Readers apply the unknown-flag policy (§18): an unknown
5//! REQUIRED flag fails the read; an unknown optional flag is silently
6//! ignored.
7
8use crate::cursor::ManifestCursor;
9use crate::error::CoreError;
10
11/// Current layout version of this section.
12pub const FEATURE_FLAGS_SECTION_VERSION: u8 = 1;
13
14/// Width of the fixed prefix (version byte + u32 LE entry count).
15/// Exposed so callers can size buffers and verify cursor advancement.
16pub const PREFIX_LEN: usize = 5;
17
18/// Width of a single entry (`u16 LE` flag id + `u8` required).
19pub const ENTRY_LEN: usize = 3;
20
21/// One feature-flag entry from the manifest.
22///
23/// `flag_id` references the feature-flag registry (spec §14).
24/// `required` reflects the wire byte: a required flag the reader does
25/// not know causes `UnsupportedFeature`; an optional flag is silently
26/// ignored (spec §18).
27#[derive(Copy, Clone, Debug, Eq, PartialEq)]
28pub struct FeatureFlag {
29    pub flag_id: u16,
30    pub required: bool,
31}
32
33/// Parsed feature flags section.
34///
35/// `entries` is in wire order (declaration order in the manifest).
36/// Duplicate flag ids are rejected at parse time per
37/// `bit-level/36-feature-flags.md` validation rule 6.
38#[derive(Clone, Debug, Eq, PartialEq, Default)]
39pub struct FeatureFlags {
40    pub entries: Vec<FeatureFlag>,
41}
42
43impl FeatureFlags {
44    #[must_use]
45    pub fn is_empty(&self) -> bool {
46        self.entries.is_empty()
47    }
48
49    #[must_use]
50    pub fn len(&self) -> usize {
51        self.entries.len()
52    }
53
54    /// Look up the entry for `flag_id`, if present.
55    #[must_use]
56    pub fn get(&self, flag_id: u16) -> Option<FeatureFlag> {
57        self.entries
58            .iter()
59            .copied()
60            .find(|entry| entry.flag_id == flag_id)
61    }
62
63    /// True iff `flag_id` is declared with `required = true`.
64    #[must_use]
65    pub fn is_required(&self, flag_id: u16) -> bool {
66        self.get(flag_id).is_some_and(|entry| entry.required)
67    }
68}
69
70/// Parse the feature flags section from the cursor's current position.
71///
72/// Advances the cursor by the section's total width
73/// (`PREFIX_LEN + ENTRY_LEN × entry_count`) on success.
74///
75/// # Errors
76///
77/// - [`CoreError::UnsupportedFeature`] if the section version is not
78///   [`FEATURE_FLAGS_SECTION_VERSION`].
79/// - [`CoreError::Corrupt`] if `entry_count` exceeds `usize`, if any
80///   `flag_id` is `0x0000`, if any `required` byte is not in
81///   `{0x00, 0x01}`, or if a `flag_id` is declared more than once.
82/// - [`CoreError::TooShort`] if the cursor has fewer bytes than the
83///   section declares.
84pub fn parse_feature_flags_section(
85    cursor: &mut ManifestCursor<'_>,
86) -> Result<FeatureFlags, CoreError> {
87    let section_version = cursor.read_u8()?;
88    if section_version != FEATURE_FLAGS_SECTION_VERSION {
89        return Err(CoreError::UnsupportedFeature {
90            feature: format!(
91                "feature_flags section version {section_version} (supported: {FEATURE_FLAGS_SECTION_VERSION})"
92            ),
93        });
94    }
95    let raw_count = cursor.read_u32_le()?;
96    let entry_count = usize::try_from(raw_count).map_err(|_| CoreError::Corrupt {
97        reason: format!("feature_flags entry count {raw_count} exceeds usize"),
98    })?;
99    // Verify the declared count fits the remaining bytes BEFORE we
100    // call Vec::with_capacity. Without this, a malicious header with
101    // entry_count = u32::MAX would ask the allocator for ~12 GB and
102    // abort the reader (DoS).
103    let payload_size = entry_count
104        .checked_mul(ENTRY_LEN)
105        .ok_or_else(|| CoreError::Corrupt {
106            reason: format!("feature_flags entry count {entry_count} overflows section size"),
107        })?;
108    if cursor.remaining_len() < payload_size {
109        return Err(CoreError::TooShort {
110            have: cursor.remaining_len(),
111            need: payload_size,
112        });
113    }
114    let mut entries = Vec::with_capacity(entry_count);
115    for index in 0..entry_count {
116        let flag_id = cursor.read_u16_le()?;
117        if flag_id == 0 {
118            return Err(CoreError::Corrupt {
119                reason: format!("feature_flags entry {index}: flag_id 0x0000 is reserved"),
120            });
121        }
122        let required_byte = cursor.read_u8()?;
123        let required = match required_byte {
124            0x00 => false,
125            0x01 => true,
126            other => {
127                return Err(CoreError::Corrupt {
128                    reason: format!(
129                        "feature_flags entry {index}: required byte must be 0x00 or 0x01, got 0x{other:02X}"
130                    ),
131                });
132            }
133        };
134        if entries
135            .iter()
136            .any(|existing: &FeatureFlag| existing.flag_id == flag_id)
137        {
138            return Err(CoreError::Corrupt {
139                reason: format!("feature_flags entry {index}: duplicate flag_id 0x{flag_id:04X}"),
140            });
141        }
142        entries.push(FeatureFlag { flag_id, required });
143    }
144    Ok(FeatureFlags { entries })
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    fn make_flags_bytes(version: u8, entries: &[(u16, u8)]) -> Vec<u8> {
152        let mut bytes = Vec::with_capacity(PREFIX_LEN + entries.len() * ENTRY_LEN);
153        bytes.push(version);
154        let count = u32::try_from(entries.len()).expect("test entries fit in u32");
155        bytes.extend_from_slice(&count.to_le_bytes());
156        for (flag_id, required) in entries {
157            bytes.extend_from_slice(&flag_id.to_le_bytes());
158            bytes.push(*required);
159        }
160        bytes
161    }
162
163    #[test]
164    fn parses_empty_section() {
165        let bytes = make_flags_bytes(FEATURE_FLAGS_SECTION_VERSION, &[]);
166        let mut cursor = ManifestCursor::new(&bytes);
167        let flags = parse_feature_flags_section(&mut cursor).expect("empty section parses");
168        assert!(flags.is_empty());
169        assert_eq!(flags.len(), 0);
170        assert_eq!(cursor.position(), PREFIX_LEN);
171    }
172
173    #[test]
174    fn parses_single_required_ec_flag() {
175        let bytes = make_flags_bytes(FEATURE_FLAGS_SECTION_VERSION, &[(0x0001, 0x01)]);
176        let mut cursor = ManifestCursor::new(&bytes);
177        let flags = parse_feature_flags_section(&mut cursor).expect("single flag parses");
178        assert_eq!(cursor.position(), PREFIX_LEN + ENTRY_LEN);
179        assert_eq!(flags.len(), 1);
180        assert_eq!(
181            flags.entries[0],
182            FeatureFlag {
183                flag_id: 0x0001,
184                required: true,
185            }
186        );
187        assert!(flags.is_required(0x0001));
188        assert!(!flags.is_required(0x0002));
189    }
190
191    #[test]
192    fn parses_mixed_required_and_optional() {
193        let bytes = make_flags_bytes(
194            FEATURE_FLAGS_SECTION_VERSION,
195            &[(0x0001, 0x01), (0x0012, 0x00), (0x0020, 0x01)],
196        );
197        let mut cursor = ManifestCursor::new(&bytes);
198        let flags = parse_feature_flags_section(&mut cursor).expect("mixed flags parse");
199        assert_eq!(cursor.position(), PREFIX_LEN + 3 * ENTRY_LEN);
200        assert_eq!(flags.len(), 3);
201        assert!(flags.is_required(0x0001));
202        assert!(!flags.is_required(0x0012));
203        assert!(flags.is_required(0x0020));
204        assert_eq!(flags.get(0x0012).unwrap().flag_id, 0x0012);
205    }
206
207    #[test]
208    fn parses_after_a_header() {
209        let mut bytes = vec![0u8; 16];
210        bytes[..4].copy_from_slice(b"LMFS");
211        bytes[4..6].copy_from_slice(&1u16.to_le_bytes());
212        bytes[6..8].copy_from_slice(&1u16.to_le_bytes());
213        bytes[8..10].copy_from_slice(&1u16.to_le_bytes());
214        bytes.extend_from_slice(&make_flags_bytes(
215            FEATURE_FLAGS_SECTION_VERSION,
216            &[(0x0001, 0x01)],
217        ));
218        let mut cursor = ManifestCursor::new(&bytes);
219        cursor.skip(16).unwrap();
220        let flags = parse_feature_flags_section(&mut cursor).expect("after header");
221        assert_eq!(flags.len(), 1);
222        assert_eq!(cursor.position(), 16 + PREFIX_LEN + ENTRY_LEN);
223    }
224
225    #[test]
226    fn rejects_unknown_section_version() {
227        let bytes = make_flags_bytes(7, &[]);
228        let mut cursor = ManifestCursor::new(&bytes);
229        match parse_feature_flags_section(&mut cursor) {
230            Err(CoreError::UnsupportedFeature { feature }) => {
231                assert!(feature.contains("version 7"), "got: {feature}");
232            }
233            other => panic!("expected UnsupportedFeature, got {other:?}"),
234        }
235    }
236
237    #[test]
238    fn rejects_short_prefix() {
239        // Section version 1 reads OK, but the count u32 has only 3
240        // bytes available.
241        let bytes = [FEATURE_FLAGS_SECTION_VERSION, 0, 0, 0];
242        let mut cursor = ManifestCursor::new(&bytes);
243        match parse_feature_flags_section(&mut cursor) {
244            Err(CoreError::TooShort { have, need }) => {
245                assert_eq!(have, 3);
246                assert_eq!(need, 4);
247            }
248            other => panic!("expected TooShort, got {other:?}"),
249        }
250    }
251
252    #[test]
253    fn rejects_truncated_entries() {
254        let mut bytes = Vec::new();
255        bytes.push(FEATURE_FLAGS_SECTION_VERSION);
256        bytes.extend_from_slice(&10u32.to_le_bytes());
257        bytes.extend_from_slice(&[0x01, 0x00, 0x01]); // 1 entry instead of 10
258        let mut cursor = ManifestCursor::new(&bytes);
259        match parse_feature_flags_section(&mut cursor) {
260            Err(CoreError::TooShort { have, need }) => {
261                // Pre-allocation check fires after reading the prefix
262                // (5 bytes) but before consuming entries. The 3 entry
263                // bytes provided are less than the 30 declared.
264                assert_eq!(have, 3);
265                assert_eq!(need, 10 * ENTRY_LEN);
266            }
267            other => panic!("expected TooShort, got {other:?}"),
268        }
269    }
270
271    #[test]
272    fn rejects_zero_flag_id() {
273        let bytes = make_flags_bytes(FEATURE_FLAGS_SECTION_VERSION, &[(0x0000, 0x01)]);
274        let mut cursor = ManifestCursor::new(&bytes);
275        match parse_feature_flags_section(&mut cursor) {
276            Err(CoreError::Corrupt { reason }) => {
277                assert!(reason.contains("0x0000"), "got: {reason}");
278                assert!(reason.contains("reserved"));
279            }
280            other => panic!("expected Corrupt, got {other:?}"),
281        }
282    }
283
284    #[test]
285    fn rejects_bad_required_byte() {
286        let bytes = make_flags_bytes(FEATURE_FLAGS_SECTION_VERSION, &[(0x0001, 0x05)]);
287        let mut cursor = ManifestCursor::new(&bytes);
288        match parse_feature_flags_section(&mut cursor) {
289            Err(CoreError::Corrupt { reason }) => {
290                assert!(reason.contains("required"), "got: {reason}");
291                assert!(reason.contains("0x05"));
292            }
293            other => panic!("expected Corrupt, got {other:?}"),
294        }
295    }
296
297    #[test]
298    fn rejects_duplicate_flag_id() {
299        let bytes = make_flags_bytes(
300            FEATURE_FLAGS_SECTION_VERSION,
301            &[(0x0001, 0x01), (0x0001, 0x00)],
302        );
303        let mut cursor = ManifestCursor::new(&bytes);
304        match parse_feature_flags_section(&mut cursor) {
305            Err(CoreError::Corrupt { reason }) => {
306                assert!(reason.contains("duplicate"), "got: {reason}");
307                assert!(reason.contains("0x0001"));
308            }
309            other => panic!("expected Corrupt, got {other:?}"),
310        }
311    }
312
313    #[test]
314    fn round_trip_all_standard_flags() {
315        let standard: &[(u16, u8)] = &[
316            (0x0001, 0x01),
317            (0x0002, 0x00),
318            (0x0010, 0x00),
319            (0x0011, 0x00),
320            (0x0012, 0x01),
321            (0x0013, 0x00),
322            (0x0014, 0x00),
323            (0x0020, 0x01),
324            (0x0021, 0x00),
325            (0x0022, 0x00),
326            (0x0100, 0x00),
327            (0x0101, 0x00),
328        ];
329        let bytes = make_flags_bytes(FEATURE_FLAGS_SECTION_VERSION, standard);
330        let mut cursor = ManifestCursor::new(&bytes);
331        let flags = parse_feature_flags_section(&mut cursor).expect("all standard parse");
332        assert_eq!(flags.len(), standard.len());
333        assert_eq!(cursor.position(), PREFIX_LEN + standard.len() * ENTRY_LEN);
334        for (entry, (flag_id, required)) in flags.entries.iter().zip(standard.iter()) {
335            assert_eq!(entry.flag_id, *flag_id);
336            assert_eq!(entry.required, *required != 0);
337        }
338    }
339}