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}
45
46impl LnmpFileMode {
47    /// Converts a raw byte into a mode.
48    pub fn from_byte(value: u8) -> Result<Self, LnmpContainerError> {
49        match value {
50            0x01 => Ok(Self::Text),
51            0x02 => Ok(Self::Binary),
52            0x03 => Ok(Self::Stream),
53            0x04 => Ok(Self::Delta),
54            0x05 => Ok(Self::QuantumSafe),
55            0x06 => Ok(Self::Embedding),
56            other => Err(LnmpContainerError::UnknownMode(other)),
57        }
58    }
59
60    /// Returns the mode identifier as a byte.
61    pub const fn as_byte(self) -> u8 {
62        self as u8
63    }
64}
65
66/// Structured representation of the `.lnmp` header.
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub struct LnmpContainerHeader {
69    /// Header version.
70    pub version: u8,
71    /// Container mode.
72    pub mode: LnmpFileMode,
73    /// Flags written in big-endian order.
74    pub flags: u16,
75    /// Length of metadata that follows the header (big-endian).
76    pub metadata_len: u32,
77}
78
79impl LnmpContainerHeader {
80    /// Creates a new header with default values.
81    pub const fn new(mode: LnmpFileMode) -> Self {
82        Self {
83            version: LNMP_CONTAINER_VERSION_1,
84            mode,
85            flags: 0,
86            metadata_len: 0,
87        }
88    }
89
90    /// Parses bytes into a header.
91    pub fn parse(bytes: &[u8]) -> Result<Self, LnmpContainerError> {
92        if bytes.len() < LNMP_HEADER_SIZE {
93            return Err(LnmpContainerError::TruncatedHeader);
94        }
95
96        if bytes[0..4] != LNMP_MAGIC {
97            return Err(LnmpContainerError::InvalidMagic);
98        }
99
100        let version = bytes[4];
101        if version != LNMP_CONTAINER_VERSION_1 {
102            return Err(LnmpContainerError::UnsupportedVersion(version));
103        }
104
105        let mode = LnmpFileMode::from_byte(bytes[5])?;
106        let flags = u16::from_be_bytes([bytes[6], bytes[7]]);
107        let metadata_len = u32::from_be_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]);
108
109        Ok(Self {
110            version,
111            mode,
112            flags,
113            metadata_len,
114        })
115    }
116
117    /// Serializes the header into bytes.
118    pub fn encode(&self) -> [u8; LNMP_HEADER_SIZE] {
119        let mut buf = [0u8; LNMP_HEADER_SIZE];
120        buf[0..4].copy_from_slice(&LNMP_MAGIC);
121        buf[4] = self.version;
122        buf[5] = self.mode.as_byte();
123        buf[6..8].copy_from_slice(&self.flags.to_be_bytes());
124        buf[8..12].copy_from_slice(&self.metadata_len.to_be_bytes());
125        buf
126    }
127}
128
129/// Errors that can occur while handling headers.
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub enum LnmpContainerError {
132    /// Header is shorter than expected.
133    TruncatedHeader,
134    /// Magic bytes do not match `LNMP`.
135    InvalidMagic,
136    /// Container version is not supported.
137    UnsupportedVersion(u8),
138    /// Mode identifier is unknown.
139    UnknownMode(u8),
140}
141
142impl fmt::Display for LnmpContainerError {
143    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144        match self {
145            LnmpContainerError::TruncatedHeader => write!(f, "LNMP header is truncated"),
146            LnmpContainerError::InvalidMagic => write!(f, "LNMP magic does not match"),
147            LnmpContainerError::UnsupportedVersion(v) => {
148                write!(f, "LNMP header version {v} is not supported")
149            }
150            LnmpContainerError::UnknownMode(mode) => {
151                write!(f, "LNMP mode {mode:#04x} is not recognized")
152            }
153        }
154    }
155}
156
157impl std::error::Error for LnmpContainerError {}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    #[test]
164    fn parse_valid_header() {
165        let header = LnmpContainerHeader::new(LnmpFileMode::Binary);
166        let encoded = header.encode();
167        let parsed = LnmpContainerHeader::parse(&encoded).unwrap();
168        assert_eq!(parsed.mode, LnmpFileMode::Binary);
169        assert_eq!(parsed.version, LNMP_CONTAINER_VERSION_1);
170        assert_eq!(parsed.flags, 0);
171        assert_eq!(parsed.metadata_len, 0);
172    }
173
174    #[test]
175    fn detect_invalid_magic() {
176        let mut bytes = [0u8; LNMP_HEADER_SIZE];
177        bytes[0..4].copy_from_slice(b"FOO!");
178        assert!(matches!(
179            LnmpContainerHeader::parse(&bytes),
180            Err(LnmpContainerError::InvalidMagic)
181        ));
182    }
183
184    #[test]
185    fn detect_unknown_mode() {
186        let mut header = LnmpContainerHeader::new(LnmpFileMode::Text).encode();
187        header[5] = 0xFF;
188        assert!(matches!(
189            LnmpContainerHeader::parse(&header),
190            Err(LnmpContainerError::UnknownMode(0xFF))
191        ));
192    }
193
194    #[test]
195    fn detect_truncated_header() {
196        assert!(matches!(
197            LnmpContainerHeader::parse(&[0u8; 4]),
198            Err(LnmpContainerError::TruncatedHeader)
199        ));
200    }
201
202    #[test]
203    fn test_embedding_mode() {
204        let header = LnmpContainerHeader::new(LnmpFileMode::Embedding);
205        let encoded = header.encode();
206        let parsed = LnmpContainerHeader::parse(&encoded).unwrap();
207        assert_eq!(parsed.mode, LnmpFileMode::Embedding);
208    }
209}