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