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