1use core::fmt;
4
5pub const LNMP_MAGIC: [u8; 4] = *b"LNMP";
7
8pub const LNMP_CONTAINER_VERSION_1: u8 = 1;
10
11pub const LNMP_HEADER_SIZE: usize = 12;
13
14pub const LNMP_FLAG_CHECKSUM_REQUIRED: u16 = 0x0001;
16pub const LNMP_FLAG_COMPRESSED: u16 = 0x0002;
18pub const LNMP_FLAG_ENCRYPTED: u16 = 0x0004;
20pub const LNMP_FLAG_QSIG: u16 = 0x0008;
22pub const LNMP_FLAG_QKEX: u16 = 0x0010;
24pub const LNMP_FLAG_EXT_META_BLOCK: u16 = 0x8000;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30#[repr(u8)]
31pub enum LnmpFileMode {
32 Text = 0x01,
34 Binary = 0x02,
36 Stream = 0x03,
38 Delta = 0x04,
40 QuantumSafe = 0x05,
42 Embedding = 0x06,
44 Spatial = 0x07,
46}
47
48impl LnmpFileMode {
49 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 pub const fn as_byte(self) -> u8 {
65 self as u8
66 }
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub struct LnmpContainerHeader {
72 pub version: u8,
74 pub mode: LnmpFileMode,
76 pub flags: u16,
78 pub metadata_len: u32,
80}
81
82impl LnmpContainerHeader {
83 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134pub enum LnmpContainerError {
135 TruncatedHeader,
137 InvalidMagic,
139 UnsupportedVersion(u8),
141 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}