1use crate::llm::human_formatter::{HumanFormatConfig, HumanFormatter};
7use crate::llm::human_parser::{HumanParseError, HumanParser};
8use crate::llm::parser::{LlmParser, ParseError};
9use crate::llm::serializer::LlmSerializer;
10use crate::llm::types::DxDocument;
11use std::borrow::Cow;
12use thiserror::Error;
13
14#[derive(Debug, Error)]
16pub enum ConvertError {
17 #[error("DX Serializer parse error: {0}")]
19 LlmParse(#[from] ParseError),
20
21 #[error("Human parse error: {0}")]
23 HumanParse(#[from] HumanParseError),
24
25 #[error("Machine format error: {msg}")]
27 MachineFormat {
28 msg: String,
30 },
31}
32
33#[must_use = "conversion result should be used"]
35pub fn llm_to_human(llm_input: &str) -> Result<String, ConvertError> {
36 let doc = LlmParser::parse(llm_input)?;
37 let formatter = HumanFormatter::new();
38 Ok(formatter.format(&doc))
39}
40
41pub fn llm_to_human_with_config(
43 llm_input: &str,
44 config: HumanFormatConfig,
45) -> Result<String, ConvertError> {
46 let doc = LlmParser::parse(llm_input)?;
47 let formatter = HumanFormatter::with_config(config);
48 Ok(formatter.format(&doc))
49}
50
51#[must_use = "conversion result should be used"]
53pub fn human_to_llm(human_input: &str) -> Result<String, ConvertError> {
54 let trimmed = human_input.trim();
55
56 if is_dsr_format(trimmed) {
58 return Ok(human_input.to_string());
59 }
60
61 let parser = HumanParser::new();
63 let doc = parser.parse(human_input)?;
64 let serializer = LlmSerializer::new();
65 Ok(serializer.serialize(&doc))
66}
67
68#[must_use]
70pub fn is_dsr_format(input: &str) -> bool {
71 let trimmed = input.trim();
72
73 let mut has_dsr_indicators = false;
85 let mut has_human_indicators = false;
86
87 for line in trimmed.lines() {
88 let line = line.trim();
89 if line.is_empty() {
90 continue;
91 }
92
93 if line.starts_with('[') {
95 has_human_indicators = true;
96 continue;
97 }
98
99 if line.starts_with('-') {
101 has_human_indicators = true;
102 continue;
103 }
104
105 if line.contains(" = ") {
107 has_human_indicators = true;
108 continue;
109 }
110
111 if line.contains(':') && line.contains('(') && line.contains('[') {
113 has_dsr_indicators = true;
114 continue;
115 }
116
117 if line.contains(':') && line.contains('=') {
119 let colon_pos = line.find(':');
120 let eq_pos = line.find('=');
121 if let (Some(cp), Some(ep)) = (colon_pos, eq_pos) {
122 if cp < ep {
123 has_dsr_indicators = true;
124 continue;
125 }
126 }
127 }
128
129 if line.contains('=') && !line.contains(" = ") {
131 if let Some(eq_pos) = line.find('=') {
132 let before = &line[..eq_pos];
133 let after = &line[eq_pos + 1..];
134 if !before.ends_with(' ') && !after.starts_with(' ') {
136 has_dsr_indicators = true;
137 continue;
138 }
139 }
140 }
141 }
142
143 if has_human_indicators {
145 return false;
146 }
147
148 has_dsr_indicators
150}
151
152#[must_use]
154pub fn is_llm_format(input: &str) -> bool {
155 is_dsr_format(input)
156}
157
158#[must_use = "parsing result should be used"]
160pub fn llm_to_document(llm_input: &str) -> Result<DxDocument, ConvertError> {
161 Ok(LlmParser::parse(llm_input)?)
162}
163
164#[must_use = "parsing result should be used"]
166pub fn human_to_document(human_input: &str) -> Result<DxDocument, ConvertError> {
167 let parser = HumanParser::new();
168 Ok(parser.parse(human_input)?)
169}
170
171#[must_use]
173pub fn document_to_llm(doc: &DxDocument) -> String {
174 let serializer = LlmSerializer::new();
175 serializer.serialize(doc)
176}
177
178#[must_use]
180pub fn document_to_human(doc: &DxDocument) -> String {
181 let formatter = HumanFormatter::new();
182 formatter.format(doc)
183}
184
185pub fn document_to_human_with_config(doc: &DxDocument, config: HumanFormatConfig) -> String {
187 let formatter = HumanFormatter::with_config(config);
188 formatter.format(doc)
189}
190
191#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
193pub enum CompressionAlgorithm {
194 #[default]
196 Lz4,
197 Zstd,
199 None,
201}
202
203const MACHINE_ENVELOPE_MAGIC: &[u8; 4] = b"DXM1";
204const MACHINE_ENVELOPE_VERSION: u8 = 1;
205const MACHINE_ENVELOPE_HEADER_LEN: usize = 56;
206
207#[derive(Debug, Clone, Copy, PartialEq, Eq)]
208enum MachineEnvelopeCodec {
209 None,
210 Lz4,
211 Zstd,
212}
213
214impl MachineEnvelopeCodec {
215 const fn as_u8(self) -> u8 {
216 match self {
217 Self::None => 0,
218 Self::Lz4 => 1,
219 Self::Zstd => 2,
220 }
221 }
222
223 fn from_u8(value: u8) -> Result<Self, ConvertError> {
224 match value {
225 0 => Ok(Self::None),
226 1 => Ok(Self::Lz4),
227 2 => Ok(Self::Zstd),
228 _ => Err(ConvertError::MachineFormat {
229 msg: format!("Unsupported machine envelope codec: {}", value),
230 }),
231 }
232 }
233}
234
235struct MachineEnvelope<'a> {
236 codec: MachineEnvelopeCodec,
237 payload: &'a [u8],
238 uncompressed_len: usize,
239}
240
241#[derive(Debug, Clone)]
245pub struct MachineFormat {
246 pub data: Vec<u8>,
248 #[cfg(feature = "compression")]
250 cached: std::cell::RefCell<Option<Vec<u8>>>,
251}
252
253impl MachineFormat {
254 pub fn new(data: Vec<u8>) -> Self {
256 Self {
257 data,
258 #[cfg(feature = "compression")]
259 cached: std::cell::RefCell::new(None),
260 }
261 }
262
263 pub fn as_bytes(&self) -> &[u8] {
265 &self.data
266 }
267}
268
269pub fn llm_to_machine(llm_input: &str) -> Result<MachineFormat, ConvertError> {
271 let doc = LlmParser::parse(llm_input)?;
272 try_document_to_machine_with_compression(&doc, CompressionAlgorithm::default())
273}
274
275pub fn llm_to_machine_with_compression(
277 llm_input: &str,
278 compression: CompressionAlgorithm,
279) -> Result<MachineFormat, ConvertError> {
280 let doc = LlmParser::parse(llm_input)?;
281 try_document_to_machine_with_compression(&doc, compression)
282}
283
284pub fn human_to_machine(human_input: &str) -> Result<MachineFormat, ConvertError> {
286 let parser = HumanParser::new();
287 let doc = parser.parse(human_input)?;
288 try_document_to_machine_with_compression(&doc, CompressionAlgorithm::default())
289}
290
291pub fn human_to_machine_uncompressed(human_input: &str) -> Result<MachineFormat, ConvertError> {
293 let parser = HumanParser::new();
294 let doc = parser.parse(human_input)?;
295 try_document_to_machine_with_compression(&doc, CompressionAlgorithm::None)
296}
297
298pub fn human_to_machine_with_compression(
300 human_input: &str,
301 compression: CompressionAlgorithm,
302) -> Result<MachineFormat, ConvertError> {
303 let parser = HumanParser::new();
304 let doc = parser.parse(human_input)?;
305 try_document_to_machine_with_compression(&doc, compression)
306}
307
308pub fn document_to_machine(doc: &DxDocument) -> MachineFormat {
310 document_to_machine_with_compression(doc, CompressionAlgorithm::default())
311}
312
313pub fn document_to_machine_with_compression(
315 doc: &DxDocument,
316 compression: CompressionAlgorithm,
317) -> MachineFormat {
318 try_document_to_machine_with_compression(doc, compression)
319 .unwrap_or_else(|error| panic!("Machine serialization failed: {}", error))
320}
321
322pub fn try_document_to_machine_with_compression(
324 doc: &DxDocument,
325 compression: CompressionAlgorithm,
326) -> Result<MachineFormat, ConvertError> {
327 use crate::machine::machine_types::MachineDocument;
328 use crate::machine::serialize;
329
330 let machine_doc = MachineDocument::from(doc);
331 let rkyv_data = serialize(&machine_doc)
332 .map_err(|e| ConvertError::MachineFormat {
333 msg: format!("RKYV serialization failed: {}", e),
334 })?
335 .into_vec();
336
337 let (codec, payload): (MachineEnvelopeCodec, Cow<'_, [u8]>) = match compression {
338 CompressionAlgorithm::None => (MachineEnvelopeCodec::None, Cow::Borrowed(&rkyv_data)),
339
340 #[cfg(feature = "compression-lz4")]
341 CompressionAlgorithm::Lz4 => {
342 use crate::machine::compress::compress_lz4;
343 match compress_lz4(&rkyv_data) {
344 Ok(compressed) => {
345 let savings_ratio =
346 compression_savings_ratio(rkyv_data.len(), compressed.len());
347 if savings_ratio > 0.10 {
348 (MachineEnvelopeCodec::Lz4, Cow::Owned(compressed))
349 } else {
350 (MachineEnvelopeCodec::None, Cow::Borrowed(&rkyv_data))
351 }
352 }
353 Err(_) => (MachineEnvelopeCodec::None, Cow::Borrowed(&rkyv_data)),
354 }
355 }
356
357 #[cfg(feature = "compression-zstd")]
358 CompressionAlgorithm::Zstd => {
359 use crate::machine::compress::{CompressionLevel, compress_zstd_level};
360 match compress_zstd_level(&rkyv_data, CompressionLevel::Fast) {
361 Ok(compressed) => {
362 let savings_ratio =
363 compression_savings_ratio(rkyv_data.len(), compressed.len());
364 if savings_ratio > 0.10 {
365 (MachineEnvelopeCodec::Zstd, Cow::Owned(compressed))
366 } else {
367 (MachineEnvelopeCodec::None, Cow::Borrowed(&rkyv_data))
368 }
369 }
370 Err(_) => (MachineEnvelopeCodec::None, Cow::Borrowed(&rkyv_data)),
371 }
372 }
373
374 #[cfg(not(feature = "compression-lz4"))]
375 CompressionAlgorithm::Lz4 => (MachineEnvelopeCodec::None, Cow::Borrowed(&rkyv_data)),
376
377 #[cfg(not(feature = "compression-zstd"))]
378 CompressionAlgorithm::Zstd => (MachineEnvelopeCodec::None, Cow::Borrowed(&rkyv_data)),
379 };
380
381 Ok(MachineFormat::new(encode_machine_envelope(
382 codec,
383 payload.as_ref(),
384 rkyv_data.len(),
385 )))
386}
387
388pub fn machine_to_document(machine: &MachineFormat) -> Result<DxDocument, ConvertError> {
390 #[cfg(feature = "compression")]
391 {
392 if let Some(cached) = machine.cached.borrow().as_ref() {
394 return rkyv_bytes_to_document(cached);
395 }
396
397 let decompressed = decode_machine_bytes(&machine.data)?;
398 *machine.cached.borrow_mut() = Some(decompressed.to_vec());
399 return rkyv_bytes_to_document(decompressed.as_ref());
400 }
401
402 #[cfg(not(feature = "compression"))]
403 machine_bytes_to_document(&machine.data)
404}
405
406pub fn machine_bytes_to_document(data: &[u8]) -> Result<DxDocument, ConvertError> {
408 let doc_data = decode_machine_bytes(data)?;
409 rkyv_bytes_to_document(doc_data.as_ref())
410}
411
412#[cfg(feature = "mmap")]
414#[allow(unsafe_code)]
415pub fn machine_file_to_document_mmap(
416 path: impl AsRef<std::path::Path>,
417) -> Result<DxDocument, ConvertError> {
418 let file = std::fs::File::open(path.as_ref()).map_err(|error| ConvertError::MachineFormat {
419 msg: format!("Machine file open failed: {}", error),
420 })?;
421 let mmap = unsafe { memmap2::MmapOptions::new().map(&file) }.map_err(|error| {
423 ConvertError::MachineFormat {
424 msg: format!("Machine file mmap failed: {}", error),
425 }
426 })?;
427
428 machine_bytes_to_document(&mmap)
429}
430
431fn rkyv_bytes_to_document(doc_data: &[u8]) -> Result<DxDocument, ConvertError> {
432 use crate::machine::machine_types::MachineDocument;
433
434 let machine_doc: MachineDocument =
435 rkyv::from_bytes(doc_data).map_err(|e: rkyv::rancor::Error| {
436 ConvertError::MachineFormat {
437 msg: format!("RKYV deserialize failed: {}", e),
438 }
439 })?;
440
441 Ok(DxDocument::from(&machine_doc))
442}
443
444#[cfg(feature = "compression")]
445fn compression_savings_ratio(uncompressed_len: usize, compressed_len: usize) -> f64 {
446 if uncompressed_len == 0 {
447 return 0.0;
448 }
449
450 1.0 - (compressed_len as f64 / uncompressed_len as f64)
451}
452
453fn encode_machine_envelope(
454 codec: MachineEnvelopeCodec,
455 payload: &[u8],
456 uncompressed_len: usize,
457) -> Vec<u8> {
458 let payload_len = payload.len() as u64;
459 let uncompressed_len = uncompressed_len as u64;
460 let payload_hash = blake3::hash(payload);
461 let mut output = Vec::with_capacity(MACHINE_ENVELOPE_HEADER_LEN + payload.len());
462
463 output.extend_from_slice(MACHINE_ENVELOPE_MAGIC);
464 output.push(MACHINE_ENVELOPE_VERSION);
465 output.push(codec.as_u8());
466 output.extend_from_slice(&[0, 0]);
467 output.extend_from_slice(&payload_len.to_le_bytes());
468 output.extend_from_slice(&uncompressed_len.to_le_bytes());
469 output.extend_from_slice(payload_hash.as_bytes());
470 output.extend_from_slice(payload);
471
472 output
473}
474
475fn decode_machine_envelope(data: &[u8]) -> Result<Option<MachineEnvelope<'_>>, ConvertError> {
476 if !data.starts_with(MACHINE_ENVELOPE_MAGIC) {
477 return Ok(None);
478 }
479
480 if data.len() < MACHINE_ENVELOPE_HEADER_LEN {
481 return Err(ConvertError::MachineFormat {
482 msg: "Machine envelope header is truncated".to_string(),
483 });
484 }
485
486 if data[4] != MACHINE_ENVELOPE_VERSION {
487 return Err(ConvertError::MachineFormat {
488 msg: format!("Unsupported machine envelope version: {}", data[4]),
489 });
490 }
491
492 if data[6] != 0 || data[7] != 0 {
493 return Err(ConvertError::MachineFormat {
494 msg: "Machine envelope reserved bytes must be zero".to_string(),
495 });
496 }
497
498 let codec = MachineEnvelopeCodec::from_u8(data[5])?;
499 let payload_len = read_u64_le(&data[8..16])?;
500 let uncompressed_len = read_u64_le(&data[16..24])?;
501 let expected_len = MACHINE_ENVELOPE_HEADER_LEN
502 .checked_add(payload_len)
503 .ok_or_else(|| ConvertError::MachineFormat {
504 msg: "Machine envelope payload length overflow".to_string(),
505 })?;
506
507 if data.len() != expected_len {
508 return Err(ConvertError::MachineFormat {
509 msg: format!(
510 "Machine envelope length mismatch: expected {}, found {}",
511 expected_len,
512 data.len()
513 ),
514 });
515 }
516
517 let payload = &data[MACHINE_ENVELOPE_HEADER_LEN..];
518 let actual_hash = blake3::hash(payload);
519 if actual_hash.as_bytes() != &data[24..56] {
520 return Err(ConvertError::MachineFormat {
521 msg: "Machine envelope payload checksum mismatch".to_string(),
522 });
523 }
524
525 Ok(Some(MachineEnvelope {
526 codec,
527 payload,
528 uncompressed_len,
529 }))
530}
531
532fn read_u64_le(bytes: &[u8]) -> Result<usize, ConvertError> {
533 let value = u64::from_le_bytes(bytes.try_into().map_err(|_| ConvertError::MachineFormat {
534 msg: "Machine envelope integer field has invalid length".to_string(),
535 })?);
536
537 usize::try_from(value).map_err(|_| ConvertError::MachineFormat {
538 msg: "Machine envelope integer is too large for this platform".to_string(),
539 })
540}
541
542fn validate_uncompressed_len(data: &[u8], expected_len: usize) -> Result<(), ConvertError> {
543 if data.len() != expected_len {
544 return Err(ConvertError::MachineFormat {
545 msg: format!(
546 "Machine envelope uncompressed length mismatch: expected {}, found {}",
547 expected_len,
548 data.len()
549 ),
550 });
551 }
552
553 Ok(())
554}
555
556#[cfg(feature = "compression")]
557fn decode_machine_bytes(data: &[u8]) -> Result<Cow<'_, [u8]>, ConvertError> {
558 if let Some(envelope) = decode_machine_envelope(data)? {
559 let decoded = decode_machine_envelope_payload(&envelope)?;
560 validate_uncompressed_len(decoded.as_ref(), envelope.uncompressed_len)?;
561 return Ok(decoded);
562 }
563
564 decompress_auto(data)
565}
566
567#[cfg(not(feature = "compression"))]
568fn decode_machine_bytes(data: &[u8]) -> Result<Cow<'_, [u8]>, ConvertError> {
569 if let Some(envelope) = decode_machine_envelope(data)? {
570 if envelope.codec != MachineEnvelopeCodec::None {
571 return Err(ConvertError::MachineFormat {
572 msg: "Compressed machine envelope requires the compression feature".to_string(),
573 });
574 }
575 validate_uncompressed_len(envelope.payload, envelope.uncompressed_len)?;
576 return Ok(Cow::Borrowed(envelope.payload));
577 }
578
579 Ok(Cow::Borrowed(data))
580}
581
582#[cfg(feature = "compression")]
583fn decode_machine_envelope_payload<'a>(
584 envelope: &MachineEnvelope<'a>,
585) -> Result<Cow<'a, [u8]>, ConvertError> {
586 match envelope.codec {
587 MachineEnvelopeCodec::None => Ok(Cow::Borrowed(envelope.payload)),
588 MachineEnvelopeCodec::Lz4 => {
589 #[cfg(feature = "compression-lz4")]
590 {
591 use crate::machine::compress::decompress_lz4;
592 decompress_lz4(envelope.payload)
593 .map_err(|e| ConvertError::MachineFormat {
594 msg: format!("LZ4 machine envelope decompression failed: {}", e),
595 })
596 .map(Cow::Owned)
597 }
598
599 #[cfg(not(feature = "compression-lz4"))]
600 {
601 Err(ConvertError::MachineFormat {
602 msg: "LZ4 machine envelope requires the compression-lz4 feature".to_string(),
603 })
604 }
605 }
606 MachineEnvelopeCodec::Zstd => {
607 #[cfg(feature = "compression-zstd")]
608 {
609 use crate::machine::compress::decompress_zstd;
610 decompress_zstd(envelope.payload)
611 .map_err(|e| ConvertError::MachineFormat {
612 msg: format!("Zstd machine envelope decompression failed: {}", e),
613 })
614 .map(Cow::Owned)
615 }
616
617 #[cfg(not(feature = "compression-zstd"))]
618 {
619 Err(ConvertError::MachineFormat {
620 msg: "Zstd machine envelope requires the compression-zstd feature".to_string(),
621 })
622 }
623 }
624 }
625}
626
627#[cfg(feature = "compression")]
629fn decompress_auto(data: &[u8]) -> Result<Cow<'_, [u8]>, ConvertError> {
630 #[cfg(feature = "compression-lz4")]
632 {
633 use crate::machine::compress::decompress_lz4;
634 if let Ok(decompressed) = decompress_lz4(data) {
635 return Ok(Cow::Owned(decompressed));
636 }
637 }
638
639 #[cfg(feature = "compression-zstd")]
641 {
642 use crate::machine::compress::decompress_zstd;
643 if let Ok(decompressed) = decompress_zstd(data) {
644 return Ok(Cow::Owned(decompressed));
645 }
646 }
647
648 Ok(Cow::Borrowed(data))
650}
651
652pub fn machine_to_llm(machine: &MachineFormat) -> Result<String, ConvertError> {
654 let doc = machine_to_document(machine)?;
655 Ok(document_to_llm(&doc))
656}
657
658pub fn machine_to_human(machine: &MachineFormat) -> Result<String, ConvertError> {
660 let doc = machine_to_document(machine)?;
661 Ok(document_to_human(&doc))
662}
663
664#[cfg(test)]
665mod tests {
666 use super::*;
667 use crate::llm::types::DxLlmValue;
668
669 #[test]
670 fn test_llm_to_human() {
671 let llm = "name=Test\ncount=42";
672 let human = llm_to_human(llm).unwrap();
673 assert!(human.contains("name") || human.contains("Test"));
674 }
675
676 #[test]
677 fn test_human_to_llm() {
678 let human = r#"
679[config]
680 name = "Test"
681 count = 42
682"#;
683 let llm = human_to_llm(human).unwrap();
684 assert!(llm.contains(":") || llm.contains("Test"));
686 }
687
688 #[test]
689 fn try_document_to_machine_reports_machine_format_errors() {
690 let doc = DxDocument::new();
691 let machine =
692 try_document_to_machine_with_compression(&doc, CompressionAlgorithm::None).unwrap();
693 let round_trip_doc = machine_to_document(&machine).unwrap();
694
695 assert_eq!(round_trip_doc.entry_order.len(), 0);
696 }
697
698 #[test]
699 fn test_machine_format_round_trip() {
700 let mut doc = DxDocument::new();
701 doc.context
702 .insert("name".to_string(), DxLlmValue::Str("Test".to_string()));
703 doc.context
704 .insert("count".to_string(), DxLlmValue::Num(42.0));
705 doc.context
706 .insert("active".to_string(), DxLlmValue::Bool(true));
707
708 let machine = document_to_machine(&doc);
709 assert!(machine.as_bytes().starts_with(MACHINE_ENVELOPE_MAGIC));
710 let round_trip_doc = machine_to_document(&machine).unwrap();
711
712 assert_eq!(doc.context.len(), round_trip_doc.context.len());
713 assert_eq!(
714 round_trip_doc.context.get("name").unwrap().as_str(),
715 Some("Test")
716 );
717 assert_eq!(
718 round_trip_doc.context.get("count").unwrap().as_num(),
719 Some(42.0)
720 );
721 }
722
723 #[test]
724 fn test_is_dsr_format() {
725 assert!(is_dsr_format("name=Test"));
727 assert!(is_dsr_format("config[host=localhost,port=8080]"));
728 assert!(is_dsr_format("friends:3=ana,luis,sam"));
729 assert!(is_dsr_format("table:2(id,name)[1,John\n2,Jane]"));
730
731 assert!(!is_dsr_format("[config]\nname = Test"));
733 }
734
735 #[test]
736 fn test_machine_format_rejects_corrupt_envelope() {
737 let mut doc = DxDocument::new();
738 doc.context
739 .insert("name".to_string(), DxLlmValue::Str("Test".to_string()));
740
741 let mut machine = document_to_machine_with_compression(&doc, CompressionAlgorithm::None);
742 let last = machine.data.len() - 1;
743 machine.data[last] ^= 0xFF;
744
745 let error = machine_to_document(&machine).unwrap_err();
746 assert!(error.to_string().contains("checksum mismatch"));
747 }
748
749 #[test]
750 fn test_machine_format_rejects_invalid_envelope_headers() {
751 let mut doc = DxDocument::new();
752 doc.context
753 .insert("name".to_string(), DxLlmValue::Str("Test".to_string()));
754 let machine = document_to_machine_with_compression(&doc, CompressionAlgorithm::None);
755
756 let truncated = MachineFormat::new(machine.data[..8].to_vec());
757 assert!(
758 machine_to_document(&truncated)
759 .unwrap_err()
760 .to_string()
761 .contains("header is truncated")
762 );
763
764 let mut bad_version = machine.clone();
765 bad_version.data[4] = MACHINE_ENVELOPE_VERSION + 1;
766 assert!(
767 machine_to_document(&bad_version)
768 .unwrap_err()
769 .to_string()
770 .contains("Unsupported machine envelope version")
771 );
772
773 let mut bad_reserved = machine.clone();
774 bad_reserved.data[6] = 1;
775 assert!(
776 machine_to_document(&bad_reserved)
777 .unwrap_err()
778 .to_string()
779 .contains("reserved bytes must be zero")
780 );
781
782 let mut bad_codec = machine.clone();
783 bad_codec.data[5] = 255;
784 assert!(
785 machine_to_document(&bad_codec)
786 .unwrap_err()
787 .to_string()
788 .contains("Unsupported machine envelope codec")
789 );
790 }
791
792 #[test]
793 fn test_machine_format_rejects_invalid_envelope_lengths() {
794 let mut doc = DxDocument::new();
795 doc.context
796 .insert("name".to_string(), DxLlmValue::Str("Test".to_string()));
797 let machine = document_to_machine_with_compression(&doc, CompressionAlgorithm::None);
798
799 let mut bad_payload_len = machine.clone();
800 bad_payload_len.data[8..16].copy_from_slice(&1u64.to_le_bytes());
801 assert!(
802 machine_to_document(&bad_payload_len)
803 .unwrap_err()
804 .to_string()
805 .contains("length mismatch")
806 );
807
808 let mut bad_uncompressed_len = machine.clone();
809 bad_uncompressed_len.data[16..24].copy_from_slice(&1u64.to_le_bytes());
810 assert!(
811 machine_to_document(&bad_uncompressed_len)
812 .unwrap_err()
813 .to_string()
814 .contains("uncompressed length mismatch")
815 );
816 }
817
818 #[test]
819 fn test_machine_format_reads_legacy_raw_rkyv() {
820 use crate::machine::machine_types::MachineDocument;
821 use crate::machine::serialize;
822
823 let mut doc = DxDocument::new();
824 doc.context.insert(
825 "legacy".to_string(),
826 DxLlmValue::Str("raw-rkyv".to_string()),
827 );
828 let machine_doc = MachineDocument::from(&doc);
829 let legacy_data = serialize(&machine_doc).unwrap().into_vec();
830 let machine = MachineFormat::new(legacy_data);
831 let round_trip_doc = machine_to_document(&machine).unwrap();
832
833 assert_eq!(
834 round_trip_doc.context.get("legacy").unwrap().as_str(),
835 Some("raw-rkyv")
836 );
837 }
838
839 #[cfg(feature = "mmap")]
840 #[test]
841 fn test_machine_file_to_document_mmap_reads_uncompressed_envelope() {
842 let temp = tempfile::tempdir().unwrap();
843 let machine_path = temp.path().join("config.machine");
844 let mut doc = DxDocument::new();
845 doc.context
846 .insert("name".to_string(), DxLlmValue::Str("mmap".to_string()));
847 let machine = document_to_machine_with_compression(&doc, CompressionAlgorithm::None);
848 std::fs::write(&machine_path, machine.as_bytes()).unwrap();
849
850 let round_trip_doc = machine_file_to_document_mmap(&machine_path).unwrap();
851
852 assert_eq!(
853 round_trip_doc.context.get("name").unwrap().as_str(),
854 Some("mmap")
855 );
856 }
857}