1use crate::nested::{adapter::DefaultAdapter, data::NestedValue, de::Deserializer};
6use crate::{DType, Tensor};
7use serde::de::DeserializeOwned;
8use std::collections::HashMap;
9use std::fs::File;
10use std::io;
11use std::io::{BufRead, BufReader, Cursor, Read, Seek, SeekFrom};
12use std::path::{Path, PathBuf};
13use std::sync::Arc;
14
15use crate::pickle_reader::{
16 Object, PersistentIds, PickleError, StorageRef, build_tensor, extract_tensors, key_string,
17 non_negative, read_pickle, storage_type_to_dtype,
18};
19use crate::storage::{Backing, LegacySource, StorageSource, TarSource, ZipSource};
20use byteorder::{LittleEndian, ReadBytesExt};
21use thiserror::Error;
22
23#[derive(Debug, Error)]
25#[non_exhaustive]
26pub enum PytorchError {
27 #[error("IO error: {0}")]
29 Io(#[from] std::io::Error),
30 #[error("pickle: {0}")]
32 Pickle(#[from] PickleError),
33 #[error("zip archive error: {0}")]
35 Zip(#[from] zip::result::ZipError),
36 #[error("TAR archive error: {0}")]
38 Tar(#[source] std::io::Error),
39 #[error("invalid PyTorch file format: {0}")]
41 InvalidFormat(String),
42 #[error("key not found in PyTorch file: {0}")]
44 KeyNotFound(String),
45 #[error("serde deserialization error: {0}")]
47 Serde(#[from] crate::nested::error::Error),
48}
49
50type Result<T> = std::result::Result<T, PytorchError>;
51
52#[derive(Debug, Clone)]
57pub struct PytorchMetadata {
58 pub format_version: Option<String>,
60 pub format_type: FileFormat,
62 pub byte_order: ByteOrder,
64 pub has_storage_alignment: bool,
66 pub pytorch_version: Option<String>,
69 pub tensor_count: usize,
71 pub total_data_size: Option<usize>,
73}
74
75impl PytorchMetadata {
76 pub fn is_modern_format(&self) -> bool {
78 matches!(self.format_type, FileFormat::Zip)
79 }
80
81 pub fn is_legacy_format(&self) -> bool {
83 matches!(self.format_type, FileFormat::Legacy)
84 }
85
86 fn for_format(format_type: FileFormat) -> Self {
87 Self {
88 format_version: None,
89 format_type,
90 byte_order: ByteOrder::LittleEndian,
91 has_storage_alignment: false,
92 pytorch_version: None,
93 tensor_count: 0,
94 total_data_size: None,
95 }
96 }
97}
98
99#[derive(Debug, Clone, PartialEq)]
101pub enum FileFormat {
102 Zip,
104 Tar,
106 Legacy,
108 Pickle,
110}
111
112#[derive(Debug, Clone, PartialEq)]
114pub enum ByteOrder {
115 LittleEndian,
116 BigEndian,
117}
118
119#[derive(Debug)]
147pub struct PytorchReader {
148 tensors: HashMap<String, Tensor>,
149 metadata: PytorchMetadata,
150}
151
152impl PytorchReader {
153 pub fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
161 Self::open(&Container::File(path.as_ref().to_path_buf()), None)
162 }
163
164 pub fn with_top_level_key<P: AsRef<Path>>(path: P, key: &str) -> Result<Self> {
182 Self::open(&Container::File(path.as_ref().to_path_buf()), Some(key))
183 }
184
185 pub fn from_bytes(bytes: Vec<u8>, top_level_key: Option<&str>) -> Result<Self> {
207 Self::open(&Container::Memory(Arc::new(bytes)), top_level_key)
208 }
209
210 fn open(container: &Container, top_level_key: Option<&str>) -> Result<Self> {
211 let Loaded { root, mut metadata } = load_file(container)?;
212 let tensors = extract_tensors_at(root, top_level_key)?;
213 metadata.tensor_count = tensors.len();
214 Ok(Self { tensors, metadata })
215 }
216
217 pub fn from_reader<R: Read>(reader: R, top_level_key: Option<&str>) -> Result<Self> {
228 let root = read_pickle(&mut BufReader::new(reader), &PersistentIds::Unavailable)?;
229 let tensors = extract_tensors_at(root, top_level_key)?;
230 let mut metadata = PytorchMetadata::for_format(FileFormat::Pickle);
231 metadata.tensor_count = tensors.len();
232 Ok(Self { tensors, metadata })
233 }
234
235 pub fn keys(&self) -> Vec<String> {
237 self.tensors.keys().cloned().collect()
238 }
239
240 pub fn get(&self, name: &str) -> Option<&Tensor> {
242 self.tensors.get(name)
243 }
244
245 pub fn tensors(&self) -> &HashMap<String, Tensor> {
247 &self.tensors
248 }
249
250 pub fn into_tensors(self) -> HashMap<String, Tensor> {
252 self.tensors
253 }
254
255 pub fn metadata(&self) -> &PytorchMetadata {
259 &self.metadata
260 }
261
262 pub fn len(&self) -> usize {
264 self.tensors.len()
265 }
266
267 pub fn is_empty(&self) -> bool {
269 self.tensors.is_empty()
270 }
271
272 pub fn read_pickle_data<P: AsRef<Path>>(
286 path: P,
287 top_level_key: Option<&str>,
288 ) -> Result<PickleValue> {
289 Self::read_pickle_data_from_container(
290 Container::File(path.as_ref().to_path_buf()),
291 top_level_key,
292 )
293 }
294
295 fn read_pickle_data_from_container(
296 container: Container,
297 top_level_key: Option<&str>,
298 ) -> Result<PickleValue> {
299 let Loaded { root, .. } = load_file(&container)?;
300 let value = select_top_level(root, top_level_key)?;
301 Ok(to_pickle_value(value))
302 }
303
304 pub fn load_config<D, P>(path: P, top_level_key: Option<&str>) -> Result<D>
341 where
342 D: DeserializeOwned,
343 P: AsRef<Path>,
344 {
345 let pickle_value = Self::read_pickle_data_from_container(
346 Container::File(path.as_ref().to_path_buf()),
347 top_level_key,
348 )?;
349 let nested_value = to_nested_value(pickle_value);
350 let deserializer = Deserializer::<DefaultAdapter>::new(nested_value, false);
351 Ok(D::deserialize(deserializer)?)
352 }
353}
354
355#[derive(Debug, Clone, PartialEq)]
360pub enum PickleValue {
361 None,
363 Bool(bool),
365 Int(i64),
367 Float(f64),
369 String(String),
371 List(Vec<PickleValue>),
373 Dict(HashMap<String, PickleValue>),
375 Bytes(Vec<u8>),
377 Unsupported(String),
381}
382
383struct Loaded {
389 root: Object,
390 metadata: PytorchMetadata,
392}
393
394#[derive(Clone)]
395pub(crate) enum Container {
396 File(PathBuf),
397 Memory(Arc<Vec<u8>>),
398}
399
400impl Container {
401 fn header(&self, n: usize) -> Result<Vec<u8>> {
403 match self {
404 Container::File(path) => {
405 let mut header = Vec::new();
406 File::open(path)?.take(n as u64).read_to_end(&mut header)?;
407 Ok(header)
408 }
409 Container::Memory(buf) => Ok(buf[..buf.len().min(n)].to_vec()),
410 }
411 }
412
413 pub(crate) fn reader(&self) -> io::Result<Box<dyn ContainerRead>> {
414 match self {
415 Container::File(path) => Ok(Box::new(BufReader::new(File::open(path)?))),
416 Container::Memory(buf) => Ok(Box::new(Cursor::new(ArcBytes(Arc::clone(buf))))),
417 }
418 }
419}
420
421pub(crate) trait ContainerRead: Read + BufRead + Seek + Send {}
422impl<T: Read + BufRead + Seek + Send + ?Sized> ContainerRead for T {}
423
424pub(crate) struct ArcBytes(pub(crate) Arc<Vec<u8>>);
425impl AsRef<[u8]> for ArcBytes {
426 fn as_ref(&self) -> &[u8] {
427 &self.0
428 }
429}
430
431const LEGACY_MAGIC_BYTES: [u8; 10] = [0x6c, 0xfc, 0x9c, 0x46, 0xf9, 0x20, 0x6a, 0xa8, 0x50, 0x19];
434
435const LEGACY_MAGIC_TEXT: &[u8] = b"119547037146038801333356";
437
438const LEGACY_MAGIC_SEARCH: usize = 64;
442
443fn starts_with_legacy_magic(header: &[u8]) -> bool {
445 let window = &header[..header.len().min(LEGACY_MAGIC_SEARCH)];
446 window
447 .windows(LEGACY_MAGIC_BYTES.len())
448 .any(|bytes| bytes == LEGACY_MAGIC_BYTES)
449 || window
450 .windows(LEGACY_MAGIC_TEXT.len())
451 .any(|bytes| bytes == LEGACY_MAGIC_TEXT)
452}
453
454const TAR_MAGIC_OFFSET: usize = 257;
456
457fn detect_format(container: &Container) -> Result<FileFormat> {
458 let header = container.header(TAR_MAGIC_OFFSET + 5)?;
459
460 if header.starts_with(b"PK\x03\x04") || header.starts_with(b"PK\x05\x06") {
461 Ok(FileFormat::Zip)
462 } else if header.get(TAR_MAGIC_OFFSET..TAR_MAGIC_OFFSET + 5) == Some(b"ustar") {
463 Ok(FileFormat::Tar)
464 } else if starts_with_legacy_magic(&header) {
465 Ok(FileFormat::Legacy)
466 } else {
467 match header.first() {
468 Some(0x80) | Some(b'(') | Some(b'}') | Some(b']') | Some(b'c')
469 if !starts_like_safetensors(&header) =>
470 {
471 Ok(FileFormat::Pickle)
472 }
473 _ => Err(PytorchError::InvalidFormat(
474 "not a PyTorch checkpoint: no ZIP, TAR, legacy or pickle header found".to_string(),
475 )),
476 }
477 }
478}
479
480const SAFETENSORS_MAX_HEADER: u64 = 100_000_000;
482
483fn starts_like_safetensors(header: &[u8]) -> bool {
488 let Some(len) = header.first_chunk::<8>().map(|b| u64::from_le_bytes(*b)) else {
489 return false;
490 };
491 len <= SAFETENSORS_MAX_HEADER && header.get(8) == Some(&b'{')
492}
493
494fn load_file(container: &Container) -> Result<Loaded> {
495 match detect_format(container)? {
496 FileFormat::Zip => load_zip(container),
497 FileFormat::Tar => load_tar(container),
498 FileFormat::Legacy => load_legacy(container),
499 FileFormat::Pickle => load_plain_pickle(container),
500 }
501}
502
503fn load_zip(container: &Container) -> Result<Loaded> {
504 let source = ZipSource::open(container)?;
505
506 match source.read_text("byteorder")?.as_deref() {
507 None | Some("little") => {}
508 Some("big") => return Err(big_endian_error()),
509 Some(other) => {
510 return Err(PytorchError::InvalidFormat(format!(
511 "Unrecognized byteorder entry '{other}', expected 'little' or 'big'"
512 )));
513 }
514 }
515 let mut metadata = PytorchMetadata::for_format(FileFormat::Zip);
516 metadata.format_version = source.read_text(".format_version")?;
517 metadata.pytorch_version = source.read_text("version")?;
518 metadata.has_storage_alignment = source.has_entry(".storage_alignment");
519 metadata.total_data_size = Some(source.data_size()? as usize);
520
521 let pickle = source.pickle()?;
522 let ids = PersistentIds::Storages(Arc::new(StorageSource::Zip(source)));
523 let root = read_pickle(&mut Cursor::new(pickle), &ids)?;
524
525 Ok(Loaded { root, metadata })
526}
527
528fn load_legacy(container: &Container) -> Result<Loaded> {
529 let (mut reader, backing): (Box<dyn ContainerRead>, Backing) = match container {
530 Container::File(path) => {
531 let file = File::open(path)?;
532 let stream = file.try_clone()?;
533 (Box::new(BufReader::new(stream)), Backing::File(file))
534 }
535 Container::Memory(buf) => (
536 Box::new(Cursor::new(ArcBytes(Arc::clone(buf)))),
537 Backing::Memory(Arc::clone(buf)),
538 ),
539 };
540
541 let read_header = |reader: &mut Box<dyn ContainerRead>, what: &str| {
542 read_pickle(reader, &PersistentIds::Unavailable).map_err(|e| {
543 PytorchError::InvalidFormat(format!("Failed to read {what} from legacy format: {e}"))
544 })
545 };
546 read_header(&mut reader, "magic number")?;
550 let protocol_version = read_header(&mut reader, "protocol version")?;
552 if !matches!(protocol_version, Object::Int(1001)) {
553 return Err(PytorchError::InvalidFormat(format!(
554 "Unsupported legacy protocol version {protocol_version:?}, expected 1001"
555 )));
556 }
557 let sys_info = read_header(&mut reader, "system info")?;
558 check_little_endian(&sys_info)?;
559
560 let source = Arc::new(StorageSource::Legacy(LegacySource::new(backing)));
565 let root = read_pickle(&mut reader, &PersistentIds::Storages(source.clone()))?;
566
567 let storage_keys = match read_header(&mut reader, "storage key list")? {
569 Object::List(keys) => keys
570 .iter()
571 .map(|key| key_string(key, "legacy storage key"))
572 .collect::<std::result::Result<Vec<_>, _>>()?,
573 other => {
574 return Err(PytorchError::InvalidFormat(format!(
575 "legacy storage key list must be a list, got {other:?}"
576 )));
577 }
578 };
579
580 let data_start = reader.stream_position()?;
581 let file_len = reader.seek(SeekFrom::End(0))?;
582 if let StorageSource::Legacy(legacy) = &*source {
583 legacy
584 .finish(&storage_keys, data_start, file_len)
585 .map_err(|e| PytorchError::InvalidFormat(e.to_string()))?;
586 }
587
588 let mut metadata = PytorchMetadata::for_format(FileFormat::Legacy);
589 metadata.total_data_size = Some((file_len - data_start) as usize);
590 Ok(Loaded { root, metadata })
591}
592
593fn load_plain_pickle(container: &Container) -> Result<Loaded> {
594 let mut reader = container.reader()?;
595 let root = read_pickle(&mut reader, &PersistentIds::Unavailable)?;
596 Ok(Loaded {
597 root,
598 metadata: PytorchMetadata::for_format(FileFormat::Pickle),
599 })
600}
601
602fn load_tar(container: &Container) -> Result<Loaded> {
603 let mut archive = tar::Archive::new(container.reader()?);
604 let mut entries: HashMap<String, Vec<u8>> = HashMap::new();
605
606 for entry in archive.entries().map_err(PytorchError::Tar)? {
607 let mut entry = entry.map_err(PytorchError::Tar)?;
608 let name = entry
609 .path()
610 .map_err(PytorchError::Tar)?
611 .to_string_lossy()
612 .trim_start_matches("./")
613 .to_string();
614 if matches!(
615 name.as_str(),
616 "sys_info" | "storages" | "tensors" | "pickle"
617 ) {
618 let mut data = Vec::new();
619 entry.read_to_end(&mut data).map_err(PytorchError::Tar)?;
620 entries.insert(name, data);
621 }
622 }
623
624 if let Some(sys_info) = entries.remove("sys_info") {
625 let sys_info = read_pickle(&mut Cursor::new(sys_info), &PersistentIds::Unavailable)?;
626 check_little_endian(&sys_info)?;
627 }
628 let mut take = |name: &str| {
629 entries
630 .remove(name)
631 .ok_or_else(|| PytorchError::InvalidFormat(format!("TAR file missing '{name}' entry")))
632 };
633 let storages = take("storages")?;
634 let tensors = take("tensors")?;
635 let pickle = take("pickle")?;
636
637 let total_data_size = storages.len();
638 let (tar_source, storage_info) = parse_tar_storages(storages)?;
639 let source = Arc::new(StorageSource::Tar(tar_source));
640 let tensors = parse_tar_tensors(&tensors, &storage_info, &source)?;
641 let root = read_pickle(&mut Cursor::new(pickle), &PersistentIds::Tensors(tensors))?;
642
643 let mut metadata = PytorchMetadata::for_format(FileFormat::Tar);
644 metadata.total_data_size = Some(total_data_size);
645 Ok(Loaded { root, metadata })
646}
647
648type TarStorageInfo = HashMap<String, (DType, usize)>;
650
651fn parse_tar_storages(blob: Vec<u8>) -> Result<(TarSource, TarStorageInfo)> {
654 let mut cursor = Cursor::new(blob.as_slice());
655 let count = read_tar_count(&mut cursor, "storage")?;
656
657 let plausible = count.min(blob.len() / 8);
659 let mut layout: HashMap<String, (usize, usize)> = HashMap::with_capacity(plausible);
660 let mut info = TarStorageInfo::with_capacity(plausible);
661
662 for _ in 0..count {
663 let meta = read_pickle(&mut cursor, &PersistentIds::Unavailable)?;
664 let [key, _location, storage_type] = tar_fields(&meta, "storage metadata")?;
665 let key = key_string(key, "TAR storage key")?;
668 let dtype = match storage_type {
669 Object::Class { name, .. } => storage_type_to_dtype(name.as_str())?,
670 other => {
671 return Err(PytorchError::InvalidFormat(format!(
672 "TAR storage type must be a class, got {other:?}"
673 )));
674 }
675 };
676
677 let numel = read_tar_usize(&mut cursor, "storage element count")?;
678 let byte_len = numel.checked_mul(dtype.size()).ok_or_else(|| {
679 PytorchError::InvalidFormat(format!("TAR storage '{key}' byte length overflows usize"))
680 })?;
681 let offset = cursor.position() as usize;
682 let end = offset
683 .checked_add(byte_len)
684 .filter(|&end| end <= blob.len())
685 .ok_or_else(|| {
686 PytorchError::InvalidFormat(format!(
687 "TAR storage '{key}' ({byte_len} bytes) extends beyond the storages entry"
688 ))
689 })?;
690
691 layout.insert(key.clone(), (offset, byte_len));
692 info.insert(key, (dtype, byte_len));
693 cursor.set_position(end as u64);
694 }
695
696 {
699 let views = read_pickle(&mut cursor, &PersistentIds::Unavailable).map_err(|e| {
700 PytorchError::InvalidFormat(format!("TAR storage views list missing: {e}"))
701 })?;
702 let Object::List(views) = views else {
703 return Err(PytorchError::InvalidFormat(format!(
704 "TAR storage views must be a list, got {views:?}"
705 )));
706 };
707 for view in views {
708 let [target, root, offset, numel] = tar_fields(&view, "storage view")?;
709 let target = key_string(target, "TAR storage view key")?;
710 let root = key_string(root, "TAR storage view root")?;
711 let offset = non_negative(offset, "TAR storage view offset")?;
712 let numel = non_negative(numel, "TAR storage view element count")?;
713 let &(root_offset, root_len) = layout.get(&root).ok_or_else(|| {
714 PytorchError::InvalidFormat(format!(
715 "TAR storage view '{target}' refers to unknown storage '{root}'"
716 ))
717 })?;
718 let (dtype, _) = info[&root];
719 let element_size = dtype.size();
720 let (start, len) = offset
721 .checked_mul(element_size)
722 .zip(numel.checked_mul(element_size))
723 .filter(|&(start, len)| start.checked_add(len).is_some_and(|end| end <= root_len))
724 .ok_or_else(|| {
725 PytorchError::InvalidFormat(format!(
726 "TAR storage view '{target}' lies outside storage '{root}'"
727 ))
728 })?;
729 layout.insert(target.clone(), (root_offset + start, len));
730 info.insert(target, (dtype, len));
731 }
732 }
733
734 Ok((TarSource::new(blob, layout), info))
735}
736
737fn parse_tar_tensors(
740 blob: &[u8],
741 storage_info: &TarStorageInfo,
742 source: &Arc<StorageSource>,
743) -> Result<HashMap<String, Tensor>> {
744 let mut cursor = Cursor::new(blob);
745 let count = read_tar_count(&mut cursor, "tensor")?;
746 let mut tensors = HashMap::with_capacity(count.min(blob.len() / 8));
747
748 for _ in 0..count {
749 let meta = read_pickle(&mut cursor, &PersistentIds::Unavailable)?;
750 let [key, storage_key, _tensor_type] = tar_fields(&meta, "tensor metadata")?;
751 let key = key_string(key, "TAR tensor key")?;
752 let storage_key = key_string(storage_key, "TAR tensor storage key")?;
753
754 let rank = cursor.read_i32::<LittleEndian>()?;
755 let rank = usize::try_from(rank)
756 .ok()
757 .filter(|&rank| rank <= 64)
758 .ok_or_else(|| {
759 PytorchError::InvalidFormat(format!("TAR tensor '{key}' has invalid rank {rank}"))
760 })?;
761 cursor.read_i32::<LittleEndian>()?; let shape = (0..rank)
763 .map(|_| read_tar_usize(&mut cursor, "tensor shape"))
764 .collect::<Result<Vec<_>>>()?;
765 let stride = (0..rank)
766 .map(|_| read_tar_usize(&mut cursor, "tensor stride"))
767 .collect::<Result<Vec<_>>>()?;
768 let storage_offset = read_tar_usize(&mut cursor, "tensor storage offset")?;
769
770 let &(dtype, byte_len) = storage_info.get(&storage_key).ok_or_else(|| {
771 PytorchError::InvalidFormat(format!(
772 "TAR tensor '{key}' refers to unknown storage '{storage_key}'"
773 ))
774 })?;
775 let storage = StorageRef {
776 source: source.clone(),
777 key: storage_key,
778 dtype: Some(dtype),
779 reachable_bytes: byte_len,
782 view_offset: 0,
783 };
784 let tensor = build_tensor(storage, dtype, storage_offset, shape, stride)?;
785 tensors.insert(key, tensor);
786 }
787
788 Ok(tensors)
789}
790
791fn read_tar_count(cursor: &mut Cursor<&[u8]>, what: &str) -> Result<usize> {
792 let count = read_pickle(cursor, &PersistentIds::Unavailable)?;
793 Ok(non_negative(&count, &format!("TAR {what} count"))?)
794}
795
796fn read_tar_usize(cursor: &mut Cursor<&[u8]>, what: &str) -> Result<usize> {
798 let value = cursor.read_i64::<LittleEndian>()?;
799 usize::try_from(value).map_err(|_| {
800 PytorchError::InvalidFormat(format!("TAR {what} must be non-negative, got {value}"))
801 })
802}
803
804fn tar_fields<'a, const N: usize>(obj: &'a Object, what: &str) -> Result<&'a [Object; N]> {
806 match obj {
807 Object::Tuple(fields) => <&[Object; N]>::try_from(fields.as_slice()).map_err(|_| {
808 PytorchError::InvalidFormat(format!(
809 "TAR {what} must have {N} fields, got {}",
810 fields.len()
811 ))
812 }),
813 other => Err(PytorchError::InvalidFormat(format!(
814 "TAR {what} must be a tuple, got {other:?}"
815 ))),
816 }
817}
818
819fn check_little_endian(sys_info: &Object) -> Result<()> {
825 let Object::Dict(dict) = sys_info else {
826 return Err(PytorchError::InvalidFormat(format!(
827 "Legacy sys_info must be a dict, got {sys_info:?}"
828 )));
829 };
830 match dict.get("little_endian") {
831 Some(Object::Bool(true)) => Ok(()),
832 Some(Object::Bool(false)) => Err(big_endian_error()),
833 other => Err(PytorchError::InvalidFormat(format!(
834 "Legacy sys_info must carry a little_endian bool, got {other:?}"
835 ))),
836 }
837}
838
839fn big_endian_error() -> PytorchError {
840 PytorchError::InvalidFormat(
841 "Big-endian PyTorch files are not supported. The file was saved on a big-endian system and requires byte order conversion."
842 .to_string(),
843 )
844}
845
846fn select_top_level(root: Object, key: Option<&str>) -> Result<Object> {
852 let Some(key) = key else {
853 return Ok(root);
854 };
855 let Object::Dict(mut dict) = root else {
856 return Err(PytorchError::InvalidFormat(format!(
857 "Cannot select top-level key '{key}': the file's root is not a dictionary"
858 )));
859 };
860 dict.remove(key).ok_or_else(|| {
861 let mut available: Vec<_> = dict.keys().cloned().collect();
862 available.sort();
863 PytorchError::KeyNotFound(format!(
864 "'{key}' not found. Available top-level keys: {available:?}"
865 ))
866 })
867}
868
869fn extract_tensors_at(
870 root: Object,
871 top_level_key: Option<&str>,
872) -> Result<HashMap<String, Tensor>> {
873 match select_top_level(root, top_level_key)? {
874 Object::Dict(dict) => Ok(extract_tensors(dict)),
875 other => {
876 let found = other.python_type_name();
877 Err(PytorchError::InvalidFormat(match top_level_key {
878 Some(key) => {
879 format!("Top-level key '{key}' does not hold a dictionary, found {found}")
880 }
881 None => {
882 format!("Expected a dictionary at the root of the PyTorch file, found {found}")
883 }
884 }))
885 }
886 }
887}
888
889fn to_pickle_value(obj: Object) -> PickleValue {
894 match obj {
895 Object::None => PickleValue::None,
896 Object::Bool(b) => PickleValue::Bool(b),
897 Object::Int(i) => PickleValue::Int(i),
898 Object::Float(f) => PickleValue::Float(f),
899 Object::String(s) => PickleValue::String(s),
900 Object::Bytes(b) => PickleValue::Bytes(b),
901 Object::List(items) | Object::Tuple(items) => {
902 PickleValue::List(items.into_iter().map(to_pickle_value).collect())
903 }
904 Object::Dict(dict) => PickleValue::Dict(
905 dict.into_iter()
906 .map(|(k, v)| (k, to_pickle_value(v)))
907 .collect(),
908 ),
909 Object::Class { .. } | Object::Storage(_) | Object::Opaque(_) | Object::Tensor(_) => {
910 PickleValue::Unsupported(obj.python_type_name())
911 }
912 }
913}
914
915fn to_nested_value(value: PickleValue) -> NestedValue {
917 match value {
918 PickleValue::None => NestedValue::Default(None),
919 PickleValue::Bool(b) => NestedValue::Bool(b),
920 PickleValue::Int(i) => NestedValue::I64(i),
921 PickleValue::Float(f) => NestedValue::F64(f),
922 PickleValue::String(s) => NestedValue::String(s),
923 PickleValue::List(list) => {
924 NestedValue::Vec(list.into_iter().map(to_nested_value).collect())
925 }
926 PickleValue::Dict(dict) => NestedValue::Map(
927 dict.into_iter()
928 .map(|(k, v)| (k, to_nested_value(v)))
929 .collect(),
930 ),
931 PickleValue::Bytes(data) => {
932 NestedValue::Vec(data.into_iter().map(NestedValue::U8).collect())
933 }
934 PickleValue::Unsupported(type_name) => NestedValue::Unsupported(type_name),
935 }
936}