Skip to main content

limnifs_core/
header.rs

1//! Manifest header (spec §5.1, `bit-level/35-manifest-header.md`).
2//!
3//! The first 16 bytes of every `.lim` image manifest. Magic `LMFS`,
4//! three independent u16 LE version fields, and a 6-byte reserved
5//! field that MUST be zero.
6
7use crate::cursor::ManifestCursor;
8use crate::error::CoreError;
9use limnifs_format::{MANIFEST_HEADER_LEN, MANIFEST_MAGIC};
10
11/// Parsed 16-byte manifest header.
12///
13/// Field widths are fixed: magic (4) + three u16 LE versions (6) +
14/// reserved (6). Reserved MUST be zero. The reserved width reconciles
15/// the spec's "first 16 bytes" framing with the field table (which
16/// otherwise sums to 14) — see `bit-level/35-manifest-header.md` and
17/// limnifs/spec#13.
18#[derive(Copy, Clone, Debug, Eq, PartialEq)]
19pub struct ManifestHeader {
20    pub drop_store_version: u16,
21    pub metadata_version: u16,
22    pub manifest_version: u16,
23}
24
25impl ManifestHeader {
26    /// The currently implemented version of each layer. A header that
27    /// names a future version is parseable structurally but flagged by
28    /// higher layers (feature-flag policy, spec §18).
29    pub const CURRENT_DROP_STORE_VERSION: u16 = 1;
30    pub const CURRENT_METADATA_VERSION: u16 = 1;
31    pub const CURRENT_MANIFEST_VERSION: u16 = 1;
32
33    #[must_use]
34    pub const fn new(
35        drop_store_version: u16,
36        metadata_version: u16,
37        manifest_version: u16,
38    ) -> Self {
39        Self {
40            drop_store_version,
41            metadata_version,
42            manifest_version,
43        }
44    }
45
46    #[must_use]
47    pub fn current() -> Self {
48        Self::new(
49            Self::CURRENT_DROP_STORE_VERSION,
50            Self::CURRENT_METADATA_VERSION,
51            Self::CURRENT_MANIFEST_VERSION,
52        )
53    }
54
55    /// Serialise to the 16-byte wire form. Inverse of [`parse_manifest_header`].
56    #[must_use]
57    pub fn to_bytes(self) -> [u8; MANIFEST_HEADER_LEN] {
58        let mut out = [0u8; MANIFEST_HEADER_LEN];
59        out[..4].copy_from_slice(&MANIFEST_MAGIC);
60        out[4..6].copy_from_slice(&self.drop_store_version.to_le_bytes());
61        out[6..8].copy_from_slice(&self.metadata_version.to_le_bytes());
62        out[8..10].copy_from_slice(&self.manifest_version.to_le_bytes());
63        // bytes 10..16 are reserved, already zero
64        out
65    }
66}
67
68/// Parse the manifest header from the cursor's current position.
69///
70/// Advances the cursor by [`MANIFEST_HEADER_LEN`] bytes on success.
71///
72/// # Errors
73///
74/// - [`CoreError::BadMagic`] if the first 4 bytes are not `LMFS`.
75/// - [`CoreError::TooShort`] if the cursor has fewer than 16 bytes.
76/// - [`CoreError::Corrupt`] if the reserved field is non-zero.
77///
78/// Version validity (e.g. "is `manifest_version` 1 supported?") is a
79/// higher-layer concern, handled by feature-flag policy (§18) and the
80/// registry data (§14). The header parser only checks structural
81/// invariants; it does not enforce version policy.
82pub fn parse_manifest_header(cursor: &mut ManifestCursor<'_>) -> Result<ManifestHeader, CoreError> {
83    let magic = cursor.read_magic()?;
84    if magic != MANIFEST_MAGIC {
85        return Err(CoreError::BadMagic { found: magic });
86    }
87    let drop_store_version = cursor.read_u16_le()?;
88    let metadata_version = cursor.read_u16_le()?;
89    let manifest_version = cursor.read_u16_le()?;
90    let reserved = cursor.read_n(6)?;
91    if reserved.iter().any(|&b| b != 0) {
92        return Err(CoreError::Corrupt {
93            reason: format!(
94                "reserved bytes 10..{MANIFEST_HEADER_LEN} must be zero, found {reserved:?}"
95            ),
96        });
97    }
98    Ok(ManifestHeader {
99        drop_store_version,
100        metadata_version,
101        manifest_version,
102    })
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    fn current_header_bytes() -> [u8; MANIFEST_HEADER_LEN] {
110        let mut bytes = [0u8; MANIFEST_HEADER_LEN];
111        bytes[..4].copy_from_slice(b"LMFS");
112        bytes[4..6].copy_from_slice(&1u16.to_le_bytes());
113        bytes[6..8].copy_from_slice(&1u16.to_le_bytes());
114        bytes[8..10].copy_from_slice(&1u16.to_le_bytes());
115        bytes
116    }
117
118    #[test]
119    fn parses_current_header() {
120        let bytes = current_header_bytes();
121        let mut cursor = ManifestCursor::new(&bytes);
122        let header = parse_manifest_header(&mut cursor).expect("current header parses");
123        assert_eq!(header, ManifestHeader::current());
124        assert_eq!(cursor.position(), MANIFEST_HEADER_LEN);
125    }
126
127    #[test]
128    fn parses_zero_versions() {
129        let mut bytes = current_header_bytes();
130        bytes[4..10].fill(0);
131        let mut cursor = ManifestCursor::new(&bytes);
132        let header = parse_manifest_header(&mut cursor).expect("zero versions parse");
133        assert_eq!(header.drop_store_version, 0);
134        assert_eq!(header.metadata_version, 0);
135        assert_eq!(header.manifest_version, 0);
136    }
137
138    #[test]
139    fn rejects_short_buffer() {
140        // Valid magic + valid versions, but the reserved field is
141        // truncated. Cursor's read_n(6) returns TooShort when it
142        // reaches the missing bytes.
143        let mut short = [0u8; 12];
144        short[..4].copy_from_slice(b"LMFS");
145        short[4..6].copy_from_slice(&1u16.to_le_bytes());
146        short[6..8].copy_from_slice(&1u16.to_le_bytes());
147        short[8..10].copy_from_slice(&1u16.to_le_bytes());
148        let mut cursor = ManifestCursor::new(&short);
149        match parse_manifest_header(&mut cursor) {
150            Err(CoreError::TooShort { have, need }) => {
151                assert_eq!(have, 2);
152                assert_eq!(need, 6);
153            }
154            other => panic!("expected TooShort, got {other:?}"),
155        }
156    }
157
158    #[test]
159    fn rejects_empty_buffer() {
160        let empty: [u8; 0] = [];
161        let mut cursor = ManifestCursor::new(&empty);
162        assert!(matches!(
163            parse_manifest_header(&mut cursor),
164            Err(CoreError::TooShort { .. })
165        ));
166    }
167
168    #[test]
169    fn rejects_bad_magic() {
170        let mut bytes = current_header_bytes();
171        bytes[0] = b'X';
172        let mut cursor = ManifestCursor::new(&bytes);
173        match parse_manifest_header(&mut cursor) {
174            Err(CoreError::BadMagic { found }) => {
175                assert_eq!(found, *b"XMFS");
176            }
177            other => panic!("expected BadMagic, got {other:?}"),
178        }
179    }
180
181    #[test]
182    fn rejects_nonzero_reserved() {
183        let mut bytes = current_header_bytes();
184        bytes[13] = 0x01;
185        let mut cursor = ManifestCursor::new(&bytes);
186        match parse_manifest_header(&mut cursor) {
187            Err(CoreError::Corrupt { reason }) => {
188                assert!(reason.contains("reserved"), "reason was: {reason}");
189            }
190            other => panic!("expected Corrupt, got {other:?}"),
191        }
192    }
193
194    #[test]
195    fn accepts_extra_bytes_after_header() {
196        let mut bytes = vec![0u8; MANIFEST_HEADER_LEN + 32];
197        bytes[..MANIFEST_HEADER_LEN].copy_from_slice(&current_header_bytes());
198        let mut cursor = ManifestCursor::new(&bytes);
199        let header = parse_manifest_header(&mut cursor).expect("extra bytes are ignored");
200        assert_eq!(header, ManifestHeader::current());
201        assert_eq!(cursor.position(), MANIFEST_HEADER_LEN);
202        assert_eq!(cursor.remaining_len(), 32);
203    }
204
205    #[test]
206    fn round_trip_to_bytes() {
207        let header = ManifestHeader::new(7, 11, 13);
208        let bytes = header.to_bytes();
209        assert_eq!(&bytes[..4], b"LMFS");
210        assert_eq!(u16::from_le_bytes([bytes[4], bytes[5]]), 7);
211        assert_eq!(u16::from_le_bytes([bytes[6], bytes[7]]), 11);
212        assert_eq!(u16::from_le_bytes([bytes[8], bytes[9]]), 13);
213        assert_eq!(&bytes[10..MANIFEST_HEADER_LEN], &[0, 0, 0, 0, 0, 0]);
214
215        let mut cursor = ManifestCursor::new(&bytes);
216        let reparsed = parse_manifest_header(&mut cursor).expect("roundtrip");
217        assert_eq!(reparsed, header);
218    }
219}