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