1use std::collections::{HashMap, HashSet, VecDeque};
10use std::fmt;
11
12use serde::{Deserialize, Serialize};
13
14use crate::error::{Result, VaultError};
15use crate::formats::ModelFormat;
16
17#[derive(Debug, Clone)]
21pub struct ConversionProgress {
22 pub step: usize,
24 pub total_steps: usize,
26 pub bytes_processed: u64,
28 pub bytes_total: u64,
30 pub message: String,
32}
33
34impl fmt::Display for ConversionProgress {
35 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36 if self.bytes_total > 0 {
37 let pct = (self.bytes_processed as f64 / self.bytes_total as f64) * 100.0;
38 write!(
39 f,
40 "[{}/{}] {:.1}% — {}",
41 self.step + 1,
42 self.total_steps,
43 pct,
44 self.message,
45 )
46 } else {
47 write!(
48 f,
49 "[{}/{}] {}",
50 self.step + 1,
51 self.total_steps,
52 self.message,
53 )
54 }
55 }
56}
57
58pub type ProgressCallback = Box<dyn Fn(&ConversionProgress) + Send + Sync>;
60
61#[derive(Debug, Clone, Default, Serialize, Deserialize)]
65pub struct ConversionOptions {
66 pub quantization: Option<String>,
68 pub opset_version: Option<u32>,
70 pub preserve_metadata: bool,
72 pub validate: bool,
74 pub tolerance: f64,
76 pub extra: HashMap<String, String>,
78}
79
80impl ConversionOptions {
81 #[must_use]
83 pub fn with_validation() -> Self {
84 Self {
85 validate: true,
86 tolerance: 1e-5,
87 preserve_metadata: true,
88 ..Default::default()
89 }
90 }
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct ConversionResult {
98 #[serde(skip)]
102 pub data: Vec<u8>,
103 #[serde(default, skip_serializing_if = "Option::is_none")]
109 pub plan: Option<serde_json::Value>,
110 pub source_format: ModelFormat,
112 pub target_format: ModelFormat,
114 pub conversion_path: Vec<ModelFormat>,
116 pub input_size: u64,
118 pub output_size: u64,
120 pub validation: Option<ValidationReport>,
122}
123
124impl ConversionResult {
125 #[must_use]
131 pub fn is_plan(&self) -> bool {
132 self.plan.is_some()
133 }
134
135 #[must_use]
137 pub fn compression_ratio(&self) -> f64 {
138 if self.input_size == 0 {
139 return 0.0;
140 }
141 self.output_size as f64 / self.input_size as f64
142 }
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct ValidationReport {
150 pub passed: bool,
152 pub checks: Vec<ValidationCheck>,
154}
155
156impl ValidationReport {
157 #[must_use]
159 pub fn from_checks(checks: Vec<ValidationCheck>) -> Self {
160 let passed = checks.iter().all(|c| c.passed);
161 Self { passed, checks }
162 }
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct ValidationCheck {
168 pub name: String,
170 pub passed: bool,
172 pub message: String,
174}
175
176impl ValidationCheck {
177 #[must_use]
179 pub fn pass(name: impl Into<String>, message: impl Into<String>) -> Self {
180 Self {
181 name: name.into(),
182 passed: true,
183 message: message.into(),
184 }
185 }
186
187 #[must_use]
189 pub fn fail(name: impl Into<String>, message: impl Into<String>) -> Self {
190 Self {
191 name: name.into(),
192 passed: false,
193 message: message.into(),
194 }
195 }
196}
197
198pub trait Converter: Send + Sync {
204 fn name(&self) -> &str;
206
207 fn source_format(&self) -> ModelFormat;
209
210 fn target_format(&self) -> ModelFormat;
212
213 fn produces_plan(&self) -> bool {
221 false
222 }
223
224 fn convert(
226 &self,
227 data: &[u8],
228 options: &ConversionOptions,
229 progress: Option<&ProgressCallback>,
230 ) -> Result<Vec<u8>>;
231
232 fn validate(
234 &self,
235 input: &[u8],
236 output: &[u8],
237 options: &ConversionOptions,
238 ) -> ValidationReport {
239 let mut checks = vec![
240 if output.is_empty() {
242 ValidationCheck::fail("non_empty", "Output is empty")
243 } else {
244 ValidationCheck::pass("non_empty", format!("Output is {} bytes", output.len()))
245 },
246 {
248 let ratio = if input.is_empty() {
249 1.0
250 } else {
251 output.len() as f64 / input.len() as f64
252 };
253 if ratio > 100.0 && options.quantization.is_none() {
254 ValidationCheck::fail(
255 "size_ratio",
256 format!("Suspicious size ratio: {ratio:.1}×"),
257 )
258 } else {
259 ValidationCheck::pass("size_ratio", format!("Size ratio: {ratio:.2}×"))
260 }
261 },
262 ];
263
264 let magic_check = validate_magic_bytes(output, &self.target_format());
266 checks.push(magic_check);
267
268 ValidationReport::from_checks(checks)
269 }
270}
271
272pub struct ConversionPipeline {
276 converters: Vec<Box<dyn Converter>>,
277 edges: HashMap<(ModelFormat, ModelFormat), usize>,
279}
280
281impl ConversionPipeline {
282 #[must_use]
284 pub fn new() -> Self {
285 Self {
286 converters: Vec::new(),
287 edges: HashMap::new(),
288 }
289 }
290
291 #[must_use]
293 pub fn with_builtins() -> Self {
294 let mut p = Self::new();
295 p.register(Box::new(SafeTensorsToRawConverter));
296 p.register(Box::new(RawToSafeTensorsConverter));
297 p.register(Box::new(GgufHeaderParser));
298 p.register(Box::new(OnnxMetadataExtractor));
299 p.register(Box::new(SafeTensorsToPyTorchConverter));
300 p.register(Box::new(PyTorchToSafeTensorsConverter));
301 p.register(Box::new(PyTorchToOnnxConverter));
302 p.register(Box::new(OnnxToTensorRtConverter));
303 p.register(Box::new(OnnxToCoreMLConverter));
304 p.register(Box::new(SafeTensorsToGgufConverter));
305 p
306 }
307
308 pub fn register(&mut self, converter: Box<dyn Converter>) {
310 let key = (converter.source_format(), converter.target_format());
311 let idx = self.converters.len();
312 self.converters.push(converter);
313 self.edges.insert(key, idx);
314 }
315
316 #[must_use]
318 pub fn can_convert_direct(&self, from: &ModelFormat, to: &ModelFormat) -> bool {
319 self.edges.contains_key(&(from.clone(), to.clone()))
320 }
321
322 #[must_use]
324 pub fn find_path(&self, from: &ModelFormat, to: &ModelFormat) -> Option<Vec<ModelFormat>> {
325 if from == to {
326 return Some(vec![from.clone()]);
327 }
328
329 let mut visited: HashSet<ModelFormat> = HashSet::new();
331 let mut queue: VecDeque<Vec<ModelFormat>> = VecDeque::new();
332 queue.push_back(vec![from.clone()]);
333 visited.insert(from.clone());
334
335 while let Some(path) = queue.pop_front() {
336 let current = path.last().unwrap();
337
338 for (src, dst) in self.edges.keys() {
339 if src == current && !visited.contains(dst) {
340 let mut new_path = path.clone();
341 new_path.push(dst.clone());
342 if dst == to {
343 return Some(new_path);
344 }
345 visited.insert(dst.clone());
346 queue.push_back(new_path);
347 }
348 }
349 }
350
351 None
352 }
353
354 #[must_use]
356 pub fn supported_conversions(&self) -> Vec<(ModelFormat, ModelFormat, &str)> {
357 self.converters
358 .iter()
359 .map(|c| (c.source_format(), c.target_format(), c.name()))
360 .collect()
361 }
362
363 pub fn convert(
365 &self,
366 data: &[u8],
367 from: &ModelFormat,
368 to: &ModelFormat,
369 options: &ConversionOptions,
370 progress: Option<&ProgressCallback>,
371 ) -> Result<ConversionResult> {
372 if from == to {
373 return Ok(ConversionResult {
374 data: data.to_vec(),
375 plan: None,
376 source_format: from.clone(),
377 target_format: to.clone(),
378 conversion_path: vec![from.clone()],
379 input_size: data.len() as u64,
380 output_size: data.len() as u64,
381 validation: None,
382 });
383 }
384
385 let path = self.find_path(from, to).ok_or_else(|| {
386 VaultError::ConversionError(format!(
387 "No conversion path from {} to {}",
388 from.name(),
389 to.name(),
390 ))
391 })?;
392
393 let total_steps = path.len() - 1;
394 let mut current_data = data.to_vec();
395 let mut plan: Option<serde_json::Value> = None;
398
399 for (i, window) in path.windows(2).enumerate() {
400 let (src, dst) = (&window[0], &window[1]);
401 let idx = self.edges.get(&(src.clone(), dst.clone())).ok_or_else(|| {
402 VaultError::ConversionError(format!(
403 "Missing converter for {} -> {}",
404 src.name(),
405 dst.name(),
406 ))
407 })?;
408
409 let converter = &self.converters[*idx];
410
411 if let Some(cb) = progress {
413 cb(&ConversionProgress {
414 step: i,
415 total_steps,
416 bytes_processed: 0,
417 bytes_total: current_data.len() as u64,
418 message: format!("{} → {}", src.name(), dst.name()),
419 });
420 }
421
422 current_data = converter.convert(¤t_data, options, progress)?;
423
424 if converter.produces_plan() {
425 plan = Some(
426 serde_json::from_slice(¤t_data)
427 .unwrap_or_else(|_| serde_json::json!({ "converter": converter.name() })),
428 );
429 break;
432 }
433
434 if options.validate && i < total_steps - 1 {
436 let report = converter.validate(data, ¤t_data, options);
437 if !report.passed {
438 return Err(VaultError::ConversionError(format!(
439 "Intermediate validation failed at step {} ({}): {:?}",
440 i + 1,
441 converter.name(),
442 report
443 .checks
444 .iter()
445 .filter(|c| !c.passed)
446 .map(|c| &c.message)
447 .collect::<Vec<_>>(),
448 )));
449 }
450 }
451 }
452
453 if plan.is_some() {
456 return Ok(ConversionResult {
457 input_size: data.len() as u64,
458 output_size: 0,
459 data: Vec::new(),
460 plan,
461 source_format: from.clone(),
462 target_format: to.clone(),
463 conversion_path: path,
464 validation: None,
465 });
466 }
467
468 let validation = if options.validate {
470 let final_idx = self
471 .edges
472 .get(&(path[path.len() - 2].clone(), to.clone()))
473 .unwrap();
474 let report = self.converters[*final_idx].validate(data, ¤t_data, options);
475 Some(report)
476 } else {
477 None
478 };
479
480 Ok(ConversionResult {
481 input_size: data.len() as u64,
482 output_size: current_data.len() as u64,
483 data: current_data,
484 plan: None,
485 source_format: from.clone(),
486 target_format: to.clone(),
487 conversion_path: path,
488 validation,
489 })
490 }
491}
492
493impl Default for ConversionPipeline {
494 fn default() -> Self {
495 Self::with_builtins()
496 }
497}
498
499fn validate_magic_bytes(data: &[u8], format: &ModelFormat) -> ValidationCheck {
503 let (expected, label): (Option<&[u8]>, &str) = match format {
504 ModelFormat::GGUF => (Some(b"GGUF"), "GGUF magic"),
505 ModelFormat::ONNX => (Some(&[0x08]), "ONNX protobuf tag"), ModelFormat::Safetensors => {
507 if data.len() >= 8 {
509 let header_len = u64::from_le_bytes(data[..8].try_into().unwrap());
510 if header_len > 0 && header_len < data.len() as u64 {
511 return ValidationCheck::pass(
512 "magic_bytes",
513 format!("Valid SafeTensors header ({header_len} bytes)"),
514 );
515 }
516 return ValidationCheck::fail("magic_bytes", "Invalid SafeTensors header length");
517 }
518 return ValidationCheck::fail("magic_bytes", "Too small for SafeTensors header");
519 }
520 ModelFormat::PyTorch => {
521 if data.len() >= 2 && data[0] == b'P' && data[1] == b'K' {
523 return ValidationCheck::pass("magic_bytes", "Valid PyTorch ZIP archive");
524 }
525 if data.len() >= 2 && data[0] == 0x80 {
527 return ValidationCheck::pass("magic_bytes", "Valid PyTorch pickle format");
528 }
529 return ValidationCheck::fail("magic_bytes", "Unrecognised PyTorch header");
530 }
531 ModelFormat::TFLite => (Some(b"\x20\x00\x00\x00"), "TFLite FlatBuffer"),
532 _ => (None, ""),
533 };
534
535 match expected {
536 Some(magic) => {
537 if data.len() >= magic.len() && &data[..magic.len()] == magic {
538 ValidationCheck::pass("magic_bytes", format!("{label} OK"))
539 } else {
540 ValidationCheck::fail("magic_bytes", format!("{label} mismatch"))
541 }
542 }
543 None => ValidationCheck::pass(
544 "magic_bytes",
545 format!("No magic-byte check for {}", format.name()),
546 ),
547 }
548}
549
550pub struct SafeTensorsToRawConverter;
556
557impl Converter for SafeTensorsToRawConverter {
558 fn name(&self) -> &str {
559 "SafeTensors → Raw"
560 }
561 fn source_format(&self) -> ModelFormat {
562 ModelFormat::Safetensors
563 }
564 fn target_format(&self) -> ModelFormat {
565 ModelFormat::Custom("raw".into())
566 }
567
568 fn convert(
569 &self,
570 data: &[u8],
571 _options: &ConversionOptions,
572 progress: Option<&ProgressCallback>,
573 ) -> Result<Vec<u8>> {
574 if data.len() < 8 {
575 return Err(VaultError::ConversionError(
576 "Data too small for SafeTensors format".into(),
577 ));
578 }
579 let header_len = u64::from_le_bytes(data[..8].try_into().unwrap()) as usize;
580 const MAX_HEADER: usize = 100 * 1024 * 1024;
582 if header_len > MAX_HEADER {
583 return Err(VaultError::ConversionError(
584 "SafeTensors header too large".into(),
585 ));
586 }
587 if 8 + header_len > data.len() {
588 return Err(VaultError::ConversionError(
589 "SafeTensors header length exceeds data".into(),
590 ));
591 }
592 let raw = data[8 + header_len..].to_vec();
593
594 if let Some(cb) = progress {
595 cb(&ConversionProgress {
596 step: 0,
597 total_steps: 1,
598 bytes_processed: raw.len() as u64,
599 bytes_total: raw.len() as u64,
600 message: format!("Extracted {} bytes of raw tensor data", raw.len()),
601 });
602 }
603 Ok(raw)
604 }
605}
606
607pub struct RawToSafeTensorsConverter;
609
610impl Converter for RawToSafeTensorsConverter {
611 fn name(&self) -> &str {
612 "Raw → SafeTensors"
613 }
614 fn source_format(&self) -> ModelFormat {
615 ModelFormat::Custom("raw".into())
616 }
617 fn target_format(&self) -> ModelFormat {
618 ModelFormat::Safetensors
619 }
620
621 fn convert(
622 &self,
623 data: &[u8],
624 _options: &ConversionOptions,
625 progress: Option<&ProgressCallback>,
626 ) -> Result<Vec<u8>> {
627 let header = format!(
629 r#"{{"__metadata__":{{"format":"raw"}},"tensor_0":{{"dtype":"U8","shape":[{}],"data_offsets":[0,{}]}}}}"#,
630 data.len(),
631 data.len(),
632 );
633 let header_bytes = header.as_bytes();
634 let header_len = header_bytes.len() as u64;
635
636 let mut out = Vec::with_capacity(8 + header_bytes.len() + data.len());
637 out.extend_from_slice(&header_len.to_le_bytes());
638 out.extend_from_slice(header_bytes);
639 out.extend_from_slice(data);
640
641 if let Some(cb) = progress {
642 cb(&ConversionProgress {
643 step: 0,
644 total_steps: 1,
645 bytes_processed: out.len() as u64,
646 bytes_total: out.len() as u64,
647 message: format!(
648 "Packed {} bytes into SafeTensors ({} byte header)",
649 data.len(),
650 header_bytes.len()
651 ),
652 });
653 }
654 Ok(out)
655 }
656}
657
658pub struct GgufHeaderParser;
662
663impl Converter for GgufHeaderParser {
664 fn name(&self) -> &str {
665 "GGUF → Metadata (JSON)"
666 }
667 fn source_format(&self) -> ModelFormat {
668 ModelFormat::GGUF
669 }
670 fn target_format(&self) -> ModelFormat {
671 ModelFormat::Custom("gguf-meta".into())
672 }
673
674 fn convert(
675 &self,
676 data: &[u8],
677 _options: &ConversionOptions,
678 _progress: Option<&ProgressCallback>,
679 ) -> Result<Vec<u8>> {
680 if data.len() < 24 {
681 return Err(VaultError::ConversionError(
682 "Data too small for GGUF format".into(),
683 ));
684 }
685 if &data[..4] != b"GGUF" {
686 return Err(VaultError::ConversionError(
687 "Invalid GGUF magic bytes".into(),
688 ));
689 }
690 let version = u32::from_le_bytes(data[4..8].try_into().unwrap());
691 let tensor_count = u64::from_le_bytes(data[8..16].try_into().unwrap());
692 let kv_count = u64::from_le_bytes(data[16..24].try_into().unwrap());
693
694 let meta = serde_json::json!({
695 "format": "GGUF",
696 "version": version,
697 "tensor_count": tensor_count,
698 "kv_count": kv_count,
699 "file_size": data.len(),
700 });
701
702 serde_json::to_vec_pretty(&meta).map_err(|e| {
703 VaultError::ConversionError(format!("Failed to serialise GGUF metadata: {e}"))
704 })
705 }
706}
707
708pub struct OnnxMetadataExtractor;
712
713impl Converter for OnnxMetadataExtractor {
714 fn name(&self) -> &str {
715 "ONNX → Metadata (JSON)"
716 }
717 fn source_format(&self) -> ModelFormat {
718 ModelFormat::ONNX
719 }
720 fn target_format(&self) -> ModelFormat {
721 ModelFormat::Custom("onnx-meta".into())
722 }
723
724 fn convert(
725 &self,
726 data: &[u8],
727 _options: &ConversionOptions,
728 _progress: Option<&ProgressCallback>,
729 ) -> Result<Vec<u8>> {
730 let mut ir_version: u64 = 0;
733 let mut producer: String = String::new();
734 let mut model_version: u64 = 0;
735 let mut doc_string: String = String::new();
736
737 let mut pos = 0;
738 while pos < data.len() {
739 let (field_num, wire_type, new_pos) = match parse_protobuf_tag(data, pos) {
740 Some(v) => v,
741 None => break,
742 };
743 pos = new_pos;
744
745 match (field_num, wire_type) {
746 (1, 0) => {
748 let (val, np) = parse_varint(data, pos);
749 ir_version = val;
750 pos = np;
751 }
752 (2, 2) => {
754 let (bytes, np) = parse_length_delimited(data, pos)?;
755 producer = String::from_utf8_lossy(bytes).into_owned();
756 pos = np;
757 }
758 (5, 0) => {
760 let (val, np) = parse_varint(data, pos);
761 model_version = val;
762 pos = np;
763 }
764 (6, 2) => {
766 let (bytes, np) = parse_length_delimited(data, pos)?;
767 doc_string = String::from_utf8_lossy(bytes).into_owned();
768 pos = np;
769 }
770 (_, 0) => {
772 let (_, np) = parse_varint(data, pos);
773 pos = np;
774 }
775 (_, 2) => {
776 let (_, np) = parse_length_delimited(data, pos)?;
777 pos = np;
778 }
779 (_, 5) => pos += 4, (_, 1) => pos += 8, _ => break,
782 }
783 }
784
785 let meta = serde_json::json!({
786 "format": "ONNX",
787 "ir_version": ir_version,
788 "producer": producer,
789 "model_version": model_version,
790 "doc_string": doc_string,
791 "file_size": data.len(),
792 });
793
794 serde_json::to_vec_pretty(&meta).map_err(|e| {
795 VaultError::ConversionError(format!("Failed to serialise ONNX metadata: {e}"))
796 })
797 }
798}
799
800fn safetensors_dtype_to_pytorch(dtype: &str) -> Option<(&'static str, usize)> {
814 match dtype {
816 "F64" => Some(("DoubleStorage", 8)),
817 "F32" => Some(("FloatStorage", 4)),
818 "F16" => Some(("HalfStorage", 2)),
819 "BF16" => Some(("BFloat16Storage", 2)),
820 "I64" => Some(("LongStorage", 8)),
821 "I32" => Some(("IntStorage", 4)),
822 "I16" => Some(("ShortStorage", 2)),
823 "I8" => Some(("CharStorage", 1)),
824 "U8" => Some(("ByteStorage", 1)),
825 "BOOL" => Some(("BoolStorage", 1)),
826 _ => None,
827 }
828}
829
830fn pytorch_storage_to_safetensors_dtype(storage_type: &str) -> Option<(&'static str, usize)> {
832 match storage_type {
834 "DoubleStorage" => Some(("F64", 8)),
835 "FloatStorage" => Some(("F32", 4)),
836 "HalfStorage" => Some(("F16", 2)),
837 "BFloat16Storage" => Some(("BF16", 2)),
838 "LongStorage" => Some(("I64", 8)),
839 "IntStorage" => Some(("I32", 4)),
840 "ShortStorage" => Some(("I16", 2)),
841 "CharStorage" => Some(("I8", 1)),
842 "ByteStorage" | "UntypedStorage" => Some(("U8", 1)),
843 "BoolStorage" => Some(("BOOL", 1)),
844 _ => None,
845 }
846}
847
848pub struct SafeTensorsToPyTorchConverter;
854
855impl Converter for SafeTensorsToPyTorchConverter {
856 fn name(&self) -> &str {
857 "SafeTensors → PyTorch"
858 }
859 fn source_format(&self) -> ModelFormat {
860 ModelFormat::Safetensors
861 }
862 fn target_format(&self) -> ModelFormat {
863 ModelFormat::PyTorch
864 }
865
866 fn convert(
867 &self,
868 data: &[u8],
869 _options: &ConversionOptions,
870 progress: Option<&ProgressCallback>,
871 ) -> Result<Vec<u8>> {
872 if data.len() < 8 {
873 return Err(VaultError::ConversionError(
874 "Data too small for SafeTensors format".into(),
875 ));
876 }
877 let header_len = u64::from_le_bytes(data[..8].try_into().unwrap()) as usize;
878 const MAX_HEADER: usize = 100 * 1024 * 1024;
880 if header_len > MAX_HEADER {
881 return Err(VaultError::ConversionError(
882 "SafeTensors header too large".into(),
883 ));
884 }
885 if 8 + header_len > data.len() {
886 return Err(VaultError::ConversionError(
887 "SafeTensors header length exceeds data".into(),
888 ));
889 }
890
891 let header: serde_json::Map<String, serde_json::Value> =
892 serde_json::from_slice(&data[8..8 + header_len]).map_err(|e| {
893 VaultError::ConversionError(format!("Invalid SafeTensors header JSON: {e}"))
894 })?;
895 let tensor_data = &data[8 + header_len..];
896
897 let mut tensors: Vec<ConvTensorEntry> = Vec::new();
899 for (name, info) in &header {
900 if name == "__metadata__" {
901 continue;
902 }
903 let obj = info.as_object().ok_or_else(|| {
904 VaultError::ConversionError(format!("Tensor '{name}' is not an object"))
905 })?;
906 let dtype = obj
907 .get("dtype")
908 .and_then(|v| v.as_str())
909 .ok_or_else(|| {
910 VaultError::ConversionError(format!("Missing dtype for tensor '{name}'"))
911 })?
912 .to_string();
913 let shape: Vec<i64> = obj
914 .get("shape")
915 .and_then(|v| v.as_array())
916 .ok_or_else(|| {
917 VaultError::ConversionError(format!("Missing shape for tensor '{name}'"))
918 })?
919 .iter()
920 .filter_map(|v| v.as_i64())
921 .collect();
922 let offsets = obj
923 .get("data_offsets")
924 .and_then(|v| v.as_array())
925 .ok_or_else(|| {
926 VaultError::ConversionError(format!("Missing data_offsets for tensor '{name}'"))
927 })?;
928 let start = offsets[0].as_u64().unwrap_or(0) as usize;
929 let end = offsets[1].as_u64().unwrap_or(0) as usize;
930
931 tensors.push(ConvTensorEntry {
932 name: name.clone(),
933 dtype,
934 shape,
935 data_start: start,
936 data_end: end,
937 });
938 }
939
940 tensors.sort_by_key(|t| t.data_start);
942
943 if let Some(cb) = progress {
944 cb(&ConversionProgress {
945 step: 0,
946 total_steps: 1,
947 bytes_processed: 0,
948 bytes_total: data.len() as u64,
949 message: format!(
950 "Converting {} tensors from SafeTensors to PyTorch",
951 tensors.len()
952 ),
953 });
954 }
955
956 let mut zip_buf = std::io::Cursor::new(Vec::new());
958 {
959 let mut zip = zip::ZipWriter::new(&mut zip_buf);
960 let stored = zip::write::SimpleFileOptions::default()
961 .compression_method(zip::CompressionMethod::Stored);
962
963 for (i, tensor) in tensors.iter().enumerate() {
965 let entry_name = format!("archive/data/{i}");
966 let slice = tensor_data
967 .get(tensor.data_start..tensor.data_end)
968 .ok_or_else(|| {
969 VaultError::ConversionError(format!(
970 "Tensor '{}' data offsets [{},{}] out of bounds (data len {})",
971 tensor.name,
972 tensor.data_start,
973 tensor.data_end,
974 tensor_data.len(),
975 ))
976 })?;
977 zip.start_file(&entry_name, stored)
978 .map_err(|e| VaultError::ConversionError(format!("ZIP write error: {e}")))?;
979 std::io::Write::write_all(&mut zip, slice)
980 .map_err(|e| VaultError::ConversionError(format!("ZIP write error: {e}")))?;
981 }
982
983 let pickle = build_pytorch_pickle(&tensors)?;
985 zip.start_file("archive/data.pkl", stored)
986 .map_err(|e| VaultError::ConversionError(format!("ZIP write error: {e}")))?;
987 std::io::Write::write_all(&mut zip, &pickle)
988 .map_err(|e| VaultError::ConversionError(format!("ZIP write error: {e}")))?;
989
990 zip.finish()
991 .map_err(|e| VaultError::ConversionError(format!("ZIP finalize error: {e}")))?;
992 }
993
994 let output = zip_buf.into_inner();
995
996 if let Some(cb) = progress {
997 cb(&ConversionProgress {
998 step: 0,
999 total_steps: 1,
1000 bytes_processed: output.len() as u64,
1001 bytes_total: output.len() as u64,
1002 message: format!(
1003 "Created PyTorch archive: {} bytes ({} tensors)",
1004 output.len(),
1005 tensors.len(),
1006 ),
1007 });
1008 }
1009
1010 Ok(output)
1011 }
1012}
1013
1014struct ConvTensorEntry {
1016 name: String,
1017 dtype: String,
1018 shape: Vec<i64>,
1019 data_start: usize,
1020 data_end: usize,
1021}
1022
1023fn build_pytorch_pickle(tensors: &[ConvTensorEntry]) -> Result<Vec<u8>> {
1033 let mut pkl = Vec::with_capacity(4096);
1034
1035 pkl.push(0x80); pkl.push(2);
1038
1039 pkl.extend_from_slice(b"c");
1041 pkl.extend_from_slice(b"collections\nOrderedDict\n");
1042 pkl.push(b')'); pkl.push(b'R'); pkl.push(b'q'); pkl.push(0);
1046
1047 pkl.push(b'('); for (idx, tensor) in tensors.iter().enumerate() {
1051 let (storage_type, elem_size) =
1052 safetensors_dtype_to_pytorch(&tensor.dtype).ok_or_else(|| {
1053 VaultError::ConversionError(format!(
1054 "Unsupported dtype '{}' for tensor '{}'",
1055 tensor.dtype, tensor.name
1056 ))
1057 })?;
1058
1059 let data_len = tensor.data_end - tensor.data_start;
1060 let numel = data_len.checked_div(elem_size).unwrap_or(data_len);
1061
1062 write_short_binunicode(&mut pkl, &tensor.name);
1064
1065 pkl.extend_from_slice(b"c");
1067 pkl.extend_from_slice(b"torch._utils\n_rebuild_tensor_v2\n");
1068
1069 pkl.push(b'('); pkl.push(b'('); write_short_binunicode(&mut pkl, "storage");
1075 pkl.extend_from_slice(b"c");
1076 pkl.extend_from_slice(format!("torch\n{storage_type}\n").as_bytes());
1077 write_short_binunicode(&mut pkl, &idx.to_string());
1078 write_short_binunicode(&mut pkl, "cpu");
1079 write_pickle_int(&mut pkl, numel as i64);
1080 pkl.push(b't'); pkl.push(b'Q'); pkl.push(0x4b); pkl.push(0);
1086
1087 pkl.push(b'('); for &dim in &tensor.shape {
1090 write_pickle_int(&mut pkl, dim);
1091 }
1092 pkl.push(b't'); pkl.push(b'('); let mut stride = vec![1i64; tensor.shape.len()];
1097 for i in (0..tensor.shape.len().saturating_sub(1)).rev() {
1098 stride[i] = stride[i + 1] * tensor.shape[i + 1];
1099 }
1100 for s in &stride {
1101 write_pickle_int(&mut pkl, *s);
1102 }
1103 pkl.push(b't'); pkl.push(0x89); pkl.push(b't'); pkl.push(b'R'); let memo_idx = (idx + 1) as u8;
1114 if memo_idx < 255 {
1115 pkl.push(b'q'); pkl.push(memo_idx);
1117 }
1118 }
1119
1120 pkl.push(b'u'); pkl.push(b'.'); Ok(pkl)
1124}
1125
1126fn write_short_binunicode(pkl: &mut Vec<u8>, s: &str) {
1127 let bytes = s.as_bytes();
1128 if bytes.len() <= 255 {
1129 pkl.push(0x8c); pkl.push(bytes.len() as u8);
1131 pkl.extend_from_slice(bytes);
1132 } else {
1133 pkl.push(0x8d); pkl.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
1135 pkl.extend_from_slice(bytes);
1136 }
1137}
1138
1139fn write_pickle_int(pkl: &mut Vec<u8>, val: i64) {
1140 if (0..=255).contains(&val) {
1141 pkl.push(0x4b); pkl.push(val as u8);
1143 } else if (0..=65535).contains(&val) {
1144 pkl.push(0x4d); pkl.extend_from_slice(&(val as u16).to_le_bytes());
1146 } else if val >= i32::MIN as i64 && val <= i32::MAX as i64 {
1147 pkl.push(0x4a); pkl.extend_from_slice(&(val as i32).to_le_bytes());
1149 } else {
1150 pkl.push(0x8a); let bytes = val.to_le_bytes();
1153 let mut len = 8;
1155 if val >= 0 {
1156 while len > 1 && bytes[len - 1] == 0 {
1157 len -= 1;
1158 }
1159 if bytes[len - 1] & 0x80 != 0 {
1161 pkl.push((len + 1) as u8);
1162 pkl.extend_from_slice(&bytes[..len]);
1163 pkl.push(0);
1164 return;
1165 }
1166 } else {
1167 while len > 1 && bytes[len - 1] == 0xff {
1168 len -= 1;
1169 }
1170 if bytes[len - 1] & 0x80 == 0 {
1171 pkl.push((len + 1) as u8);
1172 pkl.extend_from_slice(&bytes[..len]);
1173 pkl.push(0xff);
1174 return;
1175 }
1176 }
1177 pkl.push(len as u8);
1178 pkl.extend_from_slice(&bytes[..len]);
1179 }
1180}
1181
1182pub struct PyTorchToSafeTensorsConverter;
1198
1199impl Converter for PyTorchToSafeTensorsConverter {
1200 fn name(&self) -> &str {
1201 "PyTorch → SafeTensors"
1202 }
1203 fn source_format(&self) -> ModelFormat {
1204 ModelFormat::PyTorch
1205 }
1206 fn target_format(&self) -> ModelFormat {
1207 ModelFormat::Safetensors
1208 }
1209
1210 fn convert(
1211 &self,
1212 data: &[u8],
1213 _options: &ConversionOptions,
1214 progress: Option<&ProgressCallback>,
1215 ) -> Result<Vec<u8>> {
1216 let cursor = std::io::Cursor::new(data);
1218 let mut archive = zip::ZipArchive::new(cursor).map_err(|e| {
1219 VaultError::ConversionError(format!("Not a valid PyTorch ZIP archive: {e}"))
1220 })?;
1221
1222 if let Some(cb) = progress {
1223 cb(&ConversionProgress {
1224 step: 0,
1225 total_steps: 1,
1226 bytes_processed: 0,
1227 bytes_total: data.len() as u64,
1228 message: format!("Parsing PyTorch archive ({} entries)", archive.len()),
1229 });
1230 }
1231
1232 let mut storage_files: Vec<(String, Vec<u8>)> = Vec::new();
1235 let mut pkl_data: Option<Vec<u8>> = None;
1236
1237 for i in 0..archive.len() {
1238 let mut file = archive
1239 .by_index(i)
1240 .map_err(|e| VaultError::ConversionError(format!("ZIP read error: {e}")))?;
1241 let name = file.name().to_string();
1242 let mut contents = Vec::new();
1243 std::io::Read::read_to_end(&mut file, &mut contents)
1244 .map_err(|e| VaultError::ConversionError(format!("ZIP read error: {e}")))?;
1245
1246 if name.ends_with(".pkl") || name.ends_with("/data.pkl") {
1247 pkl_data = Some(contents);
1248 } else if name.contains("/data/") {
1249 storage_files.push((name, contents));
1251 }
1252 }
1253
1254 storage_files.sort_by(|a, b| {
1256 let idx_a =
1257 a.0.rsplit('/')
1258 .next()
1259 .and_then(|s| s.parse::<usize>().ok())
1260 .unwrap_or(usize::MAX);
1261 let idx_b =
1262 b.0.rsplit('/')
1263 .next()
1264 .and_then(|s| s.parse::<usize>().ok())
1265 .unwrap_or(usize::MAX);
1266 idx_a.cmp(&idx_b)
1267 });
1268
1269 let tensor_infos = if let Some(ref pkl) = pkl_data {
1271 extract_tensor_info_from_pickle(pkl)
1272 } else {
1273 Vec::new()
1274 };
1275
1276 let mut header_entries = serde_json::Map::new();
1278 let mut all_tensor_data = Vec::new();
1279
1280 if !tensor_infos.is_empty() && tensor_infos.len() == storage_files.len() {
1281 for (info, (_path, storage_data)) in tensor_infos.iter().zip(storage_files.iter()) {
1283 let offset_start = all_tensor_data.len();
1284 all_tensor_data.extend_from_slice(storage_data);
1285 let offset_end = all_tensor_data.len();
1286
1287 let entry = serde_json::json!({
1288 "dtype": info.dtype,
1289 "shape": info.shape,
1290 "data_offsets": [offset_start, offset_end],
1291 });
1292 header_entries.insert(info.name.clone(), entry);
1293 }
1294 } else {
1295 for (path, storage_data) in &storage_files {
1297 let tensor_name = path.rsplit('/').next().unwrap_or("tensor");
1298 let offset_start = all_tensor_data.len();
1299 all_tensor_data.extend_from_slice(storage_data);
1300 let offset_end = all_tensor_data.len();
1301
1302 let entry = serde_json::json!({
1303 "dtype": "U8",
1304 "shape": [storage_data.len()],
1305 "data_offsets": [offset_start, offset_end],
1306 });
1307 header_entries.insert(format!("storage_{tensor_name}"), entry);
1308 }
1309 }
1310
1311 let header_json = serde_json::to_string(&header_entries).map_err(|e| {
1313 VaultError::ConversionError(format!("Failed to serialize SafeTensors header: {e}"))
1314 })?;
1315 let header_bytes = header_json.as_bytes();
1316 let header_len = header_bytes.len() as u64;
1317
1318 let mut output = Vec::with_capacity(8 + header_bytes.len() + all_tensor_data.len());
1319 output.extend_from_slice(&header_len.to_le_bytes());
1320 output.extend_from_slice(header_bytes);
1321 output.extend_from_slice(&all_tensor_data);
1322
1323 if let Some(cb) = progress {
1324 cb(&ConversionProgress {
1325 step: 0,
1326 total_steps: 1,
1327 bytes_processed: output.len() as u64,
1328 bytes_total: output.len() as u64,
1329 message: format!(
1330 "Created SafeTensors: {} bytes ({} tensors)",
1331 output.len(),
1332 header_entries.len(),
1333 ),
1334 });
1335 }
1336
1337 Ok(output)
1338 }
1339}
1340
1341struct PickleTensorInfo {
1343 name: String,
1344 dtype: String,
1345 shape: Vec<i64>,
1346}
1347
1348fn extract_tensor_info_from_pickle(pkl: &[u8]) -> Vec<PickleTensorInfo> {
1354 let mut tensors = Vec::new();
1355 let mut strings: Vec<String> = Vec::new();
1356 let mut pos = 0;
1357
1358 while pos < pkl.len() {
1360 match pkl[pos] {
1361 0x80 => pos += 2, b'c' => {
1363 pos += 1;
1365 while pos < pkl.len() && pkl[pos] != b'\n' {
1367 pos += 1;
1368 }
1369 pos += 1; let start = pos;
1372 while pos < pkl.len() && pkl[pos] != b'\n' {
1373 pos += 1;
1374 }
1375 if let Ok(s) = std::str::from_utf8(&pkl[start..pos]) {
1376 if !s.is_empty() {
1377 strings.push(s.to_string());
1378 }
1379 }
1380 pos += 1; }
1382 0x8c => {
1383 pos += 1;
1385 if pos >= pkl.len() {
1386 break;
1387 }
1388 let len = pkl[pos] as usize;
1389 pos += 1;
1390 if pos + len <= pkl.len() {
1391 if let Ok(s) = std::str::from_utf8(&pkl[pos..pos + len]) {
1392 strings.push(s.to_string());
1393 }
1394 pos += len;
1395 } else {
1396 break;
1397 }
1398 }
1399 0x8d => {
1400 pos += 1;
1402 if pos + 4 > pkl.len() {
1403 break;
1404 }
1405 let len = u32::from_le_bytes(pkl[pos..pos + 4].try_into().unwrap()) as usize;
1406 pos += 4;
1407 if pos + len <= pkl.len() {
1408 if let Ok(s) = std::str::from_utf8(&pkl[pos..pos + len]) {
1409 strings.push(s.to_string());
1410 }
1411 pos += len;
1412 } else {
1413 break;
1414 }
1415 }
1416 _ => pos += 1,
1417 }
1418 }
1419
1420 let mut i = 0;
1427 while i < strings.len() {
1428 if let Some((dtype, _elem_size)) = pytorch_storage_to_safetensors_dtype(&strings[i]) {
1430 let mut tensor_name = None;
1433 for j in (0..i).rev() {
1434 let candidate = &strings[j];
1435 if candidate == "storage"
1436 || candidate == "cpu"
1437 || candidate == "cuda"
1438 || candidate.contains("torch")
1439 || candidate.contains("collections")
1440 || candidate.contains("OrderedDict")
1441 || candidate.starts_with("_rebuild")
1442 || pytorch_storage_to_safetensors_dtype(candidate).is_some()
1443 || candidate.parse::<usize>().is_ok()
1444 {
1445 continue;
1446 }
1447 tensor_name = Some(candidate.clone());
1449 break;
1450 }
1451
1452 if let Some(name) = tensor_name {
1453 tensors.push(PickleTensorInfo {
1454 name,
1455 dtype: dtype.to_string(),
1456 shape: Vec::new(), });
1458 }
1459 }
1460 i += 1;
1461 }
1462
1463 tensors
1467}
1468
1469pub struct PyTorchToOnnxConverter;
1471
1472impl Converter for PyTorchToOnnxConverter {
1473 fn name(&self) -> &str {
1474 "PyTorch → ONNX (shim)"
1475 }
1476
1477 fn produces_plan(&self) -> bool {
1478 true
1479 }
1480 fn source_format(&self) -> ModelFormat {
1481 ModelFormat::PyTorch
1482 }
1483 fn target_format(&self) -> ModelFormat {
1484 ModelFormat::ONNX
1485 }
1486
1487 fn convert(
1488 &self,
1489 _data: &[u8],
1490 options: &ConversionOptions,
1491 _progress: Option<&ProgressCallback>,
1492 ) -> Result<Vec<u8>> {
1493 let opset = options.opset_version.unwrap_or(17);
1494 let plan = serde_json::json!({
1495 "converter": "pytorch_to_onnx",
1496 "requires": ["torch", "onnx"],
1497 "opset_version": opset,
1498 "python": format!(
1499 concat!(
1500 "import torch, onnx\n",
1501 "model = torch.load(input_path, map_location='cpu', weights_only=False)\n",
1502 "model.eval()\n",
1503 "dummy = torch.randn(1, 3, 224, 224) # adjust shape as needed\n",
1504 "torch.onnx.export(model, dummy, output_path, opset_version={})\n",
1505 ),
1506 opset,
1507 ),
1508 });
1509
1510 serde_json::to_vec_pretty(&plan).map_err(|e| {
1511 VaultError::ConversionError(format!("Failed to create conversion plan: {e}"))
1512 })
1513 }
1514}
1515
1516pub struct OnnxToTensorRtConverter;
1518
1519impl Converter for OnnxToTensorRtConverter {
1520 fn name(&self) -> &str {
1521 "ONNX → TensorRT (shim)"
1522 }
1523
1524 fn produces_plan(&self) -> bool {
1525 true
1526 }
1527 fn source_format(&self) -> ModelFormat {
1528 ModelFormat::ONNX
1529 }
1530 fn target_format(&self) -> ModelFormat {
1531 ModelFormat::TensorRT
1532 }
1533
1534 fn convert(
1535 &self,
1536 _data: &[u8],
1537 _options: &ConversionOptions,
1538 _progress: Option<&ProgressCallback>,
1539 ) -> Result<Vec<u8>> {
1540 let plan = serde_json::json!({
1541 "converter": "onnx_to_tensorrt",
1542 "requires": ["tensorrt"],
1543 "shell": "trtexec --onnx=input_path --saveEngine=output_path",
1544 "python": concat!(
1545 "import tensorrt as trt\n",
1546 "logger = trt.Logger(trt.Logger.WARNING)\n",
1547 "builder = trt.Builder(logger)\n",
1548 "network = builder.create_network(1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH))\n",
1549 "parser = trt.OnnxParser(network, logger)\n",
1550 "with open(input_path, 'rb') as f:\n",
1551 " parser.parse(f.read())\n",
1552 "config = builder.create_builder_config()\n",
1553 "engine = builder.build_serialized_network(network, config)\n",
1554 "with open(output_path, 'wb') as f:\n",
1555 " f.write(engine)\n",
1556 ),
1557 });
1558
1559 serde_json::to_vec_pretty(&plan).map_err(|e| {
1560 VaultError::ConversionError(format!("Failed to create conversion plan: {e}"))
1561 })
1562 }
1563}
1564
1565pub struct OnnxToCoreMLConverter;
1567
1568impl Converter for OnnxToCoreMLConverter {
1569 fn name(&self) -> &str {
1570 "ONNX → Core ML (shim)"
1571 }
1572
1573 fn produces_plan(&self) -> bool {
1574 true
1575 }
1576 fn source_format(&self) -> ModelFormat {
1577 ModelFormat::ONNX
1578 }
1579 fn target_format(&self) -> ModelFormat {
1580 ModelFormat::CoreML
1581 }
1582
1583 fn convert(
1584 &self,
1585 _data: &[u8],
1586 _options: &ConversionOptions,
1587 _progress: Option<&ProgressCallback>,
1588 ) -> Result<Vec<u8>> {
1589 let plan = serde_json::json!({
1590 "converter": "onnx_to_coreml",
1591 "requires": ["coremltools", "onnx"],
1592 "python": concat!(
1593 "import coremltools as ct\n",
1594 "import onnx\n",
1595 "model = onnx.load(input_path)\n",
1596 "ml_model = ct.convert(model)\n",
1597 "ml_model.save(output_path)\n",
1598 ),
1599 });
1600
1601 serde_json::to_vec_pretty(&plan).map_err(|e| {
1602 VaultError::ConversionError(format!("Failed to create conversion plan: {e}"))
1603 })
1604 }
1605}
1606
1607pub struct SafeTensorsToGgufConverter;
1609
1610impl Converter for SafeTensorsToGgufConverter {
1611 fn name(&self) -> &str {
1612 "SafeTensors → GGUF (shim)"
1613 }
1614
1615 fn produces_plan(&self) -> bool {
1616 true
1617 }
1618 fn source_format(&self) -> ModelFormat {
1619 ModelFormat::Safetensors
1620 }
1621 fn target_format(&self) -> ModelFormat {
1622 ModelFormat::GGUF
1623 }
1624
1625 fn convert(
1626 &self,
1627 _data: &[u8],
1628 options: &ConversionOptions,
1629 _progress: Option<&ProgressCallback>,
1630 ) -> Result<Vec<u8>> {
1631 let quant = options.quantization.as_deref().unwrap_or("f16");
1632
1633 let plan = serde_json::json!({
1634 "converter": "safetensors_to_gguf",
1635 "requires": ["gguf", "numpy", "safetensors"],
1636 "quantization": quant,
1637 "shell": format!(
1638 "python -m gguf.convert --src input_path --dst output_path --type {quant}"
1639 ),
1640 "python": format!(
1641 concat!(
1642 "# Use llama.cpp convert scripts\n",
1643 "# python convert_hf_to_gguf.py --outtype {} model_dir\n",
1644 ),
1645 quant,
1646 ),
1647 });
1648
1649 serde_json::to_vec_pretty(&plan).map_err(|e| {
1650 VaultError::ConversionError(format!("Failed to create conversion plan: {e}"))
1651 })
1652 }
1653}
1654
1655fn parse_varint(data: &[u8], start: usize) -> (u64, usize) {
1658 let mut result: u64 = 0;
1659 let mut shift = 0u32;
1660 let mut pos = start;
1661 while pos < data.len() {
1662 let byte = data[pos];
1663 result |= ((byte & 0x7F) as u64) << shift;
1664 pos += 1;
1665 if byte & 0x80 == 0 {
1666 break;
1667 }
1668 shift += 7;
1669 if shift >= 64 {
1670 break;
1671 }
1672 }
1673 (result, pos)
1674}
1675
1676fn parse_protobuf_tag(data: &[u8], pos: usize) -> Option<(u64, u8, usize)> {
1677 if pos >= data.len() {
1678 return None;
1679 }
1680 let (tag, new_pos) = parse_varint(data, pos);
1681 let field_num = tag >> 3;
1682 let wire_type = (tag & 0x07) as u8;
1683 Some((field_num, wire_type, new_pos))
1684}
1685
1686fn parse_length_delimited(data: &[u8], pos: usize) -> Result<(&[u8], usize)> {
1687 let (len, new_pos) = parse_varint(data, pos);
1688 let len = len as usize;
1689 if new_pos + len > data.len() {
1690 return Err(VaultError::ConversionError(
1691 "Protobuf length-delimited field exceeds data".into(),
1692 ));
1693 }
1694 Ok((&data[new_pos..new_pos + len], new_pos + len))
1695}
1696
1697#[cfg(test)]
1700mod tests {
1701 use super::*;
1702
1703 #[test]
1704 fn test_pipeline_same_format_passthrough() {
1705 let pipeline = ConversionPipeline::with_builtins();
1706 let data = b"hello world";
1707 let result = pipeline
1708 .convert(
1709 data,
1710 &ModelFormat::PyTorch,
1711 &ModelFormat::PyTorch,
1712 &ConversionOptions::default(),
1713 None,
1714 )
1715 .unwrap();
1716 assert_eq!(result.data, data);
1717 assert_eq!(result.conversion_path, vec![ModelFormat::PyTorch]);
1718 }
1719
1720 #[test]
1721 fn test_pipeline_no_path_error() {
1722 let pipeline = ConversionPipeline::new(); let err = pipeline
1724 .convert(
1725 b"data",
1726 &ModelFormat::PyTorch,
1727 &ModelFormat::ONNX,
1728 &ConversionOptions::default(),
1729 None,
1730 )
1731 .unwrap_err();
1732 let msg = format!("{err}");
1733 assert!(msg.contains("No conversion path"));
1734 }
1735
1736 #[test]
1737 fn test_safetensors_roundtrip() {
1738 let original = b"test tensor data 1234567890";
1739 let pipeline = ConversionPipeline::with_builtins();
1740
1741 let st = RawToSafeTensorsConverter
1743 .convert(original, &ConversionOptions::default(), None)
1744 .unwrap();
1745 assert!(st.len() > original.len());
1746
1747 let raw = SafeTensorsToRawConverter
1749 .convert(&st, &ConversionOptions::default(), None)
1750 .unwrap();
1751 assert_eq!(raw, original);
1752
1753 assert!(pipeline.can_convert_direct(
1755 &ModelFormat::Custom("raw".into()),
1756 &ModelFormat::Safetensors,
1757 ));
1758 }
1759
1760 #[test]
1761 fn test_safetensors_to_raw_too_small() {
1762 let err = SafeTensorsToRawConverter
1763 .convert(b"tiny", &ConversionOptions::default(), None)
1764 .unwrap_err();
1765 assert!(format!("{err}").contains("too small"));
1766 }
1767
1768 #[test]
1769 fn test_gguf_header_parser_valid() {
1770 let mut data = Vec::new();
1772 data.extend_from_slice(b"GGUF"); data.extend_from_slice(&3u32.to_le_bytes()); data.extend_from_slice(&42u64.to_le_bytes()); data.extend_from_slice(&7u64.to_le_bytes()); data.extend_from_slice(&[0u8; 100]);
1778
1779 let result = GgufHeaderParser
1780 .convert(&data, &ConversionOptions::default(), None)
1781 .unwrap();
1782 let meta: serde_json::Value = serde_json::from_slice(&result).unwrap();
1783 assert_eq!(meta["version"], 3);
1784 assert_eq!(meta["tensor_count"], 42);
1785 assert_eq!(meta["kv_count"], 7);
1786 }
1787
1788 #[test]
1789 fn test_gguf_header_parser_invalid_magic() {
1790 let err = GgufHeaderParser
1791 .convert(
1792 b"NOT_GGUF_DATA_HERE__________",
1793 &ConversionOptions::default(),
1794 None,
1795 )
1796 .unwrap_err();
1797 assert!(format!("{err}").contains("Invalid GGUF magic"));
1798 }
1799
1800 #[test]
1801 fn test_validation_report() {
1802 let checks = vec![
1803 ValidationCheck::pass("a", "ok"),
1804 ValidationCheck::fail("b", "bad"),
1805 ];
1806 let report = ValidationReport::from_checks(checks);
1807 assert!(!report.passed);
1808 assert_eq!(report.checks.len(), 2);
1809 }
1810
1811 #[test]
1812 fn test_conversion_options_with_validation() {
1813 let opts = ConversionOptions::with_validation();
1814 assert!(opts.validate);
1815 assert!(opts.preserve_metadata);
1816 assert!((opts.tolerance - 1e-5).abs() < f64::EPSILON);
1817 }
1818
1819 #[test]
1820 fn test_conversion_result_compression_ratio() {
1821 let result = ConversionResult {
1822 data: vec![],
1823 plan: None,
1824 source_format: ModelFormat::PyTorch,
1825 target_format: ModelFormat::Safetensors,
1826 conversion_path: vec![],
1827 input_size: 1000,
1828 output_size: 500,
1829 validation: None,
1830 };
1831 assert!((result.compression_ratio() - 0.5).abs() < f64::EPSILON);
1832 }
1833
1834 #[test]
1835 fn test_find_path_direct() {
1836 let pipeline = ConversionPipeline::with_builtins();
1837 let path = pipeline
1838 .find_path(&ModelFormat::Safetensors, &ModelFormat::PyTorch)
1839 .unwrap();
1840 assert_eq!(path, vec![ModelFormat::Safetensors, ModelFormat::PyTorch]);
1841 }
1842
1843 #[test]
1844 fn test_find_path_multi_step() {
1845 let pipeline = ConversionPipeline::with_builtins();
1846 let path = pipeline
1848 .find_path(&ModelFormat::PyTorch, &ModelFormat::TensorRT)
1849 .unwrap();
1850 assert_eq!(
1851 path,
1852 vec![
1853 ModelFormat::PyTorch,
1854 ModelFormat::ONNX,
1855 ModelFormat::TensorRT
1856 ]
1857 );
1858 }
1859
1860 #[test]
1861 fn test_find_path_none() {
1862 let pipeline = ConversionPipeline::with_builtins();
1863 let path = pipeline.find_path(&ModelFormat::PyTorch, &ModelFormat::MXNet);
1865 assert!(path.is_none());
1866 }
1867
1868 #[test]
1869 fn test_supported_conversions_list() {
1870 let pipeline = ConversionPipeline::with_builtins();
1871 let conversions = pipeline.supported_conversions();
1872 assert!(conversions.len() >= 10);
1873 }
1874
1875 #[test]
1876 fn test_progress_display() {
1877 let p = ConversionProgress {
1878 step: 0,
1879 total_steps: 3,
1880 bytes_processed: 500,
1881 bytes_total: 1000,
1882 message: "Converting".into(),
1883 };
1884 let s = format!("{p}");
1885 assert!(s.contains("50.0%"));
1886 assert!(s.contains("[1/3]"));
1887 }
1888
1889 #[test]
1890 fn test_progress_display_unknown_total() {
1891 let p = ConversionProgress {
1892 step: 1,
1893 total_steps: 2,
1894 bytes_processed: 100,
1895 bytes_total: 0,
1896 message: "Working".into(),
1897 };
1898 let s = format!("{p}");
1899 assert!(s.contains("[2/2]"));
1900 assert!(s.contains("Working"));
1901 assert!(!s.contains('%'));
1902 }
1903
1904 #[test]
1905 fn test_shim_converter_produces_plan() {
1906 let converter = PyTorchToOnnxConverter;
1907 let plan_bytes = converter
1908 .convert(b"", &ConversionOptions::default(), None)
1909 .unwrap();
1910 let plan: serde_json::Value = serde_json::from_slice(&plan_bytes).unwrap();
1911 assert_eq!(plan["converter"], "pytorch_to_onnx");
1912 assert!(plan["requires"]
1913 .as_array()
1914 .unwrap()
1915 .contains(&"torch".into()));
1916 assert_eq!(plan["opset_version"], 17);
1917 }
1918
1919 #[test]
1920 fn test_shim_converter_custom_opset() {
1921 let opts = ConversionOptions {
1922 opset_version: Some(13),
1923 ..ConversionOptions::default()
1924 };
1925 let plan_bytes = PyTorchToOnnxConverter.convert(b"", &opts, None).unwrap();
1926 let plan: serde_json::Value = serde_json::from_slice(&plan_bytes).unwrap();
1927 assert_eq!(plan["opset_version"], 13);
1928 }
1929
1930 #[test]
1931 fn test_safetensors_to_gguf_quantization() {
1932 let opts = ConversionOptions {
1933 quantization: Some("q4_k_m".into()),
1934 ..ConversionOptions::default()
1935 };
1936 let plan_bytes = SafeTensorsToGgufConverter
1937 .convert(b"", &opts, None)
1938 .unwrap();
1939 let plan: serde_json::Value = serde_json::from_slice(&plan_bytes).unwrap();
1940 assert_eq!(plan["quantization"], "q4_k_m");
1941 }
1942
1943 #[test]
1944 fn test_validate_magic_bytes_safetensors() {
1945 let header = b"{}";
1947 let mut data = Vec::new();
1948 data.extend_from_slice(&(header.len() as u64).to_le_bytes());
1949 data.extend_from_slice(header);
1950 let check = validate_magic_bytes(&data, &ModelFormat::Safetensors);
1951 assert!(check.passed);
1952 }
1953
1954 #[test]
1955 fn test_validate_magic_bytes_gguf() {
1956 let mut data = b"GGUF".to_vec();
1957 data.extend_from_slice(&[0u8; 20]);
1958 let check = validate_magic_bytes(&data, &ModelFormat::GGUF);
1959 assert!(check.passed);
1960 }
1961
1962 #[test]
1963 fn test_validate_magic_bytes_pytorch_zip() {
1964 let data = b"PK\x03\x04...";
1965 let check = validate_magic_bytes(data, &ModelFormat::PyTorch);
1966 assert!(check.passed);
1967 }
1968
1969 #[test]
1970 fn test_validate_magic_bytes_unknown_format() {
1971 let check = validate_magic_bytes(b"anything", &ModelFormat::Keras);
1972 assert!(check.passed); }
1974
1975 #[test]
1976 fn test_pipeline_with_progress_callback() {
1977 let pipeline = ConversionPipeline::with_builtins();
1978 let progress_log = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
1979 let log_clone = progress_log.clone();
1980 let cb: ProgressCallback = Box::new(move |p| {
1981 log_clone.lock().unwrap().push(p.message.clone());
1982 });
1983
1984 let _ = pipeline.convert(
1985 b"",
1986 &ModelFormat::PyTorch,
1987 &ModelFormat::ONNX,
1988 &ConversionOptions::default(),
1989 Some(&cb),
1990 );
1991
1992 let log = progress_log.lock().unwrap();
1993 assert!(!log.is_empty());
1994 }
1995
1996 #[test]
1999 fn test_validate_empty_output() {
2000 let converter = PyTorchToOnnxConverter;
2002 let report = converter.validate(b"input", b"", &ConversionOptions::default());
2003 assert!(!report.passed);
2004 assert!(report
2005 .checks
2006 .iter()
2007 .any(|c| c.name == "non_empty" && !c.passed));
2008 }
2009
2010 #[test]
2011 fn test_validate_suspicious_ratio() {
2012 let converter = PyTorchToOnnxConverter;
2014 let input = b"x";
2015 let output = vec![0u8; 200];
2016 let report = converter.validate(input, &output, &ConversionOptions::default());
2017 assert!(!report.passed);
2018 assert!(report
2019 .checks
2020 .iter()
2021 .any(|c| c.name == "size_ratio" && !c.passed));
2022 }
2023
2024 #[test]
2025 fn test_validate_ratio_with_quantization_ok() {
2026 let converter = PyTorchToOnnxConverter;
2027 let input = b"x";
2028 let output = vec![0u8; 200];
2029 let opts = ConversionOptions {
2030 quantization: Some("q4_k_m".into()),
2031 ..ConversionOptions::default()
2032 };
2033 let report = converter.validate(input, &output, &opts);
2034 assert!(report
2036 .checks
2037 .iter()
2038 .any(|c| c.name == "size_ratio" && c.passed));
2039 }
2040
2041 #[test]
2042 fn test_validate_empty_input_ratio() {
2043 let converter = PyTorchToOnnxConverter;
2045 let output = vec![0u8; 10];
2046 let report = converter.validate(b"", &output, &ConversionOptions::default());
2047 assert!(report
2048 .checks
2049 .iter()
2050 .any(|c| c.name == "size_ratio" && c.passed));
2051 }
2052
2053 #[test]
2056 fn test_safetensors_pytorch_roundtrip() {
2057 let header = serde_json::json!({
2059 "weight": { "dtype": "F32", "shape": [2, 2], "data_offsets": [0, 16] }
2060 });
2061 let header_bytes = serde_json::to_vec(&header).unwrap();
2062 let mut st_data = Vec::new();
2063 st_data.extend_from_slice(&(header_bytes.len() as u64).to_le_bytes());
2064 st_data.extend_from_slice(&header_bytes);
2065 let floats: [f32; 4] = [1.0, 2.0, 3.0, 4.0];
2067 for f in &floats {
2068 st_data.extend_from_slice(&f.to_le_bytes());
2069 }
2070
2071 let pt_bytes = SafeTensorsToPyTorchConverter
2073 .convert(&st_data, &ConversionOptions::default(), None)
2074 .unwrap();
2075 assert!(pt_bytes.len() > 4);
2077 assert_eq!(&pt_bytes[0..2], b"PK"); let st2_bytes = PyTorchToSafeTensorsConverter
2081 .convert(&pt_bytes, &ConversionOptions::default(), None)
2082 .unwrap();
2083 assert!(st2_bytes.len() > 8);
2085 let hdr_len = u64::from_le_bytes(st2_bytes[0..8].try_into().unwrap()) as usize;
2086 assert!(hdr_len > 0 && hdr_len < st2_bytes.len());
2087 let hdr: serde_json::Value = serde_json::from_slice(&st2_bytes[8..8 + hdr_len]).unwrap();
2088 assert!(
2090 hdr.get("weight").is_some(),
2091 "Header missing 'weight': {hdr}"
2092 );
2093 }
2094
2095 #[test]
2096 fn test_pytorch_to_safetensors_requires_valid_zip() {
2097 let conv = PyTorchToSafeTensorsConverter;
2098 let err = conv
2099 .convert(b"model data", &ConversionOptions::default(), None)
2100 .unwrap_err();
2101 assert!(format!("{err}").contains("ZIP archive"));
2102 }
2103
2104 #[test]
2105 fn test_onnx_to_tensorrt_plan() {
2106 let conv = OnnxToTensorRtConverter;
2107 let plan_bytes = conv
2108 .convert(b"onnx data", &ConversionOptions::default(), None)
2109 .unwrap();
2110 let plan: serde_json::Value = serde_json::from_slice(&plan_bytes).unwrap();
2111 assert_eq!(plan["converter"], "onnx_to_tensorrt");
2112 }
2113
2114 #[test]
2115 fn test_onnx_to_coreml_plan() {
2116 let conv = OnnxToCoreMLConverter;
2117 let plan_bytes = conv
2118 .convert(b"onnx data", &ConversionOptions::default(), None)
2119 .unwrap();
2120 let plan: serde_json::Value = serde_json::from_slice(&plan_bytes).unwrap();
2121 assert_eq!(plan["converter"], "onnx_to_coreml");
2122 }
2123
2124 #[test]
2125 fn test_safetensors_to_gguf_plan_default() {
2126 let conv = SafeTensorsToGgufConverter;
2127 let plan_bytes = conv
2128 .convert(b"", &ConversionOptions::default(), None)
2129 .unwrap();
2130 let plan: serde_json::Value = serde_json::from_slice(&plan_bytes).unwrap();
2131 assert_eq!(plan["converter"], "safetensors_to_gguf");
2132 assert_eq!(plan["quantization"], "f16"); }
2134
2135 #[test]
2136 fn test_safetensors_to_pytorch_valid_header() {
2137 let header = serde_json::json!({
2139 "weight": { "dtype": "F32", "shape": [2, 2], "data_offsets": [0, 16] }
2140 });
2141 let header_bytes = serde_json::to_vec(&header).unwrap();
2142 let mut data = Vec::new();
2143 data.extend_from_slice(&(header_bytes.len() as u64).to_le_bytes());
2144 data.extend_from_slice(&header_bytes);
2145 data.extend_from_slice(&[0u8; 16]); let conv = SafeTensorsToPyTorchConverter;
2148 let pt_bytes = conv
2149 .convert(&data, &ConversionOptions::default(), None)
2150 .unwrap();
2151 assert!(pt_bytes.len() > 4);
2153 assert_eq!(&pt_bytes[0..2], b"PK"); }
2155
2156 #[test]
2157 fn test_safetensors_to_pytorch_too_small() {
2158 let conv = SafeTensorsToPyTorchConverter;
2159 let err = conv
2160 .convert(b"tiny", &ConversionOptions::default(), None)
2161 .unwrap_err();
2162 assert!(format!("{err}").contains("too small"));
2163 }
2164
2165 #[test]
2166 fn test_safetensors_to_pytorch_header_exceeds() {
2167 let mut data = Vec::new();
2168 data.extend_from_slice(&(1000u64).to_le_bytes()); data.extend_from_slice(b"short"); let conv = SafeTensorsToPyTorchConverter;
2171 let err = conv
2172 .convert(&data, &ConversionOptions::default(), None)
2173 .unwrap_err();
2174 assert!(format!("{err}").contains("exceeds"));
2175 }
2176
2177 #[test]
2180 fn test_onnx_metadata_extractor_minimal() {
2181 let data = vec![
2184 0x08, 0x07, 0x12, 0x04, b't', b'e', b's',
2186 b't', ];
2188
2189 let conv = OnnxMetadataExtractor;
2190 let result = conv
2191 .convert(&data, &ConversionOptions::default(), None)
2192 .unwrap();
2193 let meta: serde_json::Value = serde_json::from_slice(&result).unwrap();
2194 assert_eq!(meta["ir_version"], 7);
2195 assert_eq!(meta["producer"], "test");
2196 }
2197
2198 #[test]
2201 fn test_supported_conversions_non_empty() {
2202 let pipeline = ConversionPipeline::with_builtins();
2203 let list = pipeline.supported_conversions();
2204 assert!(!list.is_empty());
2205 for (source, target, name) in &list {
2207 assert!(!name.is_empty());
2208 assert_ne!(source, target); }
2210 }
2211
2212 #[test]
2215 fn test_multi_step_conversion_executes() {
2216 let pipeline = ConversionPipeline::with_builtins();
2217 let result = pipeline.convert(
2219 b"model data",
2220 &ModelFormat::PyTorch,
2221 &ModelFormat::TensorRT,
2222 &ConversionOptions::default(),
2223 None,
2224 );
2225 assert!(result.is_ok());
2226 let conv = result.unwrap();
2227 assert_eq!(conv.conversion_path.len(), 3);
2228 }
2229
2230 #[test]
2231 fn test_validate_empty_output_fails() {
2232 let converter = RawToSafeTensorsConverter;
2233 let report = converter.validate(b"input data", b"", &ConversionOptions::default());
2234 assert!(!report.passed, "Empty output should fail validation");
2235 assert!(report
2236 .checks
2237 .iter()
2238 .any(|c| !c.passed && c.name == "non_empty"));
2239 }
2240
2241 #[test]
2242 fn test_validate_huge_ratio_without_quantization_fails() {
2243 let converter = RawToSafeTensorsConverter;
2244 let input = b"x";
2245 let output = vec![0u8; 200];
2247 let report = converter.validate(input, &output, &ConversionOptions::default());
2248 assert!(report
2249 .checks
2250 .iter()
2251 .any(|c| !c.passed && c.name == "size_ratio"));
2252 }
2253
2254 #[test]
2255 fn test_validation_check_pass_and_fail() {
2256 let p = ValidationCheck::pass("ok", "good");
2257 assert!(p.passed);
2258 assert_eq!(p.name, "ok");
2259
2260 let f = ValidationCheck::fail("bad", "nope");
2261 assert!(!f.passed);
2262 assert_eq!(f.name, "bad");
2263 }
2264
2265 #[test]
2266 fn test_compression_ratio_zero_input() {
2267 let result = ConversionResult {
2269 data: vec![],
2270 plan: None,
2271 source_format: ModelFormat::PyTorch,
2272 target_format: ModelFormat::Safetensors,
2273 conversion_path: vec![],
2274 input_size: 0,
2275 output_size: 100,
2276 validation: None,
2277 };
2278 assert!((result.compression_ratio() - 0.0).abs() < f64::EPSILON);
2279 }
2280
2281 #[test]
2282 fn test_safetensors_to_raw_with_progress() {
2283 let raw_data = b"tensor bytes for progress test";
2285 let st = RawToSafeTensorsConverter
2287 .convert(raw_data, &ConversionOptions::default(), None)
2288 .unwrap();
2289
2290 let progress_called = std::sync::Arc::new(std::sync::Mutex::new(false));
2291 let pc = progress_called.clone();
2292 let cb: ProgressCallback = Box::new(move |_p| {
2293 *pc.lock().unwrap() = true;
2294 });
2295
2296 let result = SafeTensorsToRawConverter
2297 .convert(&st, &ConversionOptions::default(), Some(&cb))
2298 .unwrap();
2299 assert_eq!(result, raw_data);
2300 assert!(*progress_called.lock().unwrap());
2301 }
2302
2303 #[test]
2304 fn test_raw_to_safetensors_with_progress() {
2305 let progress_called = std::sync::Arc::new(std::sync::Mutex::new(false));
2307 let pc = progress_called.clone();
2308 let cb: ProgressCallback = Box::new(move |_p| {
2309 *pc.lock().unwrap() = true;
2310 });
2311
2312 let result = RawToSafeTensorsConverter
2313 .convert(b"data", &ConversionOptions::default(), Some(&cb))
2314 .unwrap();
2315 assert!(!result.is_empty());
2316 assert!(*progress_called.lock().unwrap());
2317 }
2318
2319 #[test]
2320 fn test_safetensors_to_raw_header_exceeds() {
2321 let mut data = Vec::new();
2323 data.extend_from_slice(&(9999u64).to_le_bytes());
2324 data.extend_from_slice(b"short");
2325 let err = SafeTensorsToRawConverter
2326 .convert(&data, &ConversionOptions::default(), None)
2327 .unwrap_err();
2328 assert!(format!("{err}").contains("exceeds"));
2329 }
2330
2331 #[test]
2332 fn test_shim_pipeline_returns_plan_not_data() {
2333 let pipeline = ConversionPipeline::with_builtins();
2337 let conv = pipeline
2338 .convert(
2339 b"model data",
2340 &ModelFormat::PyTorch,
2341 &ModelFormat::ONNX,
2342 &ConversionOptions::with_validation(),
2343 None,
2344 )
2345 .expect("planning a shim conversion is not an error");
2346
2347 assert!(conv.is_plan(), "shim conversion must be reported as a plan");
2348 assert!(
2349 conv.data.is_empty(),
2350 "a plan must not masquerade as model data"
2351 );
2352 assert_eq!(conv.output_size, 0);
2353 assert_eq!(conv.plan.as_ref().unwrap()["converter"], "pytorch_to_onnx");
2354 assert!(conv.validation.is_none());
2356 }
2357
2358 #[test]
2359 fn test_native_conversion_is_not_a_plan() {
2360 let header = serde_json::json!({
2362 "weight": { "dtype": "F32", "shape": [2, 2], "data_offsets": [0, 16] }
2363 });
2364 let header_bytes = serde_json::to_vec(&header).unwrap();
2365 let mut safetensors = Vec::new();
2366 safetensors.extend_from_slice(&(header_bytes.len() as u64).to_le_bytes());
2367 safetensors.extend_from_slice(&header_bytes);
2368 for f in &[1.0f32, 2.0, 3.0, 4.0] {
2369 safetensors.extend_from_slice(&f.to_le_bytes());
2370 }
2371
2372 let pipeline = ConversionPipeline::with_builtins();
2373 let conv = pipeline
2374 .convert(
2375 &safetensors,
2376 &ModelFormat::Safetensors,
2377 &ModelFormat::PyTorch,
2378 &ConversionOptions::default(),
2379 None,
2380 )
2381 .expect("native conversion should succeed");
2382
2383 assert!(!conv.is_plan());
2384 assert!(conv.plan.is_none());
2385 assert!(!conv.data.is_empty());
2386 }
2387
2388 #[test]
2389 fn test_multi_step_conversion_with_validation() {
2390 let pipeline = ConversionPipeline::with_builtins();
2392 let opts = ConversionOptions::with_validation();
2393 let result = pipeline.convert(
2395 b"model data",
2396 &ModelFormat::PyTorch,
2397 &ModelFormat::TensorRT,
2398 &opts,
2399 None,
2400 );
2401 let _ = result;
2403 }
2404
2405 #[test]
2406 fn test_onnx_metadata_fields_5_and_6() {
2407 let mut data = vec![
2409 0x08, 0x09, 0x12, 0x08, ];
2412 data.extend_from_slice(b"TestProd");
2413 data.extend_from_slice(&[
2414 0x28, 0x2A, 0x32, 0x0C, ]);
2417 data.extend_from_slice(b"A test model");
2418
2419 let result = OnnxMetadataExtractor
2420 .convert(&data, &ConversionOptions::default(), None)
2421 .unwrap();
2422 let meta: serde_json::Value = serde_json::from_slice(&result).unwrap();
2423 assert_eq!(meta["ir_version"], 9);
2424 assert_eq!(meta["producer"], "TestProd");
2425 assert_eq!(meta["model_version"], 42);
2426 assert_eq!(meta["doc_string"], "A test model");
2427 }
2428
2429 #[test]
2430 fn test_onnx_metadata_skip_32bit_64bit() {
2431 let mut data = Vec::new();
2433 data.push(0x08);
2435 data.push(0x07);
2436 data.push(0x55);
2438 data.extend_from_slice(&[0u8; 4]);
2439 data.push(0x59);
2441 data.extend_from_slice(&[0u8; 8]);
2442
2443 let result = OnnxMetadataExtractor
2444 .convert(&data, &ConversionOptions::default(), None)
2445 .unwrap();
2446 let meta: serde_json::Value = serde_json::from_slice(&result).unwrap();
2447 assert_eq!(meta["ir_version"], 7);
2448 }
2449
2450 #[test]
2451 fn test_validate_magic_bytes_onnx() {
2452 let data = vec![0x08, 0x07, 0x12, 0x04]; let check = validate_magic_bytes(&data, &ModelFormat::ONNX);
2455 assert!(check.passed);
2456 }
2457
2458 #[test]
2459 fn test_validate_magic_bytes_tflite() {
2460 let mut data = vec![0x20, 0x00, 0x00, 0x00]; data.extend_from_slice(&[0u8; 20]);
2463 let check = validate_magic_bytes(&data, &ModelFormat::TFLite);
2464 assert!(check.passed);
2465 }
2466
2467 #[test]
2468 fn test_validate_magic_bytes_pytorch_pickle() {
2469 let data = vec![0x80, 0x02, 0x00, 0x00];
2471 let check = validate_magic_bytes(&data, &ModelFormat::PyTorch);
2472 assert!(check.passed);
2473 }
2474
2475 #[test]
2476 fn test_validate_magic_bytes_pytorch_invalid() {
2477 let data = vec![0x00, 0xFF, 0x00, 0x00]; let check = validate_magic_bytes(&data, &ModelFormat::PyTorch);
2479 assert!(!check.passed);
2480 }
2481
2482 #[test]
2483 fn test_validate_magic_bytes_safetensors_invalid() {
2484 let mut data = Vec::new();
2486 data.extend_from_slice(&(99999u64).to_le_bytes()); let check = validate_magic_bytes(&data, &ModelFormat::Safetensors);
2488 assert!(!check.passed);
2489 }
2490
2491 #[test]
2492 fn test_validate_magic_bytes_safetensors_too_small() {
2493 let check = validate_magic_bytes(b"tiny", &ModelFormat::Safetensors);
2494 assert!(!check.passed);
2495 }
2496
2497 #[test]
2498 fn test_validate_magic_bytes_gguf_mismatch() {
2499 let check = validate_magic_bytes(b"NOT_GGUF", &ModelFormat::GGUF);
2500 assert!(!check.passed);
2501 }
2502
2503 #[test]
2504 fn test_validate_magic_bytes_tflite_mismatch() {
2505 let check = validate_magic_bytes(b"\x00\x00\x00\x00extra", &ModelFormat::TFLite);
2506 assert!(!check.passed);
2507 }
2508
2509 #[test]
2510 fn test_validate_magic_bytes_onnx_mismatch() {
2511 let check = validate_magic_bytes(b"\xFF\xFF", &ModelFormat::ONNX);
2512 assert!(!check.passed);
2513 }
2514
2515 #[test]
2516 fn test_gguf_header_parser_too_small() {
2517 let err = GgufHeaderParser
2519 .convert(b"tiny_data", &ConversionOptions::default(), None)
2520 .unwrap_err();
2521 assert!(format!("{err}").contains("too small"));
2522 }
2523
2524 #[test]
2525 fn test_safetensors_to_pytorch_invalid_header_json() {
2526 let invalid_json = b"not valid json at all";
2528 let mut data = Vec::new();
2529 data.extend_from_slice(&(invalid_json.len() as u64).to_le_bytes());
2530 data.extend_from_slice(invalid_json);
2531 data.extend_from_slice(&[0u8; 16]);
2532 let err = SafeTensorsToPyTorchConverter
2533 .convert(&data, &ConversionOptions::default(), None)
2534 .unwrap_err();
2535 assert!(format!("{err}").contains("Invalid SafeTensors header JSON"));
2536 }
2537
2538 #[test]
2539 fn test_pipeline_default() {
2540 let pipeline = ConversionPipeline::default();
2542 let list = pipeline.supported_conversions();
2543 assert!(!list.is_empty());
2544 }
2545
2546 #[test]
2547 fn test_multi_step_halts_at_first_plan_step() {
2548 let pipeline = ConversionPipeline::with_builtins();
2553 let steps_arc = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
2554 let sc = steps_arc.clone();
2555 let cb: ProgressCallback = Box::new(move |p| {
2556 sc.lock().unwrap().push(p.step);
2557 });
2558 let conv = pipeline
2559 .convert(
2560 b"model",
2561 &ModelFormat::PyTorch,
2562 &ModelFormat::TensorRT,
2563 &ConversionOptions::default(),
2564 Some(&cb),
2565 )
2566 .expect("planning should not error");
2567
2568 let logged = steps_arc.lock().unwrap();
2569 assert_eq!(logged.len(), 1, "must stop after the first plan step");
2570 assert!(conv.is_plan());
2571 assert_eq!(conv.plan.as_ref().unwrap()["converter"], "pytorch_to_onnx");
2572 }
2573
2574 #[test]
2575 fn test_parse_varint_overflow() {
2576 let data = vec![0x80u8; 10]; let (result, pos) = parse_varint(&data, 0);
2581 assert!(pos <= data.len());
2583 let _ = result; }
2585
2586 #[test]
2587 fn test_parse_protobuf_tag_empty() {
2588 let data: &[u8] = &[];
2590 assert!(parse_protobuf_tag(data, 0).is_none());
2591
2592 let data2 = &[0x08u8]; assert!(parse_protobuf_tag(data2, 5).is_none());
2595 }
2596
2597 #[test]
2598 fn test_parse_length_delimited_exceeds() {
2599 let data = vec![100u8, 0xAA, 0xBB]; let result = parse_length_delimited(&data, 0);
2603 assert!(result.is_err());
2604 let msg = format!("{}", result.unwrap_err());
2605 assert!(msg.contains("exceeds data"));
2606 }
2607
2608 #[test]
2611 fn test_onnx_metadata_empty_data() {
2612 let result = OnnxMetadataExtractor
2613 .convert(&[], &ConversionOptions::default(), None)
2614 .unwrap();
2615 let meta: serde_json::Value = serde_json::from_slice(&result).unwrap();
2616 assert_eq!(meta["ir_version"], 0);
2617 assert_eq!(meta["producer"], "");
2618 }
2619
2620 #[test]
2621 fn test_onnx_metadata_skip_unknown_wire_type() {
2622 let mut data = vec![
2624 0x08, 0x05, ];
2626 data.push(0xA3);
2628 data.push(0x01);
2629 let result = OnnxMetadataExtractor
2631 .convert(&data, &ConversionOptions::default(), None)
2632 .unwrap();
2633 let meta: serde_json::Value = serde_json::from_slice(&result).unwrap();
2634 assert_eq!(meta["ir_version"], 5);
2635 }
2636
2637 #[test]
2638 fn test_onnx_metadata_skip_varint_field() {
2639 let data = vec![
2641 0x08, 0x03, 0x18, 0x42, 0x28, 0x01, ];
2645 let result = OnnxMetadataExtractor
2646 .convert(&data, &ConversionOptions::default(), None)
2647 .unwrap();
2648 let meta: serde_json::Value = serde_json::from_slice(&result).unwrap();
2649 assert_eq!(meta["ir_version"], 3);
2650 assert_eq!(meta["model_version"], 1);
2651 }
2652
2653 #[test]
2654 fn test_onnx_metadata_skip_length_delimited_field() {
2655 let mut data = vec![
2657 0x08, 0x04, 0x3A, 0x03, ];
2660 data.extend_from_slice(b"abc"); data.extend_from_slice(&[0x28, 0x0A]); let result = OnnxMetadataExtractor
2664 .convert(&data, &ConversionOptions::default(), None)
2665 .unwrap();
2666 let meta: serde_json::Value = serde_json::from_slice(&result).unwrap();
2667 assert_eq!(meta["ir_version"], 4);
2668 assert_eq!(meta["model_version"], 10);
2669 }
2670
2671 #[test]
2674 fn test_safetensors_to_pytorch_header_too_large() {
2675 let mut data = Vec::new();
2677 let huge = 200 * 1024 * 1024u64; data.extend_from_slice(&huge.to_le_bytes());
2679 data.extend_from_slice(&[0u8; 16]);
2680 let err = SafeTensorsToPyTorchConverter
2681 .convert(&data, &ConversionOptions::default(), None)
2682 .unwrap_err();
2683 assert!(format!("{err}").contains("too large"));
2684 }
2685
2686 #[test]
2687 fn test_safetensors_to_pytorch_missing_dtype() {
2688 let header = serde_json::json!({
2689 "weight": { "shape": [2], "data_offsets": [0, 8] }
2690 });
2691 let hdr_bytes = serde_json::to_vec(&header).unwrap();
2692 let mut data = Vec::new();
2693 data.extend_from_slice(&(hdr_bytes.len() as u64).to_le_bytes());
2694 data.extend_from_slice(&hdr_bytes);
2695 data.extend_from_slice(&[0u8; 8]);
2696 let err = SafeTensorsToPyTorchConverter
2697 .convert(&data, &ConversionOptions::default(), None)
2698 .unwrap_err();
2699 assert!(format!("{err}").contains("dtype"));
2700 }
2701
2702 #[test]
2703 fn test_safetensors_to_pytorch_missing_shape() {
2704 let header = serde_json::json!({
2705 "weight": { "dtype": "F32", "data_offsets": [0, 8] }
2706 });
2707 let hdr_bytes = serde_json::to_vec(&header).unwrap();
2708 let mut data = Vec::new();
2709 data.extend_from_slice(&(hdr_bytes.len() as u64).to_le_bytes());
2710 data.extend_from_slice(&hdr_bytes);
2711 data.extend_from_slice(&[0u8; 8]);
2712 let err = SafeTensorsToPyTorchConverter
2713 .convert(&data, &ConversionOptions::default(), None)
2714 .unwrap_err();
2715 assert!(format!("{err}").contains("shape"));
2716 }
2717
2718 #[test]
2719 fn test_safetensors_to_pytorch_missing_offsets() {
2720 let header = serde_json::json!({
2721 "weight": { "dtype": "F32", "shape": [2] }
2722 });
2723 let hdr_bytes = serde_json::to_vec(&header).unwrap();
2724 let mut data = Vec::new();
2725 data.extend_from_slice(&(hdr_bytes.len() as u64).to_le_bytes());
2726 data.extend_from_slice(&hdr_bytes);
2727 data.extend_from_slice(&[0u8; 8]);
2728 let err = SafeTensorsToPyTorchConverter
2729 .convert(&data, &ConversionOptions::default(), None)
2730 .unwrap_err();
2731 assert!(format!("{err}").contains("data_offsets"));
2732 }
2733
2734 #[test]
2735 fn test_safetensors_to_pytorch_tensor_not_object() {
2736 let header = serde_json::json!({
2737 "weight": "not an object"
2738 });
2739 let hdr_bytes = serde_json::to_vec(&header).unwrap();
2740 let mut data = Vec::new();
2741 data.extend_from_slice(&(hdr_bytes.len() as u64).to_le_bytes());
2742 data.extend_from_slice(&hdr_bytes);
2743 let err = SafeTensorsToPyTorchConverter
2744 .convert(&data, &ConversionOptions::default(), None)
2745 .unwrap_err();
2746 assert!(format!("{err}").contains("not an object"));
2747 }
2748
2749 #[test]
2750 fn test_safetensors_to_pytorch_out_of_bounds_offsets() {
2751 let header = serde_json::json!({
2752 "weight": { "dtype": "F32", "shape": [100], "data_offsets": [0, 99999] }
2753 });
2754 let hdr_bytes = serde_json::to_vec(&header).unwrap();
2755 let mut data = Vec::new();
2756 data.extend_from_slice(&(hdr_bytes.len() as u64).to_le_bytes());
2757 data.extend_from_slice(&hdr_bytes);
2758 data.extend_from_slice(&[0u8; 16]); let err = SafeTensorsToPyTorchConverter
2760 .convert(&data, &ConversionOptions::default(), None)
2761 .unwrap_err();
2762 assert!(format!("{err}").contains("out of bounds"));
2763 }
2764
2765 #[test]
2766 fn test_safetensors_to_pytorch_unsupported_dtype() {
2767 let header = serde_json::json!({
2768 "weight": { "dtype": "COMPLEX128", "shape": [2], "data_offsets": [0, 16] }
2769 });
2770 let hdr_bytes = serde_json::to_vec(&header).unwrap();
2771 let mut data = Vec::new();
2772 data.extend_from_slice(&(hdr_bytes.len() as u64).to_le_bytes());
2773 data.extend_from_slice(&hdr_bytes);
2774 data.extend_from_slice(&[0u8; 16]);
2775 let err = SafeTensorsToPyTorchConverter
2776 .convert(&data, &ConversionOptions::default(), None)
2777 .unwrap_err();
2778 assert!(format!("{err}").contains("Unsupported dtype"));
2779 }
2780
2781 #[test]
2782 fn test_safetensors_to_pytorch_metadata_skipped() {
2783 let header = serde_json::json!({
2785 "__metadata__": { "format": "pt" },
2786 "bias": { "dtype": "F32", "shape": [4], "data_offsets": [0, 16] }
2787 });
2788 let hdr_bytes = serde_json::to_vec(&header).unwrap();
2789 let mut data = Vec::new();
2790 data.extend_from_slice(&(hdr_bytes.len() as u64).to_le_bytes());
2791 data.extend_from_slice(&hdr_bytes);
2792 data.extend_from_slice(&[0u8; 16]);
2793 let result = SafeTensorsToPyTorchConverter
2794 .convert(&data, &ConversionOptions::default(), None)
2795 .unwrap();
2796 assert_eq!(&result[0..2], b"PK"); }
2798
2799 #[test]
2800 fn test_safetensors_to_pytorch_with_progress() {
2801 let header = serde_json::json!({
2802 "w": { "dtype": "F32", "shape": [2], "data_offsets": [0, 8] }
2803 });
2804 let hdr_bytes = serde_json::to_vec(&header).unwrap();
2805 let mut data = Vec::new();
2806 data.extend_from_slice(&(hdr_bytes.len() as u64).to_le_bytes());
2807 data.extend_from_slice(&hdr_bytes);
2808 data.extend_from_slice(&[0u8; 8]);
2809
2810 let msgs = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
2811 let mc = msgs.clone();
2812 let cb: ProgressCallback = Box::new(move |p| {
2813 mc.lock().unwrap().push(p.message.clone());
2814 });
2815 let result = SafeTensorsToPyTorchConverter
2816 .convert(&data, &ConversionOptions::default(), Some(&cb))
2817 .unwrap();
2818 assert!(!result.is_empty());
2819 assert!(msgs.lock().unwrap().len() >= 2);
2820 }
2821
2822 #[test]
2825 fn test_pytorch_to_safetensors_with_progress() {
2826 let header = serde_json::json!({
2828 "w": { "dtype": "F32", "shape": [2], "data_offsets": [0, 8] }
2829 });
2830 let hdr_bytes = serde_json::to_vec(&header).unwrap();
2831 let mut st_data = Vec::new();
2832 st_data.extend_from_slice(&(hdr_bytes.len() as u64).to_le_bytes());
2833 st_data.extend_from_slice(&hdr_bytes);
2834 st_data.extend_from_slice(&[0u8; 8]);
2835
2836 let pt_bytes = SafeTensorsToPyTorchConverter
2837 .convert(&st_data, &ConversionOptions::default(), None)
2838 .unwrap();
2839
2840 let msgs = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
2841 let mc = msgs.clone();
2842 let cb: ProgressCallback = Box::new(move |p| {
2843 mc.lock().unwrap().push(p.message.clone());
2844 });
2845 let result = PyTorchToSafeTensorsConverter
2846 .convert(&pt_bytes, &ConversionOptions::default(), Some(&cb))
2847 .unwrap();
2848 assert!(!result.is_empty());
2849 assert!(!msgs.lock().unwrap().is_empty());
2850 }
2851
2852 #[test]
2853 fn test_pytorch_to_safetensors_fallback_no_pickle() {
2854 let mut buf = std::io::Cursor::new(Vec::new());
2856 {
2857 let mut zip = zip::ZipWriter::new(&mut buf);
2858 let opts = zip::write::SimpleFileOptions::default()
2859 .compression_method(zip::CompressionMethod::Stored);
2860 zip.start_file("archive/data/0", opts).unwrap();
2861 std::io::Write::write_all(&mut zip, &[1u8, 2, 3, 4]).unwrap();
2862 zip.finish().unwrap();
2863 }
2864 let pt_bytes = buf.into_inner();
2865 let result = PyTorchToSafeTensorsConverter
2866 .convert(&pt_bytes, &ConversionOptions::default(), None)
2867 .unwrap();
2868 let hdr_len = u64::from_le_bytes(result[0..8].try_into().unwrap()) as usize;
2869 let hdr: serde_json::Value = serde_json::from_slice(&result[8..8 + hdr_len]).unwrap();
2870 assert!(hdr.get("storage_0").is_some());
2872 let entry = &hdr["storage_0"];
2873 assert_eq!(entry["dtype"], "U8");
2874 }
2875
2876 #[test]
2879 fn test_safetensors_dtype_to_pytorch_all_types() {
2880 assert_eq!(
2881 safetensors_dtype_to_pytorch("F64"),
2882 Some(("DoubleStorage", 8))
2883 );
2884 assert_eq!(
2885 safetensors_dtype_to_pytorch("F32"),
2886 Some(("FloatStorage", 4))
2887 );
2888 assert_eq!(
2889 safetensors_dtype_to_pytorch("F16"),
2890 Some(("HalfStorage", 2))
2891 );
2892 assert_eq!(
2893 safetensors_dtype_to_pytorch("BF16"),
2894 Some(("BFloat16Storage", 2))
2895 );
2896 assert_eq!(
2897 safetensors_dtype_to_pytorch("I64"),
2898 Some(("LongStorage", 8))
2899 );
2900 assert_eq!(safetensors_dtype_to_pytorch("I32"), Some(("IntStorage", 4)));
2901 assert_eq!(
2902 safetensors_dtype_to_pytorch("I16"),
2903 Some(("ShortStorage", 2))
2904 );
2905 assert_eq!(safetensors_dtype_to_pytorch("I8"), Some(("CharStorage", 1)));
2906 assert_eq!(safetensors_dtype_to_pytorch("U8"), Some(("ByteStorage", 1)));
2907 assert_eq!(
2908 safetensors_dtype_to_pytorch("BOOL"),
2909 Some(("BoolStorage", 1))
2910 );
2911 assert_eq!(safetensors_dtype_to_pytorch("UNKNOWN"), None);
2912 }
2913
2914 #[test]
2915 fn test_pytorch_storage_to_safetensors_dtype_all_types() {
2916 assert_eq!(
2917 pytorch_storage_to_safetensors_dtype("DoubleStorage"),
2918 Some(("F64", 8))
2919 );
2920 assert_eq!(
2921 pytorch_storage_to_safetensors_dtype("FloatStorage"),
2922 Some(("F32", 4))
2923 );
2924 assert_eq!(
2925 pytorch_storage_to_safetensors_dtype("HalfStorage"),
2926 Some(("F16", 2))
2927 );
2928 assert_eq!(
2929 pytorch_storage_to_safetensors_dtype("BFloat16Storage"),
2930 Some(("BF16", 2))
2931 );
2932 assert_eq!(
2933 pytorch_storage_to_safetensors_dtype("LongStorage"),
2934 Some(("I64", 8))
2935 );
2936 assert_eq!(
2937 pytorch_storage_to_safetensors_dtype("IntStorage"),
2938 Some(("I32", 4))
2939 );
2940 assert_eq!(
2941 pytorch_storage_to_safetensors_dtype("ShortStorage"),
2942 Some(("I16", 2))
2943 );
2944 assert_eq!(
2945 pytorch_storage_to_safetensors_dtype("CharStorage"),
2946 Some(("I8", 1))
2947 );
2948 assert_eq!(
2949 pytorch_storage_to_safetensors_dtype("ByteStorage"),
2950 Some(("U8", 1))
2951 );
2952 assert_eq!(
2953 pytorch_storage_to_safetensors_dtype("UntypedStorage"),
2954 Some(("U8", 1))
2955 );
2956 assert_eq!(
2957 pytorch_storage_to_safetensors_dtype("BoolStorage"),
2958 Some(("BOOL", 1))
2959 );
2960 assert_eq!(pytorch_storage_to_safetensors_dtype("UnknownStorage"), None);
2961 }
2962
2963 #[test]
2966 fn test_write_short_binunicode_short() {
2967 let mut pkl = Vec::new();
2968 write_short_binunicode(&mut pkl, "abc");
2969 assert_eq!(pkl[0], 0x8c); assert_eq!(pkl[1], 3);
2971 assert_eq!(&pkl[2..5], b"abc");
2972 }
2973
2974 #[test]
2975 fn test_write_short_binunicode_long() {
2976 let mut pkl = Vec::new();
2977 let s = "x".repeat(300);
2978 write_short_binunicode(&mut pkl, &s);
2979 assert_eq!(pkl[0], 0x8d); let len = u32::from_le_bytes(pkl[1..5].try_into().unwrap());
2981 assert_eq!(len, 300);
2982 }
2983
2984 #[test]
2985 fn test_write_pickle_int_ranges() {
2986 let mut pkl = Vec::new();
2988 write_pickle_int(&mut pkl, 42);
2989 assert_eq!(pkl[0], 0x4b);
2990 assert_eq!(pkl[1], 42);
2991
2992 pkl.clear();
2994 write_pickle_int(&mut pkl, 1000);
2995 assert_eq!(pkl[0], 0x4d);
2996 assert_eq!(u16::from_le_bytes(pkl[1..3].try_into().unwrap()), 1000);
2997
2998 pkl.clear();
3000 write_pickle_int(&mut pkl, 100_000);
3001 assert_eq!(pkl[0], 0x4a);
3002 assert_eq!(i32::from_le_bytes(pkl[1..5].try_into().unwrap()), 100_000);
3003
3004 pkl.clear();
3006 write_pickle_int(&mut pkl, i64::MAX);
3007 assert_eq!(pkl[0], 0x8a); }
3009
3010 #[test]
3011 fn test_write_pickle_int_negative() {
3012 let mut pkl = Vec::new();
3013 write_pickle_int(&mut pkl, -100);
3014 assert_eq!(pkl[0], 0x4a); assert_eq!(i32::from_le_bytes(pkl[1..5].try_into().unwrap()), -100);
3016
3017 pkl.clear();
3019 write_pickle_int(&mut pkl, i64::MIN);
3020 assert_eq!(pkl[0], 0x8a); }
3022
3023 #[test]
3024 fn test_extract_tensor_info_from_pickle_empty() {
3025 let infos = extract_tensor_info_from_pickle(&[]);
3026 assert!(infos.is_empty());
3027 }
3028
3029 #[test]
3030 fn test_extract_tensor_info_from_pickle_roundtrip() {
3031 let tensors = vec![ConvTensorEntry {
3033 name: "model.weight".to_string(),
3034 dtype: "F32".to_string(),
3035 shape: vec![3, 4],
3036 data_start: 0,
3037 data_end: 48,
3038 }];
3039 let pkl = build_pytorch_pickle(&tensors).unwrap();
3040 let infos = extract_tensor_info_from_pickle(&pkl);
3041 assert!(!infos.is_empty());
3042 assert_eq!(infos[0].name, "model.weight");
3043 assert_eq!(infos[0].dtype, "F32");
3044 }
3045
3046 #[test]
3047 fn test_safetensors_to_pytorch_multiple_dtypes() {
3048 let header = serde_json::json!({
3050 "w1": { "dtype": "F32", "shape": [2], "data_offsets": [0, 8] },
3051 "w2": { "dtype": "F16", "shape": [4], "data_offsets": [8, 16] },
3052 "w3": { "dtype": "I64", "shape": [1], "data_offsets": [16, 24] }
3053 });
3054 let hdr_bytes = serde_json::to_vec(&header).unwrap();
3055 let mut data = Vec::new();
3056 data.extend_from_slice(&(hdr_bytes.len() as u64).to_le_bytes());
3057 data.extend_from_slice(&hdr_bytes);
3058 data.extend_from_slice(&[0u8; 24]);
3059 let result = SafeTensorsToPyTorchConverter
3060 .convert(&data, &ConversionOptions::default(), None)
3061 .unwrap();
3062 assert_eq!(&result[0..2], b"PK");
3063
3064 let st2 = PyTorchToSafeTensorsConverter
3066 .convert(&result, &ConversionOptions::default(), None)
3067 .unwrap();
3068 let hdr_len = u64::from_le_bytes(st2[0..8].try_into().unwrap()) as usize;
3069 let hdr: serde_json::Value = serde_json::from_slice(&st2[8..8 + hdr_len]).unwrap();
3070 assert!(hdr.as_object().unwrap().len() >= 3);
3071 }
3072
3073 #[test]
3074 fn test_pipeline_convert_no_path() {
3075 let pipeline = ConversionPipeline::with_builtins();
3077 let err = pipeline
3078 .convert(
3079 b"data",
3080 &ModelFormat::MXNet,
3081 &ModelFormat::Darknet,
3082 &ConversionOptions::default(),
3083 None,
3084 )
3085 .unwrap_err();
3086 assert!(format!("{err}").contains("No conversion path"));
3087 }
3088
3089 #[test]
3090 fn test_validate_magic_bytes_onnx_short_data() {
3091 let check = validate_magic_bytes(b"", &ModelFormat::ONNX);
3093 assert!(!check.passed);
3094 }
3095}