1use 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#[derive(Debug, Clone, Copy)]
16pub struct ContainerFrame<'a> {
17 header: LnmpContainerHeader,
18 metadata: &'a [u8],
19 payload: &'a [u8],
20}
21
22#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub struct StreamMetadata {
35 pub chunk_size: u32,
37 pub checksum_type: u8,
39 pub flags: u8,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub struct DeltaMetadata {
46 pub base_snapshot: u64,
48 pub algorithm: u8,
50 pub compression: u8,
52}
53
54impl ContainerBuilder {
55 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 pub fn with_flags(mut self, flags: u16) -> Self {
68 self.header.flags = flags;
69 self
70 }
71
72 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 pub fn with_metadata_bytes(self, metadata: &[u8]) -> Result<Self, ContainerEncodeError> {
81 self.with_metadata(metadata.to_vec())
82 }
83
84 pub fn with_checksum_confirmation(mut self, confirmed: bool) -> Self {
86 self.checksum_confirmed = confirmed;
87 self
88 }
89
90 pub const fn header(&self) -> LnmpContainerHeader {
92 self.header
93 }
94
95 pub fn wrap_payload(self, payload: &[u8]) -> Result<Vec<u8>, ContainerEncodeError> {
97 self.wrap_payload_internal(payload)
98 }
99
100 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 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 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 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 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 pub const fn header(&self) -> LnmpContainerHeader {
250 self.header
251 }
252
253 pub fn metadata(&self) -> &'a [u8] {
255 self.metadata
256 }
257
258 pub fn payload(&self) -> &'a [u8] {
260 self.payload
261 }
262
263 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 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 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 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
337pub enum ContainerBody<'a> {
338 Text(&'a [u8]),
340 Binary(&'a [u8]),
342 Stream(&'a [u8]),
344 Delta(&'a [u8]),
346 QuantumSafe(&'a [u8]),
348 Embedding(&'a [u8]),
350 Spatial(&'a [u8]),
352}
353
354#[derive(Debug)]
356pub enum ContainerFrameError {
357 Header(LnmpContainerError),
359 ReservedFlags(u16),
361 InvalidMetadataLength {
363 mode: LnmpFileMode,
365 expected: usize,
367 actual: usize,
369 },
370 TruncatedMetadata {
372 expected: u32,
374 available: usize,
376 },
377 MetadataLengthOverflow(u32),
379 InvalidMetadataValue {
381 mode: LnmpFileMode,
383 field: &'static str,
385 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#[derive(Debug, Clone, PartialEq, Eq)]
570pub enum MetadataError {
571 Truncated {
573 expected: usize,
575 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#[derive(Debug)]
597pub enum ContainerDecodeError {
598 Frame(ContainerFrameError),
600 InvalidUtf8(str::Utf8Error),
602 TextCodec(LnmpError),
604 BinaryCodec(BinaryError),
606 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#[derive(Debug)]
644pub enum ContainerEncodeError {
645 MetadataTooLarge(usize),
647 BinaryCodec(BinaryError),
649 UnsupportedMode(LnmpFileMode),
651 UnsupportedFlags(u16),
653 ReservedFlags(u16),
655 ChecksumFlagMissingHints,
657 InvalidMetadataLength {
659 mode: LnmpFileMode,
661 expected: usize,
663 actual: usize,
665 },
666 InvalidMetadataValue {
668 mode: LnmpFileMode,
670 field: &'static str,
672 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
723pub 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
739pub 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
764pub 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}