lnmp_core/
container.rs

1//! Definitions for the `.lnmp` container header.
2
3use core::fmt;
4
5/// ASCII `LNMP` magic number.
6pub const LNMP_MAGIC: [u8; 4] = *b"LNMP";
7
8/// First container version.
9pub const LNMP_CONTAINER_VERSION_1: u8 = 1;
10
11/// Header size (magic + version + mode + flags + metadata length).
12pub const LNMP_HEADER_SIZE: usize = 12;
13
14/// Payload must include checksums.
15pub const LNMP_FLAG_CHECKSUM_REQUIRED: u16 = 0x0001;
16/// Payload contains mode-specific compression.
17pub const LNMP_FLAG_COMPRESSED: u16 = 0x0002;
18/// Payload is encrypted.
19pub const LNMP_FLAG_ENCRYPTED: u16 = 0x0004;
20/// Payload contains quantum-safe signature data.
21pub const LNMP_FLAG_QSIG: u16 = 0x0008;
22/// Payload contains quantum-safe key-exchange metadata.
23pub const LNMP_FLAG_QKEX: u16 = 0x0010;
24/// Reserved for signaling a metadata extension block (TLV chain) after fixed metadata.
25/// MUST be `0` in v1; a future version/flag bump is required to enable.
26pub const LNMP_FLAG_EXT_META_BLOCK: u16 = 0x8000;
27
28/// Supported `.lnmp` modes.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30#[repr(u8)]
31pub enum LnmpFileMode {
32    /// LNMP/Text.
33    Text = 0x01,
34    /// LNMP/Binary.
35    Binary = 0x02,
36    /// LNMP/Stream.
37    Stream = 0x03,
38    /// LNMP/Delta.
39    Delta = 0x04,
40    /// LNMP/Quantum-Safe (reserved for future use).
41    QuantumSafe = 0x05,
42    /// LNMP/Embedding.
43    Embedding = 0x06,
44    /// LNMP/Spatial.
45    Spatial = 0x07,
46}
47
48impl LnmpFileMode {
49    /// Converts a raw byte into a mode.
50    pub fn from_byte(value: u8) -> Result<Self, LnmpContainerError> {
51        match value {
52            0x01 => Ok(Self::Text),
53            0x02 => Ok(Self::Binary),
54            0x03 => Ok(Self::Stream),
55            0x04 => Ok(Self::Delta),
56            0x05 => Ok(Self::QuantumSafe),
57            0x06 => Ok(Self::Embedding),
58            0x07 => Ok(Self::Spatial),
59            other => Err(LnmpContainerError::UnknownMode(other)),
60        }
61    }
62
63    /// Returns the mode identifier as a byte.
64    pub const fn as_byte(self) -> u8 {
65        self as u8
66    }
67}
68
69/// Structured representation of the `.lnmp` header.
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub struct LnmpContainerHeader {
72    /// Header version.
73    pub version: u8,
74    /// Container mode.
75    pub mode: LnmpFileMode,
76    /// Flags written in big-endian order.
77    pub flags: u16,
78    /// Length of metadata that follows the header (big-endian).
79    pub metadata_len: u32,
80}
81
82impl LnmpContainerHeader {
83    /// Creates a new header with default values.
84    pub const fn new(mode: LnmpFileMode) -> Self {
85        Self {
86            version: LNMP_CONTAINER_VERSION_1,
87            mode,
88            flags: 0,
89            metadata_len: 0,
90        }
91    }
92
93    /// Parses bytes into a header.
94    pub fn parse(bytes: &[u8]) -> Result<Self, LnmpContainerError> {
95        if bytes.len() < LNMP_HEADER_SIZE {
96            return Err(LnmpContainerError::TruncatedHeader);
97        }
98
99        if bytes[0..4] != LNMP_MAGIC {
100            return Err(LnmpContainerError::InvalidMagic);
101        }
102
103        let version = bytes[4];
104        if version != LNMP_CONTAINER_VERSION_1 {
105            return Err(LnmpContainerError::UnsupportedVersion(version));
106        }
107
108        let mode = LnmpFileMode::from_byte(bytes[5])?;
109        let flags = u16::from_be_bytes([bytes[6], bytes[7]]);
110        let metadata_len = u32::from_be_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]);
111
112        Ok(Self {
113            version,
114            mode,
115            flags,
116            metadata_len,
117        })
118    }
119
120    /// Serializes the header into bytes.
121    pub fn encode(&self) -> [u8; LNMP_HEADER_SIZE] {
122        let mut buf = [0u8; LNMP_HEADER_SIZE];
123        buf[0..4].copy_from_slice(&LNMP_MAGIC);
124        buf[4] = self.version;
125        buf[5] = self.mode.as_byte();
126        buf[6..8].copy_from_slice(&self.flags.to_be_bytes());
127        buf[8..12].copy_from_slice(&self.metadata_len.to_be_bytes());
128        buf
129    }
130}
131
132/// Errors that can occur while handling headers.
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134pub enum LnmpContainerError {
135    /// Header is shorter than expected.
136    TruncatedHeader,
137    /// Magic bytes do not match `LNMP`.
138    InvalidMagic,
139    /// Container version is not supported.
140    UnsupportedVersion(u8),
141    /// Mode identifier is unknown.
142    UnknownMode(u8),
143}
144
145impl fmt::Display for LnmpContainerError {
146    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147        match self {
148            LnmpContainerError::TruncatedHeader => write!(f, "LNMP header is truncated"),
149            LnmpContainerError::InvalidMagic => write!(f, "LNMP magic does not match"),
150            LnmpContainerError::UnsupportedVersion(v) => {
151                write!(f, "LNMP header version {v} is not supported")
152            }
153            LnmpContainerError::UnknownMode(mode) => {
154                write!(f, "LNMP mode {mode:#04x} is not recognized")
155            }
156        }
157    }
158}
159
160impl std::error::Error for LnmpContainerError {}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    #[test]
167    fn parse_valid_header() {
168        let header = LnmpContainerHeader::new(LnmpFileMode::Binary);
169        let encoded = header.encode();
170        let parsed = LnmpContainerHeader::parse(&encoded).unwrap();
171        assert_eq!(parsed.mode, LnmpFileMode::Binary);
172        assert_eq!(parsed.version, LNMP_CONTAINER_VERSION_1);
173        assert_eq!(parsed.flags, 0);
174        assert_eq!(parsed.metadata_len, 0);
175    }
176
177    #[test]
178    fn detect_invalid_magic() {
179        let mut bytes = [0u8; LNMP_HEADER_SIZE];
180        bytes[0..4].copy_from_slice(b"FOO!");
181        assert!(matches!(
182            LnmpContainerHeader::parse(&bytes),
183            Err(LnmpContainerError::InvalidMagic)
184        ));
185    }
186
187    #[test]
188    fn detect_unknown_mode() {
189        let mut header = LnmpContainerHeader::new(LnmpFileMode::Text).encode();
190        header[5] = 0xFF;
191        assert!(matches!(
192            LnmpContainerHeader::parse(&header),
193            Err(LnmpContainerError::UnknownMode(0xFF))
194        ));
195    }
196
197    #[test]
198    fn detect_truncated_header() {
199        assert!(matches!(
200            LnmpContainerHeader::parse(&[0u8; 4]),
201            Err(LnmpContainerError::TruncatedHeader)
202        ));
203    }
204
205    #[test]
206    fn test_embedding_mode() {
207        let header = LnmpContainerHeader::new(LnmpFileMode::Embedding);
208        let encoded = header.encode();
209        let parsed = LnmpContainerHeader::parse(&encoded).unwrap();
210        assert_eq!(parsed.mode, LnmpFileMode::Embedding);
211    }
212}