lnmp_codec/
container.rs

1//! Container-aware helpers that bridge `.lnmp` headers with codec entry points.
2
3use std::{fmt, str};
4
5use crate::{
6    binary::{delta::DeltaApplyContext, BinaryDecoder, BinaryEncoder, BinaryError},
7    Encoder, LnmpError, Parser,
8};
9use lnmp_core::{
10    LnmpContainerError, LnmpContainerHeader, LnmpFileMode, LnmpRecord, LNMP_FLAG_CHECKSUM_REQUIRED,
11    LNMP_FLAG_COMPRESSED, LNMP_FLAG_ENCRYPTED, LNMP_HEADER_SIZE,
12};
13
14/// Borrowed view over a `.lnmp` container.
15#[derive(Debug, Clone, Copy)]
16pub struct ContainerFrame<'a> {
17    header: LnmpContainerHeader,
18    metadata: &'a [u8],
19    payload: &'a [u8],
20}
21
22/// Helper that builds `.lnmp` containers from parsed records or raw payloads.
23#[derive(Debug, Clone)]
24pub struct ContainerBuilder {
25    header: LnmpContainerHeader,
26    metadata: Vec<u8>,
27    checksum_confirmed: bool,
28    stream_meta: Option<StreamMetadata>,
29    delta_meta: Option<DeltaMetadata>,
30}
31
32/// Decoded view over stream metadata (mode `0x03`).
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub struct StreamMetadata {
35    /// Preferred chunk size in bytes.
36    pub chunk_size: u32,
37    /// Checksum type (0 = none, 1 = XOR32, 2 = SC32).
38    pub checksum_type: u8,
39    /// Stream flags bitfield.
40    pub flags: u8,
41}
42
43/// Decoded view over delta metadata (mode `0x04`).
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub struct DeltaMetadata {
46    /// Base snapshot identifier.
47    pub base_snapshot: u64,
48    /// Delta algorithm identifier.
49    pub algorithm: u8,
50    /// Compression hint identifier.
51    pub compression: u8,
52}
53
54impl ContainerBuilder {
55    /// Starts a builder for the given mode.
56    pub fn new(mode: LnmpFileMode) -> Self {
57        Self {
58            header: LnmpContainerHeader::new(mode),
59            metadata: Vec::new(),
60            checksum_confirmed: true,
61            stream_meta: None,
62            delta_meta: None,
63        }
64    }
65
66    /// Overrides the header flags.
67    pub fn with_flags(mut self, flags: u16) -> Self {
68        self.header.flags = flags;
69        self
70    }
71
72    /// Attaches metadata bytes that are written after the header.
73    pub fn with_metadata(mut self, metadata: Vec<u8>) -> Result<Self, ContainerEncodeError> {
74        self.header.metadata_len = Self::checked_metadata_len(metadata.len())?;
75        self.metadata = metadata;
76        Ok(self)
77    }
78
79    /// Attaches metadata from a borrowed buffer.
80    pub fn with_metadata_bytes(self, metadata: &[u8]) -> Result<Self, ContainerEncodeError> {
81        self.with_metadata(metadata.to_vec())
82    }
83
84    /// Indicates whether checksum hints are present when `checksum` flag is set.
85    pub fn with_checksum_confirmation(mut self, confirmed: bool) -> Self {
86        self.checksum_confirmed = confirmed;
87        self
88    }
89
90    /// Returns the current header snapshot.
91    pub const fn header(&self) -> LnmpContainerHeader {
92        self.header
93    }
94
95    /// Wraps an existing payload slice with the configured header/metadata.
96    pub fn wrap_payload(self, payload: &[u8]) -> Result<Vec<u8>, ContainerEncodeError> {
97        self.wrap_payload_internal(payload)
98    }
99
100    /// Encodes a record according to the selected mode and wraps it in a container.
101    pub fn encode_record(self, record: &LnmpRecord) -> Result<Vec<u8>, ContainerEncodeError> {
102        self.validate_flags()?;
103        self.validate_checksum_requirements()?;
104        match self.header.mode {
105            LnmpFileMode::Text => {
106                let encoder = Encoder::new();
107                let text = encoder.encode(record);
108                self.wrap_payload_internal(text.as_bytes())
109            }
110            LnmpFileMode::Binary | LnmpFileMode::Embedding => {
111                let encoder = BinaryEncoder::new();
112                let binary = encoder
113                    .encode(record)
114                    .map_err(ContainerEncodeError::BinaryCodec)?;
115                self.wrap_payload_internal(&binary)
116            }
117            mode => Err(ContainerEncodeError::UnsupportedMode(mode)),
118        }
119    }
120
121    /// Attaches stream metadata and switches mode to stream.
122    pub fn with_stream_metadata(
123        mut self,
124        meta: StreamMetadata,
125    ) -> Result<Self, ContainerEncodeError> {
126        self.header.mode = LnmpFileMode::Stream;
127        self.stream_meta = Some(meta);
128        Ok(self)
129    }
130
131    /// Attaches delta metadata and switches mode to delta.
132    pub fn with_delta_metadata(
133        mut self,
134        meta: DeltaMetadata,
135    ) -> Result<Self, ContainerEncodeError> {
136        self.header.mode = LnmpFileMode::Delta;
137        self.delta_meta = Some(meta);
138        Ok(self)
139    }
140
141    fn wrap_payload_internal(mut self, payload: &[u8]) -> Result<Vec<u8>, ContainerEncodeError> {
142        self.populate_auto_metadata()?;
143        self.validate_flags()?;
144        encode_validate_metadata_requirements(self.header.mode, self.metadata.len())?;
145        encode_validate_metadata_semantics(self.header.mode, &self.metadata)?;
146        let mut buffer = Vec::with_capacity(LNMP_HEADER_SIZE + self.metadata.len() + payload.len());
147        buffer.extend_from_slice(&self.header.encode());
148        buffer.extend_from_slice(&self.metadata);
149        buffer.extend_from_slice(payload);
150        Ok(buffer)
151    }
152
153    fn checked_metadata_len(len: usize) -> Result<u32, ContainerEncodeError> {
154        u32::try_from(len).map_err(|_| ContainerEncodeError::MetadataTooLarge(len))
155    }
156
157    fn populate_auto_metadata(&mut self) -> Result<(), ContainerEncodeError> {
158        if let Some(meta) = self.stream_meta {
159            let mut buf = Vec::with_capacity(6);
160            buf.extend_from_slice(&meta.chunk_size.to_be_bytes());
161            buf.push(meta.checksum_type);
162            buf.push(meta.flags);
163            self.header.metadata_len = Self::checked_metadata_len(buf.len())?;
164            self.metadata = buf;
165        } else if let Some(meta) = self.delta_meta {
166            let mut buf = Vec::with_capacity(10);
167            buf.extend_from_slice(&meta.base_snapshot.to_be_bytes());
168            buf.push(meta.algorithm);
169            buf.push(meta.compression);
170            self.header.metadata_len = Self::checked_metadata_len(buf.len())?;
171            self.metadata = buf;
172        } else if self.header.metadata_len as usize != self.metadata.len() {
173            self.header.metadata_len = Self::checked_metadata_len(self.metadata.len())?;
174        }
175        Ok(())
176    }
177
178    fn validate_flags(&self) -> Result<(), ContainerEncodeError> {
179        let flags = self.header.flags;
180        // In v1 only the checksum flag is allowed.
181        let reserved = flags & !LNMP_FLAG_CHECKSUM_REQUIRED;
182        if reserved != 0 {
183            return Err(ContainerEncodeError::ReservedFlags(reserved));
184        }
185        if flags & (LNMP_FLAG_COMPRESSED | LNMP_FLAG_ENCRYPTED) != 0 {
186            return Err(ContainerEncodeError::UnsupportedFlags(
187                flags & (LNMP_FLAG_COMPRESSED | LNMP_FLAG_ENCRYPTED),
188            ));
189        }
190        Ok(())
191    }
192
193    fn validate_checksum_requirements(&self) -> Result<(), ContainerEncodeError> {
194        if self.header.flags & LNMP_FLAG_CHECKSUM_REQUIRED == 0 {
195            return Ok(());
196        }
197        if !self.checksum_confirmed {
198            return Err(ContainerEncodeError::ChecksumFlagMissingHints);
199        }
200        Ok(())
201    }
202}
203
204impl<'a> ContainerFrame<'a> {
205    /// Parses a `.lnmp` container from raw bytes.
206    pub fn parse(bytes: &'a [u8]) -> Result<Self, ContainerFrameError> {
207        if bytes.len() < LNMP_HEADER_SIZE {
208            return Err(ContainerFrameError::Header(
209                LnmpContainerError::TruncatedHeader,
210            ));
211        }
212
213        let header_bytes = &bytes[..LNMP_HEADER_SIZE];
214        let header =
215            LnmpContainerHeader::parse(header_bytes).map_err(ContainerFrameError::Header)?;
216
217        let metadata_len = usize::try_from(header.metadata_len)
218            .map_err(|_| ContainerFrameError::MetadataLengthOverflow(header.metadata_len))?;
219
220        let available = bytes.len() - LNMP_HEADER_SIZE;
221        if available < metadata_len {
222            return Err(ContainerFrameError::TruncatedMetadata {
223                expected: header.metadata_len,
224                available,
225            });
226        }
227
228        let metadata_start = LNMP_HEADER_SIZE;
229        let metadata_end = metadata_start + metadata_len;
230        let metadata = &bytes[metadata_start..metadata_end];
231        let payload = &bytes[metadata_end..];
232
233        validate_reserved_flags(header.flags)?;
234        validate_metadata_requirements(header.mode, metadata_len)?;
235        validate_metadata_semantics(header.mode, metadata)?;
236
237        Ok(Self {
238            header,
239            metadata,
240            payload,
241        })
242    }
243
244    /// Header describing this container.
245    pub const fn header(&self) -> LnmpContainerHeader {
246        self.header
247    }
248
249    /// Metadata segment placed immediately after the header.
250    pub fn metadata(&self) -> &'a [u8] {
251        self.metadata
252    }
253
254    /// Raw payload region.
255    pub fn payload(&self) -> &'a [u8] {
256        self.payload
257    }
258
259    /// Builds a delta apply context from the metadata (if mode is Delta).
260    pub fn delta_apply_context(&self) -> Option<DeltaApplyContext> {
261        if self.header.mode != LnmpFileMode::Delta {
262            return None;
263        }
264        let meta = parse_delta_metadata(self.metadata).ok()?;
265        Some(delta_apply_context_from_metadata(&meta))
266    }
267
268    /// Returns a typed view over the payload bytes.
269    pub fn body(&self) -> ContainerBody<'a> {
270        match self.header.mode {
271            LnmpFileMode::Text => ContainerBody::Text(self.payload),
272            LnmpFileMode::Binary => ContainerBody::Binary(self.payload),
273            LnmpFileMode::Stream => ContainerBody::Stream(self.payload),
274            LnmpFileMode::Delta => ContainerBody::Delta(self.payload),
275            LnmpFileMode::QuantumSafe => ContainerBody::QuantumSafe(self.payload),
276            LnmpFileMode::Embedding => ContainerBody::Embedding(self.payload),
277        }
278    }
279
280    /// Decodes the payload into a [`LnmpRecord`] using mode-specific codecs.
281    pub fn decode_record(&self) -> Result<LnmpRecord, ContainerDecodeError> {
282        match self.header.mode {
283            LnmpFileMode::Text => self.decode_text_record(),
284            LnmpFileMode::Binary => self.decode_binary_record(),
285            mode => Err(ContainerDecodeError::UnsupportedMode(mode)),
286        }
287    }
288
289    /// Parses stream metadata if present (mode `0x03`).
290    pub fn stream_metadata(&self) -> Option<Result<StreamMetadata, MetadataError>> {
291        if self.header.mode != LnmpFileMode::Stream {
292            return None;
293        }
294        Some(parse_stream_metadata(self.metadata))
295    }
296
297    /// Parses delta metadata if present (mode `0x04`).
298    pub fn delta_metadata(&self) -> Option<Result<DeltaMetadata, MetadataError>> {
299        if self.header.mode != LnmpFileMode::Delta {
300            return None;
301        }
302        Some(parse_delta_metadata(self.metadata))
303    }
304
305    /// Canonicalizes the payload into LNMP text using [`Encoder`].
306    pub fn decode_to_text(&self) -> Result<String, ContainerDecodeError> {
307        let record = self.decode_record()?;
308        let encoder = Encoder::new();
309        Ok(encoder.encode(&record))
310    }
311
312    fn decode_text_record(&self) -> Result<LnmpRecord, ContainerDecodeError> {
313        let text = str::from_utf8(self.payload).map_err(ContainerDecodeError::InvalidUtf8)?;
314        let mut parser = Parser::new(text).map_err(ContainerDecodeError::TextCodec)?;
315        parser
316            .parse_record()
317            .map_err(ContainerDecodeError::TextCodec)
318    }
319
320    fn decode_binary_record(&self) -> Result<LnmpRecord, ContainerDecodeError> {
321        let decoder = BinaryDecoder::new();
322        decoder
323            .decode(self.payload)
324            .map_err(ContainerDecodeError::BinaryCodec)
325    }
326}
327
328/// High-level view over the payload region for each mode.
329#[derive(Debug, Clone, Copy, PartialEq, Eq)]
330pub enum ContainerBody<'a> {
331    /// LNMP/Text payload (UTF-8).
332    Text(&'a [u8]),
333    /// LNMP/Binary payload.
334    Binary(&'a [u8]),
335    /// LNMP/Stream payload.
336    Stream(&'a [u8]),
337    /// LNMP/Delta payload.
338    Delta(&'a [u8]),
339    /// LNMP/Quantum-Safe payload.
340    QuantumSafe(&'a [u8]),
341    /// LNMP/Embedding payload.
342    Embedding(&'a [u8]),
343}
344
345/// Errors that can surface while parsing a `.lnmp` container frame.
346#[derive(Debug)]
347pub enum ContainerFrameError {
348    /// Header level validation failed.
349    Header(LnmpContainerError),
350    /// Reserved flags were set in a v1 container.
351    ReservedFlags(u16),
352    /// Metadata length does not satisfy mode requirements.
353    InvalidMetadataLength {
354        /// Mode specified in the header.
355        mode: LnmpFileMode,
356        /// Expected metadata length for this mode.
357        expected: usize,
358        /// Actual metadata length from the header.
359        actual: usize,
360    },
361    /// Metadata length exceeded available bytes.
362    TruncatedMetadata {
363        /// Metadata bytes declared in the header.
364        expected: u32,
365        /// Metadata bytes available in the frame.
366        available: usize,
367    },
368    /// Metadata length cannot be represented on this platform.
369    MetadataLengthOverflow(u32),
370    /// Metadata field contains a value that is not allowed for this mode.
371    InvalidMetadataValue {
372        /// Mode specified in the header.
373        mode: LnmpFileMode,
374        /// Field name within the metadata.
375        field: &'static str,
376        /// Offending value.
377        value: u8,
378    },
379}
380
381impl fmt::Display for ContainerFrameError {
382    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
383        match self {
384            ContainerFrameError::Header(err) => write!(f, "{err}"),
385            ContainerFrameError::ReservedFlags(flags) => {
386                write!(f, "reserved flags set in v1 container: 0x{flags:04x}")
387            }
388            ContainerFrameError::InvalidMetadataLength {
389                mode,
390                expected,
391                actual,
392            } => write!(
393                f,
394                "mode {mode:?} requires {expected} metadata bytes but header declares {actual}"
395            ),
396            ContainerFrameError::TruncatedMetadata {
397                expected,
398                available,
399            } => {
400                write!(
401                    f,
402                    "metadata expects {expected} bytes but only {available} are available"
403                )
404            }
405            ContainerFrameError::MetadataLengthOverflow(len) => write!(
406                f,
407                "metadata length {len} cannot be represented on this platform"
408            ),
409            ContainerFrameError::InvalidMetadataValue { mode, field, value } => {
410                write!(
411                    f,
412                    "mode {mode:?} metadata field {field} contains unsupported value 0x{value:02X}"
413                )
414            }
415        }
416    }
417}
418
419impl std::error::Error for ContainerFrameError {
420    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
421        match self {
422            ContainerFrameError::Header(err) => Some(err),
423            ContainerFrameError::ReservedFlags(_) => None,
424            ContainerFrameError::InvalidMetadataLength { .. } => None,
425            _ => None,
426        }
427    }
428}
429
430impl From<LnmpContainerError> for ContainerFrameError {
431    fn from(value: LnmpContainerError) -> Self {
432        Self::Header(value)
433    }
434}
435
436fn validate_reserved_flags(flags: u16) -> Result<(), ContainerFrameError> {
437    const ALLOWED: u16 = LNMP_FLAG_CHECKSUM_REQUIRED;
438    let reserved = flags & !ALLOWED;
439    if reserved != 0 {
440        return Err(ContainerFrameError::ReservedFlags(reserved));
441    }
442    Ok(())
443}
444
445fn validate_metadata_requirements(
446    mode: LnmpFileMode,
447    metadata_len: usize,
448) -> Result<(), ContainerFrameError> {
449    match mode {
450        LnmpFileMode::Stream => {
451            if metadata_len != 6 {
452                return Err(ContainerFrameError::InvalidMetadataLength {
453                    mode,
454                    expected: 6,
455                    actual: metadata_len,
456                });
457            }
458        }
459        LnmpFileMode::Delta => {
460            if metadata_len != 10 {
461                return Err(ContainerFrameError::InvalidMetadataLength {
462                    mode,
463                    expected: 10,
464                    actual: metadata_len,
465                });
466            }
467        }
468        _ => {}
469    }
470    Ok(())
471}
472
473fn validate_metadata_semantics(
474    mode: LnmpFileMode,
475    metadata: &[u8],
476) -> Result<(), ContainerFrameError> {
477    if mode == LnmpFileMode::Delta {
478        if metadata.len() >= 9 {
479            let algorithm = metadata[8];
480            if algorithm != 0x00 && algorithm != 0x01 {
481                return Err(ContainerFrameError::InvalidMetadataValue {
482                    mode,
483                    field: "algorithm",
484                    value: algorithm,
485                });
486            }
487        }
488        if metadata.len() >= 10 {
489            let compression = metadata[9];
490            if compression != 0x00 && compression != 0x01 {
491                return Err(ContainerFrameError::InvalidMetadataValue {
492                    mode,
493                    field: "compression",
494                    value: compression,
495                });
496            }
497        }
498    }
499    Ok(())
500}
501
502fn encode_validate_metadata_requirements(
503    mode: LnmpFileMode,
504    metadata_len: usize,
505) -> Result<(), ContainerEncodeError> {
506    match mode {
507        LnmpFileMode::Stream => {
508            if metadata_len != 6 {
509                return Err(ContainerEncodeError::InvalidMetadataLength {
510                    mode,
511                    expected: 6,
512                    actual: metadata_len,
513                });
514            }
515        }
516        LnmpFileMode::Delta => {
517            if metadata_len != 10 {
518                return Err(ContainerEncodeError::InvalidMetadataLength {
519                    mode,
520                    expected: 10,
521                    actual: metadata_len,
522                });
523            }
524        }
525        _ => {}
526    }
527    Ok(())
528}
529
530fn encode_validate_metadata_semantics(
531    mode: LnmpFileMode,
532    metadata: &[u8],
533) -> Result<(), ContainerEncodeError> {
534    if mode == LnmpFileMode::Delta {
535        if metadata.len() >= 9 {
536            let algorithm = metadata[8];
537            if algorithm != 0x00 && algorithm != 0x01 {
538                return Err(ContainerEncodeError::InvalidMetadataValue {
539                    mode,
540                    field: "algorithm",
541                    value: algorithm,
542                });
543            }
544        }
545        if metadata.len() >= 10 {
546            let compression = metadata[9];
547            if compression != 0x00 && compression != 0x01 {
548                return Err(ContainerEncodeError::InvalidMetadataValue {
549                    mode,
550                    field: "compression",
551                    value: compression,
552                });
553            }
554        }
555    }
556    Ok(())
557}
558
559/// Errors that surface while decoding metadata blocks.
560#[derive(Debug, Clone, PartialEq, Eq)]
561pub enum MetadataError {
562    /// Metadata buffer is too short.
563    Truncated {
564        /// Expected number of bytes.
565        expected: usize,
566        /// Actual metadata length available.
567        actual: usize,
568    },
569}
570
571impl fmt::Display for MetadataError {
572    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
573        match self {
574            MetadataError::Truncated { expected, actual } => {
575                write!(
576                    f,
577                    "metadata too short: expected at least {expected} bytes, found {actual}"
578                )
579            }
580        }
581    }
582}
583
584impl std::error::Error for MetadataError {}
585
586/// Errors returned while decoding the payload content.
587#[derive(Debug)]
588pub enum ContainerDecodeError {
589    /// Failure when parsing the container frame.
590    Frame(ContainerFrameError),
591    /// Payload contained invalid UTF-8 (text mode only).
592    InvalidUtf8(str::Utf8Error),
593    /// Text codec reported an error.
594    TextCodec(LnmpError),
595    /// Binary codec reported an error.
596    BinaryCodec(BinaryError),
597    /// Mode is not currently supported by the decoder.
598    UnsupportedMode(LnmpFileMode),
599}
600
601impl fmt::Display for ContainerDecodeError {
602    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
603        match self {
604            ContainerDecodeError::Frame(err) => write!(f, "{err}"),
605            ContainerDecodeError::InvalidUtf8(err) => write!(f, "invalid UTF-8: {err}"),
606            ContainerDecodeError::TextCodec(err) => write!(f, "{err}"),
607            ContainerDecodeError::BinaryCodec(err) => write!(f, "{err}"),
608            ContainerDecodeError::UnsupportedMode(mode) => {
609                write!(f, "mode {mode:?} is not supported yet")
610            }
611        }
612    }
613}
614
615impl std::error::Error for ContainerDecodeError {
616    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
617        match self {
618            ContainerDecodeError::Frame(err) => Some(err),
619            ContainerDecodeError::InvalidUtf8(err) => Some(err),
620            ContainerDecodeError::TextCodec(err) => Some(err),
621            ContainerDecodeError::BinaryCodec(err) => Some(err),
622            ContainerDecodeError::UnsupportedMode(_) => None,
623        }
624    }
625}
626
627impl From<ContainerFrameError> for ContainerDecodeError {
628    fn from(value: ContainerFrameError) -> Self {
629        Self::Frame(value)
630    }
631}
632
633/// Errors produced while emitting `.lnmp` containers.
634#[derive(Debug)]
635pub enum ContainerEncodeError {
636    /// Metadata payload cannot fit in the header field.
637    MetadataTooLarge(usize),
638    /// Binary encoder failed.
639    BinaryCodec(BinaryError),
640    /// Mode is not supported for encoding helpers.
641    UnsupportedMode(LnmpFileMode),
642    /// Requested flags require capabilities that are not available yet.
643    UnsupportedFlags(u16),
644    /// Reserved flags are set in a v1 container (only checksum is allowed).
645    ReservedFlags(u16),
646    /// Checksum flag set but record lacks checksum hints.
647    ChecksumFlagMissingHints,
648    /// Metadata length does not satisfy mode requirements.
649    InvalidMetadataLength {
650        /// Mode provided.
651        mode: LnmpFileMode,
652        /// Expected metadata length.
653        expected: usize,
654        /// Actual metadata length.
655        actual: usize,
656    },
657    /// Metadata field contains a value that is not allowed for this mode.
658    InvalidMetadataValue {
659        /// Mode provided.
660        mode: LnmpFileMode,
661        /// Field name.
662        field: &'static str,
663        /// Offending value.
664        value: u8,
665    },
666}
667
668impl fmt::Display for ContainerEncodeError {
669    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
670        match self {
671            ContainerEncodeError::MetadataTooLarge(len) => {
672                write!(f, "metadata of length {len} is too large for the container")
673            }
674            ContainerEncodeError::BinaryCodec(err) => write!(f, "{err}"),
675            ContainerEncodeError::UnsupportedMode(mode) => {
676                write!(f, "mode {mode:?} is not supported for encoding yet")
677            }
678            ContainerEncodeError::UnsupportedFlags(bits) => write!(
679                f,
680                "flags {bits:#06X} require compression/encryption which is not implemented"
681            ),
682            ContainerEncodeError::ReservedFlags(bits) => {
683                write!(f, "reserved flags are not allowed in v1: {bits:#06X}")
684            }
685            ContainerEncodeError::ChecksumFlagMissingHints => write!(
686                f,
687                "checksum flag is set but no fields contain embedded checksum hints"
688            ),
689            ContainerEncodeError::InvalidMetadataLength {
690                mode,
691                expected,
692                actual,
693            } => write!(
694                f,
695                "mode {mode:?} requires {expected} metadata bytes but header declares {actual}"
696            ),
697            ContainerEncodeError::InvalidMetadataValue { mode, field, value } => write!(
698                f,
699                "mode {mode:?} metadata field {field} contains unsupported value 0x{value:02X}"
700            ),
701        }
702    }
703}
704
705impl std::error::Error for ContainerEncodeError {
706    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
707        match self {
708            ContainerEncodeError::BinaryCodec(err) => Some(err),
709            _ => None,
710        }
711    }
712}
713
714/// Parses stream metadata bytes into a [`StreamMetadata`] structure.
715pub fn parse_stream_metadata(metadata: &[u8]) -> Result<StreamMetadata, MetadataError> {
716    if metadata.len() < 6 {
717        return Err(MetadataError::Truncated {
718            expected: 6,
719            actual: metadata.len(),
720        });
721    }
722    let chunk_size = u32::from_be_bytes([metadata[0], metadata[1], metadata[2], metadata[3]]);
723    Ok(StreamMetadata {
724        chunk_size,
725        checksum_type: metadata[4],
726        flags: metadata[5],
727    })
728}
729
730/// Parses delta metadata bytes into a [`DeltaMetadata`] structure.
731pub fn parse_delta_metadata(metadata: &[u8]) -> Result<DeltaMetadata, MetadataError> {
732    if metadata.len() < 10 {
733        return Err(MetadataError::Truncated {
734            expected: 10,
735            actual: metadata.len(),
736        });
737    }
738    let base_snapshot = u64::from_be_bytes([
739        metadata[0],
740        metadata[1],
741        metadata[2],
742        metadata[3],
743        metadata[4],
744        metadata[5],
745        metadata[6],
746        metadata[7],
747    ]);
748    Ok(DeltaMetadata {
749        base_snapshot,
750        algorithm: metadata[8],
751        compression: metadata[9],
752    })
753}
754
755/// Constructs a delta apply context from decoded delta metadata.
756pub fn delta_apply_context_from_metadata(meta: &DeltaMetadata) -> DeltaApplyContext {
757    DeltaApplyContext {
758        metadata_base: Some(meta.base_snapshot),
759        required_base: None,
760        algorithm: Some(meta.algorithm),
761        compression: Some(meta.compression),
762    }
763}
764
765#[cfg(test)]
766mod tests {
767    use super::*;
768    use crate::binary::BinaryEncoder;
769    use lnmp_core::{LnmpField, LnmpValue, LNMP_FLAG_CHECKSUM_REQUIRED, LNMP_FLAG_COMPRESSED};
770
771    fn build_container_bytes(mode: LnmpFileMode, metadata: &[u8], payload: &[u8]) -> Vec<u8> {
772        let mut header = LnmpContainerHeader::new(mode);
773        header.metadata_len = metadata.len() as u32;
774        let mut bytes = header.encode().to_vec();
775        bytes.extend_from_slice(metadata);
776        bytes.extend_from_slice(payload);
777        bytes
778    }
779
780    #[test]
781    fn parse_text_frame() {
782        let payload = b"F7=1\nF12=14532\n";
783        let bytes = build_container_bytes(LnmpFileMode::Text, &[], payload);
784        let frame = ContainerFrame::parse(&bytes).unwrap();
785        assert_eq!(frame.metadata(), b"");
786        assert_eq!(frame.payload(), payload);
787        assert_eq!(frame.body(), ContainerBody::Text(payload));
788        assert_eq!(frame.header().mode, LnmpFileMode::Text);
789    }
790
791    #[test]
792    fn decode_text_record() {
793        let payload = b"F7=1\nF12=14532\n";
794        let bytes = build_container_bytes(LnmpFileMode::Text, &[], payload);
795        let frame = ContainerFrame::parse(&bytes).unwrap();
796        let record = frame.decode_record().unwrap();
797        assert_eq!(record.fields().len(), 2);
798        let text = frame.decode_to_text().unwrap();
799        assert!(text.contains("F7=1"));
800        assert!(text.contains("F12=14532"));
801    }
802
803    #[test]
804    fn decode_binary_record() {
805        let mut record = LnmpRecord::new();
806        record.add_field(LnmpField {
807            fid: 7,
808            value: LnmpValue::Bool(true),
809        });
810        record.add_field(LnmpField {
811            fid: 12,
812            value: LnmpValue::Int(14532),
813        });
814        let encoder = BinaryEncoder::new();
815        let binary = encoder.encode(&record).unwrap();
816        let bytes = build_container_bytes(LnmpFileMode::Binary, &[], &binary);
817        let frame = ContainerFrame::parse(&bytes).unwrap();
818        let decoded = frame.decode_record().unwrap();
819        assert_eq!(decoded.fields().len(), 2);
820    }
821
822    #[test]
823    fn detect_truncated_metadata() {
824        let mut header = LnmpContainerHeader::new(LnmpFileMode::Text);
825        header.metadata_len = 4;
826        let mut bytes = header.encode().to_vec();
827        bytes.extend_from_slice(&[0xAA, 0xBB]);
828        let err = ContainerFrame::parse(&bytes).unwrap_err();
829        match err {
830            ContainerFrameError::TruncatedMetadata {
831                expected,
832                available,
833            } => {
834                assert_eq!(expected, 4);
835                assert_eq!(available, 2);
836            }
837            other => panic!("unexpected error: {other:?}"),
838        }
839    }
840
841    #[test]
842    fn builder_wraps_text_record() {
843        let mut record = LnmpRecord::new();
844        record.add_field(LnmpField {
845            fid: 1,
846            value: LnmpValue::Int(42),
847        });
848        let builder = ContainerBuilder::new(LnmpFileMode::Text);
849        let bytes = builder.encode_record(&record).unwrap();
850        let frame = ContainerFrame::parse(&bytes).unwrap();
851        assert_eq!(frame.header().mode, LnmpFileMode::Text);
852    }
853
854    #[test]
855    fn builder_wraps_binary_record() {
856        let mut record = LnmpRecord::new();
857        record.add_field(LnmpField {
858            fid: 1,
859            value: LnmpValue::Int(42),
860        });
861        let builder = ContainerBuilder::new(LnmpFileMode::Binary);
862        let bytes = builder.encode_record(&record).unwrap();
863        let frame = ContainerFrame::parse(&bytes).unwrap();
864        assert_eq!(frame.header().mode, LnmpFileMode::Binary);
865        assert!(!frame.payload().is_empty());
866    }
867
868    #[test]
869    fn builder_rejects_compression_flag() {
870        let mut record = LnmpRecord::new();
871        record.add_field(LnmpField {
872            fid: 1,
873            value: LnmpValue::Int(42),
874        });
875        let builder = ContainerBuilder::new(LnmpFileMode::Text).with_flags(LNMP_FLAG_COMPRESSED);
876        let err = builder.encode_record(&record).unwrap_err();
877        assert!(matches!(err, ContainerEncodeError::ReservedFlags(_)));
878    }
879
880    #[test]
881    fn builder_rejects_reserved_flags() {
882        let record = LnmpRecord::new();
883        let builder = ContainerBuilder::new(LnmpFileMode::Text).with_flags(0x0002);
884        let err = builder.encode_record(&record).unwrap_err();
885        assert!(matches!(err, ContainerEncodeError::ReservedFlags(_)));
886    }
887
888    #[test]
889    fn checksum_flag_requires_hint() {
890        let mut record = LnmpRecord::new();
891        record.add_field(LnmpField {
892            fid: 12,
893            value: LnmpValue::Int(10),
894        });
895        let builder = ContainerBuilder::new(LnmpFileMode::Text)
896            .with_flags(LNMP_FLAG_CHECKSUM_REQUIRED)
897            .with_checksum_confirmation(false);
898        let err = builder.encode_record(&record).unwrap_err();
899        assert!(matches!(
900            err,
901            ContainerEncodeError::ChecksumFlagMissingHints
902        ));
903    }
904
905    #[test]
906    fn builder_requires_stream_metadata_length() {
907        let builder = ContainerBuilder::new(LnmpFileMode::Stream)
908            .with_metadata(vec![])
909            .unwrap();
910        let err = builder.wrap_payload(b"payload").unwrap_err();
911        assert!(matches!(
912            err,
913            ContainerEncodeError::InvalidMetadataLength {
914                expected: 6,
915                actual: 0,
916                ..
917            }
918        ));
919    }
920
921    #[test]
922    fn builder_requires_delta_metadata_length() {
923        let builder = ContainerBuilder::new(LnmpFileMode::Delta)
924            .with_metadata(vec![])
925            .unwrap();
926        let err = builder.wrap_payload(b"payload").unwrap_err();
927        assert!(matches!(
928            err,
929            ContainerEncodeError::InvalidMetadataLength {
930                expected: 10,
931                actual: 0,
932                ..
933            }
934        ));
935    }
936
937    #[test]
938    fn builder_rejects_invalid_delta_algorithm() {
939        let builder = ContainerBuilder::new(LnmpFileMode::Delta)
940            .with_delta_metadata(DeltaMetadata {
941                base_snapshot: 1,
942                algorithm: 0xFF,
943                compression: 0x00,
944            })
945            .unwrap();
946        let err = builder.wrap_payload(b"payload").unwrap_err();
947        assert!(matches!(
948            err,
949            ContainerEncodeError::InvalidMetadataValue {
950                mode: LnmpFileMode::Delta,
951                field: "algorithm",
952                value: 0xFF
953            }
954        ));
955    }
956
957    #[test]
958    fn builder_rejects_invalid_delta_compression() {
959        let builder = ContainerBuilder::new(LnmpFileMode::Delta)
960            .with_delta_metadata(DeltaMetadata {
961                base_snapshot: 1,
962                algorithm: 0x00,
963                compression: 0xFF,
964            })
965            .unwrap();
966        let err = builder.wrap_payload(b"payload").unwrap_err();
967        assert!(matches!(
968            err,
969            ContainerEncodeError::InvalidMetadataValue {
970                mode: LnmpFileMode::Delta,
971                field: "compression",
972                value: 0xFF
973            }
974        ));
975    }
976
977    #[test]
978    fn parse_stream_metadata_bytes() {
979        let bytes = [0x00, 0x00, 0x10, 0x00, 0x02, 0x03];
980        let meta = parse_stream_metadata(&bytes).unwrap();
981        assert_eq!(meta.chunk_size, 4096);
982        assert_eq!(meta.checksum_type, 0x02);
983        assert_eq!(meta.flags, 0x03);
984    }
985
986    #[test]
987    fn parse_delta_metadata_bytes() {
988        let bytes = [0, 0, 0, 0, 0, 0, 0, 5, 0x01, 0x00];
989        let meta = parse_delta_metadata(&bytes).unwrap();
990        assert_eq!(meta.base_snapshot, 5);
991        assert_eq!(meta.algorithm, 0x01);
992        assert_eq!(meta.compression, 0x00);
993    }
994}