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 | 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 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 LnmpFileMode::Spatial => ContainerBody::Spatial(self.payload),
278 }
279 }
280
281 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 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
331pub enum ContainerBody<'a> {
332 Text(&'a [u8]),
334 Binary(&'a [u8]),
336 Stream(&'a [u8]),
338 Delta(&'a [u8]),
340 QuantumSafe(&'a [u8]),
342 Embedding(&'a [u8]),
344 Spatial(&'a [u8]),
346}
347
348#[derive(Debug)]
350pub enum ContainerFrameError {
351 Header(LnmpContainerError),
353 ReservedFlags(u16),
355 InvalidMetadataLength {
357 mode: LnmpFileMode,
359 expected: usize,
361 actual: usize,
363 },
364 TruncatedMetadata {
366 expected: u32,
368 available: usize,
370 },
371 MetadataLengthOverflow(u32),
373 InvalidMetadataValue {
375 mode: LnmpFileMode,
377 field: &'static str,
379 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#[derive(Debug, Clone, PartialEq, Eq)]
564pub enum MetadataError {
565 Truncated {
567 expected: usize,
569 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#[derive(Debug)]
591pub enum ContainerDecodeError {
592 Frame(ContainerFrameError),
594 InvalidUtf8(str::Utf8Error),
596 TextCodec(LnmpError),
598 BinaryCodec(BinaryError),
600 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#[derive(Debug)]
638pub enum ContainerEncodeError {
639 MetadataTooLarge(usize),
641 BinaryCodec(BinaryError),
643 UnsupportedMode(LnmpFileMode),
645 UnsupportedFlags(u16),
647 ReservedFlags(u16),
649 ChecksumFlagMissingHints,
651 InvalidMetadataLength {
653 mode: LnmpFileMode,
655 expected: usize,
657 actual: usize,
659 },
660 InvalidMetadataValue {
662 mode: LnmpFileMode,
664 field: &'static str,
666 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
717pub 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
733pub 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
758pub 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}