1use crate::storage::{StorageSource, read_exact_len};
16use crate::{DType, MAX_TENSOR_SIZE, Tensor};
17use byteorder::{BigEndian, LittleEndian, ReadBytesExt};
18use std::collections::{HashMap, HashSet};
19use std::io::{self, BufRead};
20use std::sync::Arc;
21use thiserror::Error;
22
23const STRING_PREALLOC_BOUND: usize = 1 << 16;
26
27#[derive(Debug, Error)]
29#[non_exhaustive]
30pub enum PickleError {
31 #[error("IO error: {0}")]
32 Io(#[from] io::Error),
33 #[error(
35 "invalid pickle opcode: 0x{0:02x}. The file may be corrupted or use an unsupported pickle feature."
36 )]
37 InvalidOpCode(u8),
38 #[error("unsupported pickle opcode {0:?}")]
41 UnsupportedOpCode(OpCode),
42 #[error("invalid or unsupported pickle protocol version: {0}. Supported versions are 0-5.")]
43 InvalidProtocol(u8),
44 #[error("unexpected pickle opcode {0:?} in current context")]
45 UnexpectedOpCode(OpCode),
46 #[error(
47 "unsupported Python type '{0}'. The file holds tensor data in a form this reader cannot represent."
48 )]
49 UnsupportedType(String),
50 #[error("invalid data in pickle file: {0}")]
51 InvalidData(String),
52 #[error("pickle stack underflow - the file may be corrupted")]
53 StackUnderflow,
54 #[error("pickle memo reference {0} not found - the file may be corrupted")]
55 MemoNotFound(u32),
56 #[error(
58 "pickle references tensor storages but no tensor data is available; tensors can only be loaded from a PyTorch checkpoint file, not a plain pickle"
59 )]
60 NoDataSource,
61}
62
63type Result<T> = std::result::Result<T, PickleError>;
64
65#[repr(u8)]
67#[derive(Debug, Eq, PartialEq, Clone, Copy)]
68#[non_exhaustive]
69pub enum OpCode {
70 Int = b'I',
72 Long = b'L',
73 Float = b'F',
74 Unicode = b'V',
75 Get = b'g',
76 Put = b'p',
77 PersId = b'P',
78 Global = b'c',
79 Mark = b'(',
80 Stop = b'.',
81 Pop = b'0',
82 PopMark = b'1',
83 Dup = b'2',
84 None = b'N',
85 Reduce = b'R',
86 Build = b'b',
87 Dict = b'd',
88 List = b'l',
89 Tuple = b't',
90 SetItem = b's',
91 Append = b'a',
92 BinInt = b'J',
94 BinInt1 = b'K',
95 BinInt2 = b'M',
96 BinFloat = b'G',
97 BinString = b'T',
98 ShortBinString = b'U',
99 BinUnicode = b'X',
100 EmptyTuple = b')',
101 EmptyList = b']',
102 EmptyDict = b'}',
103 Appends = b'e',
104 SetItems = b'u',
105 BinGet = b'h',
106 LongBinGet = b'j',
107 BinPut = b'q',
108 LongBinPut = b'r',
109 BinPersId = b'Q',
110 Proto = 0x80,
112 NewObj = 0x81,
113 Ext1 = 0x82,
114 Ext2 = 0x83,
115 Ext4 = 0x84,
116 Tuple1 = 0x85,
117 Tuple2 = 0x86,
118 Tuple3 = 0x87,
119 NewTrue = 0x88,
120 NewFalse = 0x89,
121 Long1 = 0x8a,
122 Long4 = 0x8b,
123 BinBytes = b'B',
125 ShortBinBytes = b'C',
126 ShortBinUnicode = 0x8c,
128 BinUnicode8 = 0x8d,
129 BinBytes8 = 0x8e,
130 EmptySet = 0x8f,
131 AddItems = 0x90,
132 FrozenSet = 0x91,
133 NewObjEx = 0x92,
134 StackGlobal = 0x93,
135 Memoize = 0x94,
136 Frame = 0x95,
137 ByteArray8 = 0x96,
139 NextBuffer = 0x97,
140 ReadonlyBuffer = 0x98,
141}
142
143impl TryFrom<u8> for OpCode {
144 type Error = u8;
145 fn try_from(value: u8) -> std::result::Result<Self, Self::Error> {
146 use OpCode::*;
147 Ok(match value {
148 b'I' => Int,
149 b'L' => Long,
150 b'F' => Float,
151 b'V' => Unicode,
152 b'g' => Get,
153 b'p' => Put,
154 b'P' => PersId,
155 b'c' => Global,
156 b'(' => Mark,
157 b'.' => Stop,
158 b'0' => Pop,
159 b'1' => PopMark,
160 b'2' => Dup,
161 b'N' => None,
162 b'R' => Reduce,
163 b'b' => Build,
164 b'd' => Dict,
165 b'l' => List,
166 b't' => Tuple,
167 b's' => SetItem,
168 b'a' => Append,
169 b'J' => BinInt,
170 b'K' => BinInt1,
171 b'M' => BinInt2,
172 b'G' => BinFloat,
173 b'T' => BinString,
174 b'U' => ShortBinString,
175 b'X' => BinUnicode,
176 b')' => EmptyTuple,
177 b']' => EmptyList,
178 b'}' => EmptyDict,
179 b'e' => Appends,
180 b'u' => SetItems,
181 b'h' => BinGet,
182 b'j' => LongBinGet,
183 b'q' => BinPut,
184 b'r' => LongBinPut,
185 b'Q' => BinPersId,
186 0x80 => Proto,
187 0x81 => NewObj,
188 0x82 => Ext1,
189 0x83 => Ext2,
190 0x84 => Ext4,
191 0x85 => Tuple1,
192 0x86 => Tuple2,
193 0x87 => Tuple3,
194 0x88 => NewTrue,
195 0x89 => NewFalse,
196 0x8a => Long1,
197 0x8b => Long4,
198 b'B' => BinBytes,
199 b'C' => ShortBinBytes,
200 0x8c => ShortBinUnicode,
201 0x8d => BinUnicode8,
202 0x8e => BinBytes8,
203 0x8f => EmptySet,
204 0x90 => AddItems,
205 0x91 => FrozenSet,
206 0x92 => NewObjEx,
207 0x93 => StackGlobal,
208 0x94 => Memoize,
209 0x95 => Frame,
210 0x96 => ByteArray8,
211 0x97 => NextBuffer,
212 0x98 => ReadonlyBuffer,
213 other => return Err(other),
214 })
215 }
216}
217
218#[derive(Debug, Clone)]
220pub struct StorageRef {
221 pub(crate) source: Arc<StorageSource>,
223 pub(crate) key: String,
225 pub(crate) dtype: Option<DType>,
227 pub(crate) reachable_bytes: usize,
230 pub(crate) view_offset: usize,
233}
234
235#[derive(Debug, Clone)]
236pub enum Object {
237 None,
238 Bool(bool),
239 Int(i64),
240 Float(f64),
241 String(String),
242 Bytes(Vec<u8>),
243 Tuple(Vec<Object>),
244 List(Vec<Object>),
245 Dict(HashMap<String, Object>),
246 Class {
247 module_name: String,
248 name: String,
249 },
250 Storage(StorageRef),
251 Opaque(String),
255 Tensor(Tensor),
256}
257
258impl Object {
259 fn node_count(&self) -> usize {
262 match self {
263 Object::Tuple(v) | Object::List(v) => {
264 1 + v.iter().map(|o| o.node_count()).sum::<usize>()
265 }
266 Object::Dict(m) => 1 + m.values().map(|o| o.node_count()).sum::<usize>(),
267 _ => 1,
268 }
269 }
270
271 fn payload_bytes(&self) -> usize {
273 match self {
274 Object::String(s) => s.len(),
275 Object::Bytes(b) => b.len(),
276 Object::Opaque(name) => name.len(),
277 Object::Tuple(v) | Object::List(v) => v.iter().map(|o| o.payload_bytes()).sum(),
278 Object::Dict(m) => m.values().map(|o| o.payload_bytes()).sum(),
279 _ => 0,
280 }
281 }
282
283 fn holds_tensor_data(&self) -> bool {
285 match self {
286 Object::Storage(_) | Object::Tensor(_) => true,
287 Object::Tuple(v) | Object::List(v) => v.iter().any(Object::holds_tensor_data),
288 Object::Dict(m) => m.values().any(Object::holds_tensor_data),
289 _ => false,
290 }
291 }
292
293 fn type_name(&self) -> &'static str {
294 match self {
295 Object::None => "None",
296 Object::Bool(_) => "bool",
297 Object::Int(_) => "int",
298 Object::Float(_) => "float",
299 Object::String(_) => "str",
300 Object::Bytes(_) => "bytes",
301 Object::Tuple(_) => "tuple",
302 Object::List(_) => "list",
303 Object::Dict(_) => "dict",
304 Object::Class { .. } => "class",
305 Object::Storage(_) => "storage",
306 Object::Opaque(_) => "object",
307 Object::Tensor(_) => "tensor",
308 }
309 }
310
311 pub(crate) fn python_type_name(&self) -> String {
314 match self {
315 Object::Class { module_name, name } => format!("{module_name}.{name}"),
316 Object::Opaque(name) => name.clone(),
317 Object::Tensor(_) => "torch.Tensor".to_string(),
318 Object::Storage(StorageRef { dtype: Some(_), .. }) => "torch.TypedStorage".to_string(),
319 Object::Storage(_) => "torch.UntypedStorage".to_string(),
320 other => other.type_name().to_string(),
321 }
322 }
323}
324
325pub(crate) enum PersistentIds {
327 Unavailable,
329 Storages(Arc<StorageSource>),
331 Tensors(HashMap<String, Tensor>),
333}
334
335pub(crate) fn read_pickle<R: BufRead>(r: &mut R, ids: &PersistentIds) -> Result<Object> {
337 Unpickler::new(ids).run(r)
338}
339
340pub(crate) fn storage_type_to_dtype(storage_type: &str) -> Result<DType> {
346 match storage_type {
347 "FloatStorage" => Ok(DType::F32),
348 "DoubleStorage" => Ok(DType::F64),
349 "HalfStorage" => Ok(DType::F16),
350 "BFloat16Storage" => Ok(DType::BF16),
351 "LongStorage" => Ok(DType::I64),
352 "IntStorage" => Ok(DType::I32),
353 "ShortStorage" => Ok(DType::I16),
354 "CharStorage" => Ok(DType::I8),
355 "ByteStorage" => Ok(DType::U8),
356 "BoolStorage" => Ok(DType::Bool),
357 _ => Err(PickleError::UnsupportedType(format!(
358 "torch.{storage_type}"
359 ))),
360 }
361}
362
363fn torch_dtype_to_dtype(name: &str) -> Result<DType> {
365 match name {
366 "float32" | "float" => Ok(DType::F32),
367 "float64" | "double" => Ok(DType::F64),
368 "float16" | "half" => Ok(DType::F16),
369 "bfloat16" => Ok(DType::BF16),
370 "int64" | "long" => Ok(DType::I64),
371 "int32" | "int" => Ok(DType::I32),
372 "int16" | "short" => Ok(DType::I16),
373 "int8" => Ok(DType::I8),
374 "uint8" => Ok(DType::U8),
375 "uint16" => Ok(DType::U16),
376 "uint32" => Ok(DType::U32),
377 "uint64" => Ok(DType::U64),
378 "bool" => Ok(DType::Bool),
379 _ => Err(PickleError::UnsupportedType(format!("torch.{name}"))),
380 }
381}
382
383pub(crate) fn non_negative(value: &Object, what: &str) -> Result<usize> {
385 match value {
386 Object::Int(i) => usize::try_from(*i)
387 .map_err(|_| PickleError::InvalidData(format!("{what} must be non-negative, got {i}"))),
388 other => Err(PickleError::InvalidData(format!(
389 "{what} must be an int, got {}",
390 other.type_name()
391 ))),
392 }
393}
394
395pub(crate) fn key_string(value: &Object, what: &str) -> Result<String> {
397 match value {
398 Object::String(s) => Ok(s.clone()),
399 Object::Int(i) => Ok(i.to_string()),
400 other => Err(PickleError::InvalidData(format!(
401 "{what} must be a str or int, got {}",
402 other.type_name()
403 ))),
404 }
405}
406
407fn resolve_storage_id(pid: &[Object], source: &Arc<StorageSource>) -> Result<StorageRef> {
409 let [tag, storage_type, key, _location, numel, view @ ..] = pid else {
410 return Err(PickleError::InvalidData(format!(
411 "storage persistent id has {} fields, expected at least 5",
412 pid.len()
413 )));
414 };
415 if !matches!(tag, Object::String(tag) if tag == "storage") {
416 return Err(PickleError::InvalidData(format!(
417 "persistent id tag must be 'storage', got {}",
418 tag.type_name()
419 )));
420 }
421
422 let type_name = match storage_type {
423 Object::Class { name, .. } | Object::String(name) => name.as_str(),
424 other => {
425 return Err(PickleError::InvalidData(format!(
426 "storage type must be a class, got {}",
427 other.type_name()
428 )));
429 }
430 };
431 let dtype = if type_name == "UntypedStorage" {
432 None
433 } else {
434 Some(storage_type_to_dtype(type_name)?)
435 };
436
437 let key = key_string(key, "storage key")?;
438 let numel = non_negative(numel, "storage element count")?;
439 let element_size = dtype.map_or(1, |dtype| dtype.size());
440 let byte_len = numel.checked_mul(element_size).ok_or_else(|| {
441 PickleError::InvalidData(format!("storage '{key}' byte length overflows usize"))
442 })?;
443
444 let (view_offset, reachable) = match view.first() {
448 None | Some(Object::None) => (0, numel),
449 Some(Object::Tuple(view)) if view.len() == 3 => {
450 if dtype.is_none() {
451 return Err(PickleError::InvalidData(format!(
452 "storage '{key}' is untyped but carries view metadata"
453 )));
454 }
455 let offset = non_negative(&view[1], "storage view offset")?;
456 let size = non_negative(&view[2], "storage view size")?;
457 let end = offset
458 .checked_add(size)
459 .filter(|&end| end <= numel)
460 .ok_or_else(|| {
461 PickleError::InvalidData(format!(
462 "storage '{key}' has a view of {size} elements at offset {offset}, past its {numel} elements"
463 ))
464 })?;
465 (offset, end)
466 }
467 Some(other) => {
468 return Err(PickleError::InvalidData(format!(
469 "storage view metadata must be None or a 3-tuple, got {}",
470 other.type_name()
471 )));
472 }
473 };
474
475 source
476 .declare(&key, byte_len, element_size)
477 .map_err(|err| PickleError::InvalidData(err.to_string()))?;
478
479 Ok(StorageRef {
480 source: source.clone(),
481 key,
482 dtype,
483 reachable_bytes: reachable * element_size,
485 view_offset,
486 })
487}
488
489fn resolve_persistent_id(pid: Object, ids: &PersistentIds) -> Result<Object> {
490 match ids {
491 PersistentIds::Unavailable => Err(PickleError::NoDataSource),
492 PersistentIds::Storages(source) => match pid {
493 Object::Tuple(fields) => Ok(Object::Storage(resolve_storage_id(&fields, source)?)),
494 other => Err(PickleError::InvalidData(format!(
495 "persistent id must be a tuple, got {}",
496 other.type_name()
497 ))),
498 },
499 PersistentIds::Tensors(tensors) => {
500 let key = key_string(&pid, "persistent id")?;
501 tensors
502 .get(&key)
503 .cloned()
504 .map(Object::Tensor)
505 .ok_or_else(|| {
506 PickleError::InvalidData(format!(
507 "persistent id '{key}' does not name a tensor in the archive"
508 ))
509 })
510 }
511 }
512}
513
514fn reduce(callable: Object, args: Object) -> Result<Object> {
521 let (module_name, name) = match &callable {
522 Object::Class { module_name, name } => (module_name.as_str(), name.as_str()),
523 _ => return opaque(&callable, &args),
524 };
525
526 match (module_name, name) {
527 ("collections", "OrderedDict") => ordered_dict(args),
528 ("builtins" | "__builtin__", "set" | "frozenset") => {
532 sequence_arg(args, name).map(Object::List)
533 }
534 ("torch._utils", "_rebuild_tensor") => rebuild_tensor(args, TensorRebuild::Legacy),
535 ("torch._utils", "_rebuild_tensor_v2") => rebuild_tensor(args, TensorRebuild::V2),
536 ("torch._utils", "_rebuild_tensor_v3") => rebuild_tensor(args, TensorRebuild::V3),
537 ("torch._utils", "_rebuild_parameter" | "_rebuild_parameter_with_state") => match args {
540 Object::Tuple(mut fields) if !fields.is_empty() => {
541 if fields[1..].iter().any(Object::holds_tensor_data) {
542 return Err(PickleError::UnsupportedType(format!(
543 "{name} with tensor-bearing state"
544 )));
545 }
546 Ok(fields.swap_remove(0))
547 }
548 other => Err(PickleError::InvalidData(format!(
549 "{name}: expected a non-empty argument tuple, got {}",
550 other.type_name()
551 ))),
552 },
553 ("torch._tensor", "_rebuild_from_type_v2") => match args {
557 Object::Tuple(mut fields) if fields.len() >= 3 => {
558 if fields.get(3).is_some_and(Object::holds_tensor_data) {
559 return Err(PickleError::UnsupportedType(
560 "tensor subclass with tensor-bearing state".to_string(),
561 ));
562 }
563 let inner_args = fields.swap_remove(2);
564 let func = fields.swap_remove(0);
565 reduce(func, inner_args)
566 }
567 other => Err(PickleError::InvalidData(format!(
568 "_rebuild_from_type_v2: expected at least 3 arguments, got {}",
569 other.type_name()
570 ))),
571 },
572 _ => opaque(&callable, &args),
573 }
574}
575
576fn opaque(callable: &Object, args: &Object) -> Result<Object> {
584 let name = callable.python_type_name();
585 if callable.holds_tensor_data() || args.holds_tensor_data() {
586 return Err(PickleError::UnsupportedType(name));
587 }
588 Ok(Object::Opaque(name))
589}
590
591fn sequence_arg(args: Object, what: &str) -> Result<Vec<Object>> {
593 let mut fields = match args {
594 Object::Tuple(fields) => fields,
595 other => {
596 return Err(PickleError::InvalidData(format!(
597 "{what} arguments must be a tuple, got {}",
598 other.type_name()
599 )));
600 }
601 };
602 if fields.len() > 1 {
603 return Err(PickleError::InvalidData(format!(
604 "{what} takes one argument, got {}",
605 fields.len()
606 )));
607 }
608 match fields.pop() {
609 None => Ok(Vec::new()),
610 Some(Object::List(items) | Object::Tuple(items)) => Ok(items),
611 Some(other) => Err(PickleError::InvalidData(format!(
612 "{what} argument must be a list, got {}",
613 other.type_name()
614 ))),
615 }
616}
617
618fn ordered_dict(args: Object) -> Result<Object> {
620 let items = sequence_arg(args, "OrderedDict")?;
621 let mut dict = HashMap::with_capacity(items.len());
622 for item in items {
623 match item {
624 Object::List(mut pair) | Object::Tuple(mut pair) if pair.len() == 2 => {
625 let value = pair.pop().expect("pair has two items");
626 let key = pair.pop().expect("pair has two items");
627 dict.insert(dict_key(key)?, value);
628 }
629 other => {
630 return Err(PickleError::InvalidData(format!(
631 "OrderedDict items must be (key, value) pairs, got {}",
632 other.type_name()
633 )));
634 }
635 }
636 }
637 Ok(Object::Dict(dict))
638}
639
640const MODULE_TABLES: [&str; 3] = ["_parameters", "_buffers", "_modules"];
642
643fn is_module_state(attrs: &HashMap<String, Object>) -> bool {
646 MODULE_TABLES
647 .into_iter()
648 .all(|table| matches!(attrs.get(table), Some(Object::Dict(_))))
649}
650
651fn module_state_dict(mut attrs: HashMap<String, Object>) -> Result<Object> {
664 let non_persistent = match attrs.remove("_non_persistent_buffers_set") {
665 None => HashSet::new(),
666 Some(Object::List(names)) => names
667 .iter()
668 .map(|name| key_string(name, "non-persistent buffer name"))
669 .collect::<Result<HashSet<_>>>()?,
670 Some(other) => {
671 return Err(PickleError::InvalidData(format!(
672 "module _non_persistent_buffers_set must be a set, got {}",
673 other.type_name()
674 )));
675 }
676 };
677
678 let mut dict = HashMap::new();
679 for table in MODULE_TABLES {
680 let Some(Object::Dict(entries)) = attrs.remove(table) else {
681 return Err(PickleError::InvalidData(format!(
682 "module is missing its {table} table"
683 )));
684 };
685 for (name, value) in entries {
686 if matches!(value, Object::None)
687 || (table == "_buffers" && non_persistent.contains(&name))
688 {
689 continue;
690 }
691 if dict.contains_key(&name) {
694 return Err(PickleError::InvalidData(format!(
695 "module registers '{name}' more than once"
696 )));
697 }
698 dict.insert(name, value);
699 }
700 }
701 Ok(Object::Dict(dict))
702}
703
704#[derive(Clone, Copy)]
705enum TensorRebuild {
706 Legacy,
708 V2,
710 V3,
714}
715
716impl TensorRebuild {
717 fn name(self) -> &'static str {
718 match self {
719 Self::Legacy => "_rebuild_tensor",
720 Self::V2 => "_rebuild_tensor_v2",
721 Self::V3 => "_rebuild_tensor_v3",
722 }
723 }
724
725 fn arity(self) -> usize {
726 match self {
727 Self::Legacy => 4,
728 Self::V2 => 6,
729 Self::V3 => 7,
730 }
731 }
732}
733
734fn rebuild_tensor(args: Object, kind: TensorRebuild) -> Result<Object> {
735 let fn_name = kind.name();
736 let mut fields = match args {
737 Object::Tuple(fields) => fields,
738 other => {
739 return Err(PickleError::InvalidData(format!(
740 "{fn_name}: expected an argument tuple, got {}",
741 other.type_name()
742 )));
743 }
744 };
745 if fields.len() < kind.arity() {
746 return Err(PickleError::InvalidData(format!(
747 "{fn_name}: expected at least {} arguments, got {}",
748 kind.arity(),
749 fields.len()
750 )));
751 }
752
753 let storage = match std::mem::replace(&mut fields[0], Object::None) {
754 Object::Storage(storage) => storage,
755 other => {
756 return Err(PickleError::InvalidData(format!(
757 "{fn_name}: expected a storage, got {}",
758 other.type_name()
759 )));
760 }
761 };
762 let storage_offset = non_negative(&fields[1], "storage offset")?;
763 let shape = parse_dims(&fields[2], "shape")?;
764 let stride = parse_dims(&fields[3], "stride")?;
765
766 let dtype = match kind {
767 TensorRebuild::V3 => match &fields[6] {
768 Object::Class { name, .. } => torch_dtype_to_dtype(name)?,
769 other => {
770 return Err(PickleError::InvalidData(format!(
771 "{fn_name}: expected a torch dtype, got {}",
772 other.type_name()
773 )));
774 }
775 },
776 _ => storage.dtype.ok_or_else(|| {
777 PickleError::InvalidData(format!(
778 "{fn_name}: untyped storage '{}' needs an explicit dtype",
779 storage.key
780 ))
781 })?,
782 };
783 if let Some(storage_dtype) = storage.dtype
784 && storage_dtype != dtype
785 {
786 return Err(PickleError::InvalidData(format!(
787 "{fn_name}: storage '{}' holds {storage_dtype:?} but the tensor is {dtype:?}",
788 storage.key
789 )));
790 }
791
792 build_tensor(storage, dtype, storage_offset, shape, stride).map(Object::Tensor)
793}
794
795fn parse_dims(value: &Object, what: &str) -> Result<Vec<usize>> {
796 match value {
797 Object::Tuple(items) | Object::List(items) => {
798 items.iter().map(|item| non_negative(item, what)).collect()
799 }
800 other => Err(PickleError::InvalidData(format!(
801 "{what} must be a tuple, got {}",
802 other.type_name()
803 ))),
804 }
805}
806
807fn tensor_num_elements(shape: &[usize]) -> Result<usize> {
813 shape.iter().try_fold(1usize, |total, dim| {
814 total.checked_mul(*dim).ok_or_else(|| {
815 PickleError::InvalidData("Tensor shape element count overflows usize".to_string())
816 })
817 })
818}
819
820fn tensor_storage_extent(
825 shape: &[usize],
826 stride: &[usize],
827 storage_offset: usize,
828) -> Result<usize> {
829 if shape.len() != stride.len() {
830 return Err(PickleError::InvalidData(format!(
831 "Tensor stride rank {} does not match shape rank {}",
832 stride.len(),
833 shape.len()
834 )));
835 }
836
837 if tensor_num_elements(shape)? == 0 {
838 return Ok(storage_offset);
839 }
840
841 let max_index = shape
842 .iter()
843 .zip(stride)
844 .try_fold(storage_offset, |index, (&dim, &step)| {
845 let offset = (dim - 1).checked_mul(step).ok_or_else(|| {
846 PickleError::InvalidData("Tensor stride calculation overflows usize".to_string())
847 })?;
848 index.checked_add(offset).ok_or_else(|| {
849 PickleError::InvalidData("Tensor storage extent overflows usize".to_string())
850 })
851 })?;
852
853 max_index.checked_add(1).ok_or_else(|| {
854 PickleError::InvalidData("Tensor storage extent overflows usize".to_string())
855 })
856}
857
858fn is_contiguous_stride(shape: &[usize], stride: &[usize]) -> bool {
862 let mut expected = 1usize;
863 for (&dim, &step) in shape.iter().zip(stride).rev() {
864 if dim > 1 && step != expected {
865 return false;
866 }
867 let Some(next) = expected.checked_mul(dim) else {
868 return false;
869 };
870 expected = next;
871 }
872 true
873}
874
875fn gather_strided(
880 data: &[u8],
881 shape: &[usize],
882 stride: &[usize],
883 element_size: usize,
884 byte_len: usize,
885) -> Vec<u8> {
886 let mut out = Vec::with_capacity(byte_len);
887 let num_elements = byte_len / element_size;
888 for linear_index in 0..num_elements {
889 let mut remaining = linear_index;
890 let mut storage_index = 0;
891 for (&dim, &step) in shape.iter().zip(stride).rev() {
892 storage_index += (remaining % dim) * step;
893 remaining /= dim;
894 }
895 let start = storage_index * element_size;
896 out.extend_from_slice(&data[start..start + element_size]);
897 }
898 out
899}
900
901fn to_native_endian(bytes: &mut [u8], element_size: usize) {
903 if cfg!(target_endian = "big") && element_size > 1 {
904 for element in bytes.chunks_exact_mut(element_size) {
905 element.reverse();
906 }
907 }
908}
909
910pub(crate) fn build_tensor(
915 storage: StorageRef,
916 dtype: DType,
917 storage_offset: usize,
918 shape: Vec<usize>,
919 stride: Vec<usize>,
920) -> Result<Tensor> {
921 let element_size = dtype.size();
922 let storage_offset = storage_offset
923 .checked_add(storage.view_offset)
924 .ok_or_else(|| PickleError::InvalidData("Storage offset overflows usize".to_string()))?;
925 let num_elements = tensor_num_elements(&shape)?;
926 let extent = tensor_storage_extent(&shape, &stride, storage_offset)?;
927 let byte_len = num_elements.checked_mul(element_size).ok_or_else(|| {
928 PickleError::InvalidData("Tensor byte length overflows usize".to_string())
929 })?;
930
931 let declared_elements = storage.reachable_bytes / element_size;
932 if extent > declared_elements {
933 return Err(PickleError::InvalidData(format!(
934 "Tensor with shape {shape:?} and stride {stride:?} at offset {storage_offset} needs {extent} elements, but storage '{}' declares {declared_elements}",
935 storage.key
936 )));
937 }
938 if byte_len > MAX_TENSOR_SIZE {
940 return Err(PickleError::InvalidData(format!(
941 "Tensor with shape {shape:?} would materialize {byte_len} bytes, above the {MAX_TENSOR_SIZE} byte limit"
942 )));
943 }
944
945 let StorageRef { source, key, .. } = storage;
946 let provider_shape = shape.clone();
947 let window_start = storage_offset.saturating_mul(element_size);
950 let window_len = (extent - storage_offset).saturating_mul(element_size);
951 let provider = move || -> io::Result<Vec<u8>> {
952 let shape = &provider_shape;
953 let (mut data, skipped) = source.read(&key, window_start, window_len).map_err(|err| {
954 let kind = match err.kind() {
959 kind if err.raw_os_error().is_some() => kind,
960 io::ErrorKind::UnexpectedEof => io::ErrorKind::UnexpectedEof,
961 _ => io::ErrorKind::InvalidData,
962 };
963 io::Error::new(
964 kind,
965 format!("Failed to read storage '{key}' for tensor with shape {shape:?}: {err}"),
966 )
967 })?;
968
969 let available = (skipped + data.len()) / element_size;
971 if extent > available {
972 return Err(io::Error::new(
973 io::ErrorKind::UnexpectedEof,
974 format!(
975 "Tensor with shape {shape:?} requires {extent} elements from storage '{key}', but only {available} are available"
976 ),
977 ));
978 }
979
980 let mut bytes = if is_contiguous_stride(shape, &stride) {
981 data.truncate(byte_len);
982 data
983 } else {
984 gather_strided(&data, shape, &stride, element_size, byte_len)
985 };
986
987 to_native_endian(&mut bytes, element_size);
988 if dtype == DType::Bool {
989 for byte in &mut bytes {
992 *byte = u8::from(*byte != 0);
993 }
994 }
995
996 Ok(bytes)
997 };
998
999 Ok(Tensor::new(String::new(), dtype, shape, provider))
1002}
1003
1004const MAX_COPIED_NODES: usize = 1_000_000;
1011
1012const MAX_COPIED_BYTES: usize = 64 << 20;
1015
1016const MAX_NESTING_DEPTH: u32 = 1000;
1022
1023#[derive(Clone)]
1025struct Entry {
1026 object: Object,
1027 depth: u32,
1028 memo: Option<u32>,
1031}
1032
1033struct Unpickler<'a> {
1034 stack: Vec<Entry>,
1035 marks: Vec<usize>,
1037 memo: HashMap<u32, Entry>,
1038 copied_nodes: usize,
1039 copied_bytes: usize,
1040 ids: &'a PersistentIds,
1041}
1042
1043impl<'a> Unpickler<'a> {
1044 fn new(ids: &'a PersistentIds) -> Self {
1045 Self {
1046 stack: Vec::new(),
1047 marks: Vec::new(),
1048 memo: HashMap::new(),
1049 copied_nodes: 0,
1050 copied_bytes: 0,
1051 ids,
1052 }
1053 }
1054
1055 fn push_scalar(&mut self, object: Object) {
1056 self.stack.push(Entry {
1057 object,
1058 depth: 0,
1059 memo: None,
1060 });
1061 }
1062
1063 fn push(&mut self, object: Object, depth: u32) -> Result<()> {
1064 if depth > MAX_NESTING_DEPTH {
1065 return Err(PickleError::InvalidData(format!(
1066 "pickle nesting exceeds {MAX_NESTING_DEPTH} levels"
1067 )));
1068 }
1069 self.stack.push(Entry {
1070 object,
1071 depth,
1072 memo: None,
1073 });
1074 Ok(())
1075 }
1076
1077 fn mark_on_top(&self) -> bool {
1079 self.marks.last() == Some(&self.stack.len())
1080 }
1081
1082 fn pop(&mut self) -> Result<Entry> {
1083 if self.mark_on_top() {
1084 return Err(PickleError::InvalidData(
1085 "expected a value on the stack, found MARK".to_string(),
1086 ));
1087 }
1088 self.stack.pop().ok_or(PickleError::StackUnderflow)
1089 }
1090
1091 fn pop_object(&mut self) -> Result<Object> {
1092 self.pop().map(|entry| entry.object)
1093 }
1094
1095 fn top(&self) -> Result<&Entry> {
1096 if self.mark_on_top() {
1097 return Err(PickleError::InvalidData(
1098 "expected a value on the stack, found MARK".to_string(),
1099 ));
1100 }
1101 self.stack.last().ok_or(PickleError::StackUnderflow)
1102 }
1103
1104 fn top_mut(&mut self) -> Result<&mut Entry> {
1105 if self.mark_on_top() {
1106 return Err(PickleError::InvalidData(
1107 "expected a value on the stack, found MARK".to_string(),
1108 ));
1109 }
1110 self.stack.last_mut().ok_or(PickleError::StackUnderflow)
1111 }
1112
1113 fn pop_to_mark(&mut self) -> Result<(Vec<Object>, u32)> {
1117 let mark = self
1118 .marks
1119 .pop()
1120 .ok_or_else(|| PickleError::InvalidData("MARK not found on stack".to_string()))?;
1121 let entries = self.stack.split_off(mark);
1122 let depth = entries.iter().map(|entry| entry.depth).max().unwrap_or(0);
1123 Ok((
1124 entries.into_iter().map(|entry| entry.object).collect(),
1125 depth,
1126 ))
1127 }
1128
1129 fn charge_copy(&mut self, object: &Object) -> Result<()> {
1131 self.copied_nodes += object.node_count();
1132 self.copied_bytes += object.payload_bytes();
1133 if self.copied_nodes > MAX_COPIED_NODES || self.copied_bytes > MAX_COPIED_BYTES {
1134 return Err(PickleError::InvalidData(format!(
1135 "Pickle memo bomb detected: copies exceeded {MAX_COPIED_NODES} nodes or {MAX_COPIED_BYTES} bytes"
1136 )));
1137 }
1138 Ok(())
1139 }
1140
1141 fn memo_get(&mut self, idx: u32) -> Result<Entry> {
1142 let entry = self
1143 .memo
1144 .get(&idx)
1145 .ok_or(PickleError::MemoNotFound(idx))?
1146 .clone();
1147 self.charge_copy(&entry.object)?;
1148 Ok(entry)
1149 }
1150
1151 fn memo_put(&mut self, idx: u32) -> Result<()> {
1152 let top = self.top_mut()?;
1153 top.memo = Some(idx);
1154 let entry = top.clone();
1155 self.charge_copy(&entry.object)?;
1156 self.memo.insert(idx, entry);
1157 Ok(())
1158 }
1159
1160 fn append(&mut self, items: Vec<Object>, items_depth: u32, op: OpCode) -> Result<()> {
1162 let top = self.top_mut()?;
1163 let Object::List(list) = &mut top.object else {
1164 return Err(PickleError::UnexpectedOpCode(op));
1165 };
1166 list.extend(items);
1167 let depth = top.depth.max(items_depth + 1);
1168 top.depth = depth;
1169 if depth > MAX_NESTING_DEPTH {
1170 return Err(PickleError::InvalidData(format!(
1171 "pickle nesting exceeds {MAX_NESTING_DEPTH} levels"
1172 )));
1173 }
1174 Ok(())
1175 }
1176
1177 fn set_items(&mut self, items: Vec<Object>, items_depth: u32, op: OpCode) -> Result<()> {
1179 let top = self.top_mut()?;
1180 let Object::Dict(dict) = &mut top.object else {
1181 return Err(PickleError::UnexpectedOpCode(op));
1182 };
1183 insert_pairs(dict, items)?;
1184 let depth = top.depth.max(items_depth + 1);
1185 top.depth = depth;
1186 if depth > MAX_NESTING_DEPTH {
1187 return Err(PickleError::InvalidData(format!(
1188 "pickle nesting exceeds {MAX_NESTING_DEPTH} levels"
1189 )));
1190 }
1191 Ok(())
1192 }
1193
1194 fn run<R: BufRead>(mut self, r: &mut R) -> Result<Object> {
1195 loop {
1196 let byte = r.read_u8()?;
1197 let op = OpCode::try_from(byte).map_err(PickleError::InvalidOpCode)?;
1198 match op {
1199 OpCode::Proto => {
1200 let version = r.read_u8()?;
1201 if version > 5 {
1202 return Err(PickleError::InvalidProtocol(version));
1203 }
1204 }
1205 OpCode::Frame => {
1206 r.read_u64::<LittleEndian>()?;
1208 }
1209 OpCode::Stop => break,
1210
1211 OpCode::None => self.push_scalar(Object::None),
1213 OpCode::NewTrue => self.push_scalar(Object::Bool(true)),
1214 OpCode::NewFalse => self.push_scalar(Object::Bool(false)),
1215 OpCode::Int => {
1216 let text = read_line(r)?;
1217 let value = match text.as_str() {
1219 "00" => Object::Bool(false),
1220 "01" => Object::Bool(true),
1221 _ => Object::Int(parse_int(&text)?),
1222 };
1223 self.push_scalar(value);
1224 }
1225 OpCode::Long => {
1226 let text = read_line(r)?;
1227 let text = text.strip_suffix('L').unwrap_or(&text);
1228 self.push_scalar(parse_long(text)?);
1229 }
1230 OpCode::Float => {
1231 let text = read_line(r)?;
1232 let value = text.parse::<f64>().map_err(|e| {
1233 PickleError::InvalidData(format!("Invalid FLOAT value '{text}': {e}"))
1234 })?;
1235 self.push_scalar(Object::Float(value));
1236 }
1237 OpCode::Unicode => {
1238 let text = read_raw_unicode_escape_line(r)?;
1239 self.push_scalar(Object::String(text));
1240 }
1241 OpCode::BinInt => {
1242 let v = r.read_i32::<LittleEndian>()?;
1243 self.push_scalar(Object::Int(v as i64));
1244 }
1245 OpCode::BinInt1 => {
1246 let v = r.read_u8()?;
1247 self.push_scalar(Object::Int(v as i64));
1248 }
1249 OpCode::BinInt2 => {
1250 let v = r.read_u16::<LittleEndian>()?;
1251 self.push_scalar(Object::Int(v as i64));
1252 }
1253 OpCode::BinFloat => {
1254 let v = r.read_f64::<BigEndian>()?;
1255 self.push_scalar(Object::Float(v));
1256 }
1257 OpCode::Long1 => {
1258 let len = r.read_u8()? as u64;
1259 let bytes = read_exact_len(r, len, STRING_PREALLOC_BOUND)?;
1260 self.push_scalar(long_object(&bytes));
1261 }
1262 OpCode::Long4 => {
1263 let len = r.read_u32::<LittleEndian>()? as u64;
1264 let bytes = read_exact_len(r, len, STRING_PREALLOC_BOUND)?;
1265 self.push_scalar(long_object(&bytes));
1266 }
1267
1268 OpCode::ShortBinUnicode => {
1270 let len = r.read_u8()? as u64;
1271 self.push_scalar(read_string(r, len)?);
1272 }
1273 OpCode::BinUnicode => {
1274 let len = r.read_u32::<LittleEndian>()? as u64;
1275 self.push_scalar(read_string(r, len)?);
1276 }
1277 OpCode::ShortBinString => {
1278 let len = r.read_u8()? as u64;
1279 self.push_scalar(read_py2_string(r, len)?);
1280 }
1281 OpCode::BinString => {
1282 let len = r.read_u32::<LittleEndian>()? as u64;
1283 self.push_scalar(read_py2_string(r, len)?);
1284 }
1285 OpCode::BinUnicode8 => {
1286 let len = r.read_u64::<LittleEndian>()?;
1287 self.push_scalar(read_string(r, len)?);
1288 }
1289 OpCode::ShortBinBytes => {
1290 let len = r.read_u8()? as u64;
1291 self.push_scalar(Object::Bytes(read_bytes(r, len)?));
1292 }
1293 OpCode::BinBytes => {
1294 let len = r.read_u32::<LittleEndian>()? as u64;
1295 self.push_scalar(Object::Bytes(read_bytes(r, len)?));
1296 }
1297 OpCode::BinBytes8 | OpCode::ByteArray8 => {
1298 let len = r.read_u64::<LittleEndian>()?;
1299 self.push_scalar(Object::Bytes(read_bytes(r, len)?));
1300 }
1301
1302 OpCode::Mark => self.marks.push(self.stack.len()),
1304 OpCode::EmptyTuple => self.push_scalar(Object::Tuple(Vec::new())),
1305 OpCode::EmptyList | OpCode::EmptySet => self.push_scalar(Object::List(Vec::new())),
1306 OpCode::EmptyDict => self.push_scalar(Object::Dict(HashMap::new())),
1307 OpCode::Tuple => {
1308 let (items, depth) = self.pop_to_mark()?;
1309 self.push(Object::Tuple(items), depth + 1)?;
1310 }
1311 OpCode::Tuple1 => {
1312 let a = self.pop()?;
1313 self.push(Object::Tuple(vec![a.object]), a.depth + 1)?;
1314 }
1315 OpCode::Tuple2 => {
1316 let b = self.pop()?;
1317 let a = self.pop()?;
1318 let depth = a.depth.max(b.depth) + 1;
1319 self.push(Object::Tuple(vec![a.object, b.object]), depth)?;
1320 }
1321 OpCode::Tuple3 => {
1322 let c = self.pop()?;
1323 let b = self.pop()?;
1324 let a = self.pop()?;
1325 let depth = a.depth.max(b.depth).max(c.depth) + 1;
1326 self.push(Object::Tuple(vec![a.object, b.object, c.object]), depth)?;
1327 }
1328 OpCode::List | OpCode::FrozenSet => {
1329 let (items, depth) = self.pop_to_mark()?;
1330 self.push(Object::List(items), depth + 1)?;
1331 }
1332 OpCode::Append => {
1333 let value = self.pop()?;
1334 self.append(vec![value.object], value.depth, op)?;
1335 }
1336 OpCode::Appends | OpCode::AddItems => {
1337 let (items, depth) = self.pop_to_mark()?;
1338 self.append(items, depth, op)?;
1339 }
1340 OpCode::Dict => {
1341 let (items, depth) = self.pop_to_mark()?;
1342 let mut dict = HashMap::with_capacity(items.len() / 2);
1343 insert_pairs(&mut dict, items)?;
1344 self.push(Object::Dict(dict), depth + 1)?;
1345 }
1346 OpCode::SetItem => {
1347 let value = self.pop()?;
1348 let key = self.pop()?;
1349 let depth = key.depth.max(value.depth);
1350 self.set_items(vec![key.object, value.object], depth, op)?;
1351 }
1352 OpCode::SetItems => {
1353 let (items, depth) = self.pop_to_mark()?;
1354 self.set_items(items, depth, op)?;
1355 }
1356
1357 OpCode::Pop => {
1359 if self.mark_on_top() {
1361 self.marks.pop();
1362 } else {
1363 self.pop()?;
1364 }
1365 }
1366 OpCode::PopMark => {
1367 self.pop_to_mark()?;
1368 }
1369 OpCode::Dup => {
1370 let top = self.top()?.clone();
1371 self.charge_copy(&top.object)?;
1372 self.stack.push(top);
1373 }
1374
1375 OpCode::Get => {
1377 let idx = parse_index(&read_line(r)?)?;
1378 let entry = self.memo_get(idx)?;
1379 self.stack.push(entry);
1380 }
1381 OpCode::BinGet => {
1382 let idx = r.read_u8()? as u32;
1383 let entry = self.memo_get(idx)?;
1384 self.stack.push(entry);
1385 }
1386 OpCode::LongBinGet => {
1387 let idx = r.read_u32::<LittleEndian>()?;
1388 let entry = self.memo_get(idx)?;
1389 self.stack.push(entry);
1390 }
1391 OpCode::Put => {
1392 let idx = parse_index(&read_line(r)?)?;
1393 self.memo_put(idx)?;
1394 }
1395 OpCode::BinPut => {
1396 let idx = r.read_u8()? as u32;
1397 self.memo_put(idx)?;
1398 }
1399 OpCode::LongBinPut => {
1400 let idx = r.read_u32::<LittleEndian>()?;
1401 self.memo_put(idx)?;
1402 }
1403 OpCode::Memoize => {
1404 let idx = self.memo.len() as u32;
1405 self.memo_put(idx)?;
1406 }
1407
1408 OpCode::Global => {
1410 let module_name = read_line(r)?;
1411 let name = read_line(r)?;
1412 self.push_scalar(Object::Class { module_name, name });
1413 }
1414 OpCode::StackGlobal => {
1415 let name = self.pop_object()?;
1416 let module_name = self.pop_object()?;
1417 match (module_name, name) {
1418 (Object::String(module_name), Object::String(name)) => {
1419 self.push_scalar(Object::Class { module_name, name })
1420 }
1421 _ => {
1422 return Err(PickleError::InvalidData(
1423 "STACK_GLOBAL expects two strings".to_string(),
1424 ));
1425 }
1426 }
1427 }
1428 OpCode::PersId => {
1429 let pid = Object::String(read_line(r)?);
1430 let obj = resolve_persistent_id(pid, self.ids)?;
1431 self.push_scalar(obj);
1432 }
1433 OpCode::BinPersId => {
1434 let pid = self.pop_object()?;
1435 let obj = resolve_persistent_id(pid, self.ids)?;
1436 self.push_scalar(obj);
1437 }
1438 OpCode::Reduce => {
1439 let args = self.pop()?;
1440 let callable = self.pop_object()?;
1441 let obj = reduce(callable, args.object)?;
1443 self.push(obj, args.depth)?;
1444 }
1445 OpCode::NewObj => {
1446 let args = self.pop_object()?;
1447 let cls = self.pop_object()?;
1448 let obj = opaque(&cls, &args)?;
1449 self.push_scalar(obj);
1450 }
1451 OpCode::NewObjEx => {
1452 let kwargs = self.pop_object()?;
1453 let args = self.pop_object()?;
1454 let cls = self.pop_object()?;
1455 opaque(&cls, &kwargs)?;
1456 let obj = opaque(&cls, &args)?;
1457 self.push_scalar(obj);
1458 }
1459 OpCode::Build => {
1460 let state = self.pop()?;
1461 let object = self.pop()?;
1462 let memo = object.memo;
1463 let (built, depth) = match (object.object, state.object) {
1464 (Object::Dict(mut dict), Object::Dict(update)) => {
1465 dict.extend(update);
1466 (Object::Dict(dict), object.depth.max(state.depth))
1467 }
1468 (dict @ Object::Dict(_), state) => {
1471 if state.holds_tensor_data() {
1472 return Err(PickleError::UnsupportedType(
1473 "dict with tensor-bearing state".to_string(),
1474 ));
1475 }
1476 (dict, object.depth)
1477 }
1478 (Object::Opaque(_), Object::Dict(attrs)) if is_module_state(&attrs) => {
1481 (module_state_dict(attrs)?, state.depth)
1482 }
1483 (object, state) => (opaque(&object, &state)?, 0),
1484 };
1485 if let Some(idx) = memo {
1490 self.charge_copy(&built)?;
1491 self.memo.insert(
1492 idx,
1493 Entry {
1494 object: built.clone(),
1495 depth,
1496 memo: Some(idx),
1497 },
1498 );
1499 }
1500 self.push(built, depth)?;
1501 }
1502
1503 OpCode::Ext1
1504 | OpCode::Ext2
1505 | OpCode::Ext4
1506 | OpCode::NextBuffer
1507 | OpCode::ReadonlyBuffer => return Err(PickleError::UnsupportedOpCode(op)),
1508 }
1509 }
1510
1511 self.pop_object()
1512 }
1513}
1514
1515fn read_line<R: BufRead>(r: &mut R) -> Result<String> {
1516 let mut data = Vec::with_capacity(32);
1517 r.read_until(b'\n', &mut data)?;
1518 if data.pop() != Some(b'\n') {
1519 return Err(PickleError::Io(io::Error::new(
1520 io::ErrorKind::UnexpectedEof,
1521 "unterminated text field",
1522 )));
1523 }
1524 if data.last() == Some(&b'\r') {
1525 data.pop();
1526 }
1527 String::from_utf8(data).map_err(|e| PickleError::InvalidData(format!("Invalid UTF-8: {e}")))
1528}
1529
1530fn read_raw_unicode_escape_line<R: BufRead>(r: &mut R) -> Result<String> {
1533 let mut data = Vec::with_capacity(32);
1534 r.read_until(b'\n', &mut data)?;
1535 if data.pop() != Some(b'\n') {
1536 return Err(PickleError::Io(io::Error::new(
1537 io::ErrorKind::UnexpectedEof,
1538 "unterminated text field",
1539 )));
1540 }
1541
1542 let mut text = String::with_capacity(data.len());
1543 let mut i = 0;
1544 while i < data.len() {
1545 let byte = data[i];
1546 let escape_len = match (byte, data.get(i + 1)) {
1547 (b'\\', Some(b'u')) => Some(4),
1548 (b'\\', Some(b'U')) => Some(8),
1549 _ => None,
1550 };
1551 match escape_len {
1552 Some(digits) => {
1553 let hex = data
1554 .get(i + 2..i + 2 + digits)
1555 .and_then(|hex| std::str::from_utf8(hex).ok())
1556 .and_then(|hex| u32::from_str_radix(hex, 16).ok())
1557 .and_then(char::from_u32)
1558 .ok_or_else(|| {
1559 PickleError::InvalidData("Invalid raw-unicode-escape sequence".to_string())
1560 })?;
1561 text.push(hex);
1562 i += 2 + digits;
1563 }
1564 None => {
1565 text.push(char::from(byte));
1566 i += 1;
1567 }
1568 }
1569 }
1570 Ok(text)
1571}
1572
1573fn read_bytes<R: BufRead>(r: &mut R, len: u64) -> Result<Vec<u8>> {
1574 Ok(read_exact_len(r, len, STRING_PREALLOC_BOUND)?)
1575}
1576
1577fn read_string<R: BufRead>(r: &mut R, len: u64) -> Result<Object> {
1578 let s = String::from_utf8(read_bytes(r, len)?)
1579 .map_err(|e| PickleError::InvalidData(format!("Invalid UTF-8: {e}")))?;
1580 Ok(Object::String(s))
1581}
1582
1583fn read_py2_string<R: BufRead>(r: &mut R, len: u64) -> Result<Object> {
1586 Ok(match String::from_utf8(read_bytes(r, len)?) {
1587 Ok(text) => Object::String(text),
1588 Err(err) => Object::Bytes(err.into_bytes()),
1589 })
1590}
1591
1592fn parse_int(text: &str) -> Result<i64> {
1593 text.parse::<i64>()
1594 .map_err(|e| PickleError::InvalidData(format!("Invalid integer '{text}': {e}")))
1595}
1596
1597fn parse_index(text: &str) -> Result<u32> {
1598 text.parse::<u32>()
1599 .map_err(|e| PickleError::InvalidData(format!("Invalid memo index '{text}': {e}")))
1600}
1601
1602fn long_object(bytes: &[u8]) -> Object {
1609 int_from_le_bytes(bytes).map_or_else(|| Object::Opaque("int".to_string()), Object::Int)
1610}
1611
1612fn parse_long(text: &str) -> Result<Object> {
1615 use std::num::IntErrorKind;
1616
1617 match text.parse::<i64>() {
1618 Ok(value) => Ok(Object::Int(value)),
1619 Err(e)
1620 if matches!(
1621 e.kind(),
1622 IntErrorKind::PosOverflow | IntErrorKind::NegOverflow
1623 ) =>
1624 {
1625 Ok(Object::Opaque("int".to_string()))
1626 }
1627 Err(e) => Err(PickleError::InvalidData(format!(
1628 "Invalid integer '{text}': {e}"
1629 ))),
1630 }
1631}
1632
1633fn int_from_le_bytes(bytes: &[u8]) -> Option<i64> {
1635 let negative = bytes.last().is_some_and(|b| b & 0x80 != 0);
1636 let fill = if negative { 0xff } else { 0x00 };
1637 let fits = bytes.len() <= 8
1640 || (bytes[8..].iter().all(|&b| b == fill) && (bytes[7] & 0x80 != 0) == negative);
1641 if !fits {
1642 return None;
1643 }
1644 let mut buf = [fill; 8];
1645 let len = bytes.len().min(8);
1646 buf[..len].copy_from_slice(&bytes[..len]);
1647 Some(i64::from_le_bytes(buf))
1648}
1649
1650fn dict_key(key: Object) -> Result<String> {
1651 match key {
1652 Object::String(s) => Ok(s),
1653 other => key_string(&other, "dict key"),
1654 }
1655}
1656
1657fn insert_pairs(dict: &mut HashMap<String, Object>, items: Vec<Object>) -> Result<()> {
1658 if !items.len().is_multiple_of(2) {
1659 return Err(PickleError::InvalidData(
1660 "dict items must come in key/value pairs".to_string(),
1661 ));
1662 }
1663 let mut items = items.into_iter();
1664 while let (Some(key), Some(value)) = (items.next(), items.next()) {
1665 dict.insert(dict_key(key)?, value);
1666 }
1667 Ok(())
1668}
1669
1670pub(crate) fn extract_tensors(dict: HashMap<String, Object>) -> HashMap<String, Tensor> {
1683 fn walk(obj: Object, path: &mut Vec<String>, tensors: &mut HashMap<String, Tensor>) {
1684 match obj {
1685 Object::Dict(dict) => {
1686 for (key, value) in dict {
1687 path.push(key);
1688 walk(value, path, tensors);
1689 path.pop();
1690 }
1691 }
1692 Object::List(items) | Object::Tuple(items) => {
1693 for (index, item) in items.into_iter().enumerate() {
1694 path.push(index.to_string());
1695 walk(item, path, tensors);
1696 path.pop();
1697 }
1698 }
1699 Object::Tensor(mut tensor) => {
1700 tensor.name = path.join(".");
1701 tensors.insert(tensor.name.clone(), tensor);
1702 }
1703 _ => {}
1704 }
1705 }
1706 let mut tensors = HashMap::new();
1707 walk(Object::Dict(dict), &mut Vec::new(), &mut tensors);
1708 tensors
1709}
1710
1711#[cfg(test)]
1712mod tests {
1713 use super::*;
1714 use crate::tests::{read_as, test_data_path};
1715 use std::io::Cursor;
1716
1717 fn fixture_source() -> Arc<StorageSource> {
1718 let path = test_data_path("non_contiguous.pt");
1719 Arc::new(StorageSource::Zip(
1720 crate::storage::ZipSource::open(&path).unwrap(),
1721 ))
1722 }
1723
1724 fn storage_pid(storage_type: &str, key: &str, numel: i64) -> Object {
1725 Object::Tuple(vec![
1726 Object::String("storage".to_string()),
1727 Object::Class {
1728 module_name: "torch".to_string(),
1729 name: storage_type.to_string(),
1730 },
1731 Object::String(key.to_string()),
1732 Object::String("cpu".to_string()),
1733 Object::Int(numel),
1734 ])
1735 }
1736
1737 fn rebuild_args(
1738 storage_type: &str,
1739 key: &str,
1740 numel: i64,
1741 offset: i64,
1742 shape: &[i64],
1743 stride: &[i64],
1744 source: &Arc<StorageSource>,
1745 ) -> Object {
1746 let storage = match &storage_pid(storage_type, key, numel) {
1747 Object::Tuple(fields) => resolve_storage_id(fields, source).unwrap(),
1748 _ => unreachable!(),
1749 };
1750 Object::Tuple(vec![
1751 Object::Storage(storage),
1752 Object::Int(offset),
1753 Object::Tuple(shape.iter().copied().map(Object::Int).collect()),
1754 Object::Tuple(stride.iter().copied().map(Object::Int).collect()),
1755 ])
1756 }
1757
1758 fn rebuild(args: Object) -> Result<Tensor> {
1759 match rebuild_tensor(args, TensorRebuild::Legacy)? {
1760 Object::Tensor(tensor) => Ok(tensor),
1761 other => panic!("expected tensor, got {other:?}"),
1762 }
1763 }
1764
1765 fn plain(bytes: &[u8]) -> Result<Object> {
1766 read_pickle(&mut Cursor::new(bytes), &PersistentIds::Unavailable)
1767 }
1768
1769 #[test]
1770 fn test_memo_bomb_mitigation() {
1771 let mut poc = vec![0x89, b'q', 0x00];
1773 let n = 20; for _ in 0..n {
1775 poc.extend_from_slice(&[b'h', 0x00, b'h', 0x00, 0x86, b'q', 0x00]);
1776 }
1777 poc.push(b'.');
1778
1779 let err = plain(&poc).unwrap_err();
1780 assert!(matches!(err, PickleError::InvalidData(msg) if msg.contains("memo bomb")));
1781 }
1782
1783 #[test]
1784 fn memo_copies_of_large_payloads_are_bounded() {
1785 let payload = vec![0xabu8; 2 << 20];
1787 let mut bytes = vec![0x80, 0x02, b'B'];
1788 bytes.extend_from_slice(&(payload.len() as u32).to_le_bytes());
1789 bytes.extend_from_slice(&payload);
1790 bytes.extend_from_slice(b"q\x00(");
1791 for _ in 0..40 {
1792 bytes.extend_from_slice(b"h\x00");
1793 }
1794 bytes.extend_from_slice(b"t.");
1795 assert!(matches!(
1796 plain(&bytes).unwrap_err(),
1797 PickleError::InvalidData(msg) if msg.contains("memo bomb")
1798 ));
1799 }
1800
1801 #[test]
1802 fn dup_doubling_is_bounded() {
1803 let mut bytes = vec![0x80, 0x02, b'K', 0x01];
1805 for _ in 0..40 {
1806 bytes.extend_from_slice(b"2\x86");
1807 }
1808 bytes.push(b'.');
1809 assert!(matches!(
1810 plain(&bytes).unwrap_err(),
1811 PickleError::InvalidData(msg) if msg.contains("memo bomb")
1812 ));
1813 }
1814
1815 #[test]
1816 fn unicode_opcode_decodes_raw_unicode_escape() {
1817 let bytes = b"V\xe9t\xe9 \\u6a21\np0\n.";
1819 assert!(matches!(
1820 plain(bytes).unwrap(),
1821 Object::String(s) if s == "\u{e9}t\u{e9} \u{6a21}"
1822 ));
1823 assert!(plain(b"V\\u12.\n.").is_err());
1824 }
1825
1826 #[test]
1827 fn rebuild_from_type_v2_rejects_tensor_bearing_state() {
1828 let source = fixture_source();
1829 let callable = Object::Class {
1830 module_name: "torch._tensor".to_string(),
1831 name: "_rebuild_from_type_v2".to_string(),
1832 };
1833 let mut state = HashMap::new();
1834 state.insert(
1835 "extra".to_string(),
1836 rebuild_args("FloatStorage", "0", 32, 0, &[2], &[1], &source),
1837 );
1838 let args = Object::Tuple(vec![
1839 Object::Class {
1840 module_name: "torch._utils".to_string(),
1841 name: "_rebuild_tensor".to_string(),
1842 },
1843 Object::Class {
1844 module_name: "torch".to_string(),
1845 name: "Tensor".to_string(),
1846 },
1847 rebuild_args("FloatStorage", "0", 32, 5, &[2, 3], &[3, 1], &source),
1848 Object::Dict(state),
1849 ]);
1850 assert!(matches!(
1851 reduce(callable, args).unwrap_err(),
1852 PickleError::UnsupportedType(_)
1853 ));
1854 }
1855
1856 #[test]
1857 fn protocol_4_pickle_parses() {
1858 let bytes: &[u8] = &[
1860 0x80, 0x04, 0x95, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7d, 0x94, 0x28,
1861 0x8c, 0x01, 0x61, 0x94, 0x4b, 0x01, 0x8c, 0x01, 0x62, 0x94, 0x5d, 0x94, 0x28, 0x47,
1862 0x3f, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x88, 0x4e, 0x65, 0x8c, 0x01, 0x63,
1863 0x94, 0x43, 0x02, 0x78, 0x79, 0x94, 0x8c, 0x01, 0x64, 0x94, 0x7d, 0x94, 0x8c, 0x01,
1864 0x65, 0x94, 0x8c, 0x01, 0x66, 0x94, 0x73, 0x75, 0x2e,
1865 ];
1866 let Object::Dict(dict) = plain(bytes).unwrap() else {
1867 panic!("expected dict");
1868 };
1869 assert!(matches!(dict["a"], Object::Int(1)));
1870 let Object::List(items) = &dict["b"] else {
1871 panic!("expected list");
1872 };
1873 assert!(
1874 matches!(items[..], [Object::Float(f), Object::Bool(true), Object::None] if f == 1.5)
1875 );
1876 assert!(matches!(&dict["c"], Object::Bytes(b) if b == b"xy"));
1877 let Object::Dict(inner) = &dict["d"] else {
1878 panic!("expected dict");
1879 };
1880 assert!(matches!(&inner["e"], Object::String(s) if s == "f"));
1881 }
1882
1883 #[test]
1884 fn stack_global_builds_class() {
1885 let bytes: &[u8] = &[
1887 0x80, 0x04, 0x95, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8c, 0x0b, b'c',
1888 b'o', b'l', b'l', b'e', b'c', b't', b'i', b'o', b'n', b's', 0x94, 0x8c, 0x0b, b'O',
1889 b'r', b'd', b'e', b'r', b'e', b'd', b'D', b'i', b'c', b't', 0x94, 0x93, 0x94, 0x2e,
1890 ];
1891 assert!(matches!(
1892 plain(bytes).unwrap(),
1893 Object::Class { module_name, name } if module_name == "collections" && name == "OrderedDict"
1894 ));
1895 }
1896
1897 #[test]
1898 fn protocol_0_text_opcodes_parse() {
1899 let bytes =
1901 b"(dp0\nVn\np1\nI-3\nsVf\np2\nF2.5\nsVt\np3\nI01\nsVbig\np4\nL1099511627776L\ns.";
1902 let Object::Dict(dict) = plain(bytes).unwrap_or_else(|e| panic!("{e}")) else {
1903 panic!("expected dict");
1904 };
1905 assert!(matches!(dict["n"], Object::Int(-3)));
1906 assert!(matches!(dict["f"], Object::Float(f) if f == 2.5));
1907 assert!(matches!(dict["t"], Object::Bool(true)));
1908 assert!(matches!(dict["big"], Object::Int(1099511627776)));
1909 }
1910
1911 #[test]
1912 fn long1_handles_sign_and_width() {
1913 assert_eq!(int_from_le_bytes(&[]).unwrap(), 0);
1914 assert_eq!(int_from_le_bytes(&[0xff]).unwrap(), -1);
1915 assert_eq!(int_from_le_bytes(&[0x00, 0x01]).unwrap(), 256);
1916 assert_eq!(
1917 int_from_le_bytes(&[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0x00]).unwrap(),
1918 i64::MAX
1919 );
1920 assert!(
1921 int_from_le_bytes(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01]).is_none()
1922 );
1923 }
1924
1925 #[test]
1926 fn oversized_int_beside_weights_stays_opaque() {
1927 for bytes in [
1929 b"\x80\x02}q\x00(X\x04\x00\x00\x00seedq\x01\x8a\t\x00\x00\x00\x00\x00\x00\x00\x00\x01X\x01\x00\x00\x00nq\x02K\x07u.".as_slice(),
1930 b"(dp0\nVseed\np1\nL18446744073709551616L\nsVn\np2\nI7\ns.".as_slice(),
1931 ] {
1932 let Object::Dict(dict) = plain(bytes).unwrap_or_else(|e| panic!("{e}")) else {
1933 panic!("expected dict");
1934 };
1935 assert!(matches!(&dict["seed"], Object::Opaque(name) if name == "int"));
1936 assert!(matches!(dict["n"], Object::Int(7)));
1937 }
1938 }
1939
1940 #[test]
1941 fn a_long_that_is_not_a_number_is_an_error() {
1942 assert!(plain(b"Lnope\n.").is_err());
1944 }
1945
1946 #[test]
1947 fn truncated_string_length_does_not_allocate() {
1948 let bytes: &[u8] = &[0x80, 0x02, b'X', 0xff, 0xff, 0xff, 0xff, b'h', b'i', b'.'];
1950 assert!(matches!(plain(bytes).unwrap_err(), PickleError::Io(_)));
1951 }
1952
1953 #[test]
1954 fn unknown_reduce_is_kept_opaque() {
1955 let bytes = b"\x80\x02cnumpy.core.multiarray\nscalar\nU\x02f8U\x08\x00\x00\x00\x00\x00\x00\xe0?\x86R.";
1957 assert!(matches!(
1958 plain(bytes).unwrap(),
1959 Object::Opaque(name) if name == "numpy.core.multiarray.scalar"
1960 ));
1961 }
1962
1963 #[test]
1964 fn unknown_call_consuming_tensor_data_is_an_error() {
1965 let source = fixture_source();
1966 let callable = Object::Class {
1967 module_name: "torch._utils".to_string(),
1968 name: "_rebuild_qtensor".to_string(),
1969 };
1970 let args = rebuild_args("FloatStorage", "0", 32, 0, &[2], &[1], &source);
1971 assert!(matches!(
1972 reduce(callable, args).unwrap_err(),
1973 PickleError::UnsupportedType(name) if name == "torch._utils._rebuild_qtensor"
1974 ));
1975 }
1976
1977 #[test]
1978 fn persistent_id_without_source_is_an_error() {
1979 let bytes = b"\x80\x02U\x07storageQ.";
1980 assert!(matches!(
1981 plain(bytes).unwrap_err(),
1982 PickleError::NoDataSource
1983 ));
1984 }
1985
1986 #[test]
1987 fn int_dict_keys_are_accepted_everywhere() {
1988 assert!(
1990 matches!(plain(b"\x80\x02}K\x01K\x02s.").unwrap(), Object::Dict(d) if d.contains_key("1"))
1991 );
1992 assert!(
1993 matches!(plain(b"\x80\x02(K\x03K\x04d.").unwrap(), Object::Dict(d) if d.contains_key("3"))
1994 );
1995 }
1996
1997 #[test]
1998 fn stride_rejects_negative_values() {
1999 let negative_stride = Object::Tuple(vec![Object::Int(-1)]);
2000 assert!(matches!(
2001 parse_dims(&negative_stride, "stride"),
2002 Err(PickleError::InvalidData(msg)) if msg == "stride must be non-negative, got -1"
2003 ));
2004 }
2005
2006 #[test]
2007 fn storage_extent_rejects_rank_mismatch() {
2008 assert!(matches!(
2009 tensor_storage_extent(&[2, 3], &[3], 0),
2010 Err(PickleError::InvalidData(msg))
2011 if msg == "Tensor stride rank 1 does not match shape rank 2"
2012 ));
2013 }
2014
2015 #[test]
2016 fn storage_extent_rejects_overflow() {
2017 assert!(matches!(
2018 tensor_storage_extent(&[usize::MAX], &[2], 0),
2019 Err(PickleError::InvalidData(msg))
2020 if msg == "Tensor stride calculation overflows usize"
2021 ));
2022 }
2023
2024 #[test]
2025 fn strided_views_match_reference() {
2026 type Case = (
2032 &'static str,
2033 &'static [usize],
2034 &'static [usize],
2035 usize,
2036 &'static [f32],
2037 );
2038 let cases: &[Case] = &[
2039 (
2040 "channels_last",
2041 &[2, 3, 2, 2][..],
2042 &[12, 1, 6, 3][..],
2043 0,
2044 &[
2045 0.0, 3.0, 6.0, 9.0, 1.0, 4.0, 7.0, 10.0, 2.0, 5.0, 8.0, 11.0, 12.0, 15.0, 18.0,
2046 21.0, 13.0, 16.0, 19.0, 22.0, 14.0, 17.0, 20.0, 23.0,
2047 ][..],
2048 ),
2049 (
2050 "transposed",
2051 &[3, 4][..],
2052 &[1, 3][..],
2053 2,
2054 &[
2055 2.0, 5.0, 8.0, 11.0, 3.0, 6.0, 9.0, 12.0, 4.0, 7.0, 10.0, 13.0,
2056 ][..],
2057 ),
2058 (
2059 "step_slice",
2060 &[5][..],
2061 &[2][..],
2062 1,
2063 &[1.0, 3.0, 5.0, 7.0, 9.0][..],
2064 ),
2065 (
2066 "overlapping_windows",
2067 &[4, 3][..],
2068 &[1, 1][..],
2069 0,
2070 &[0.0, 1.0, 2.0, 1.0, 2.0, 3.0, 2.0, 3.0, 4.0, 3.0, 4.0, 5.0][..],
2071 ),
2072 (
2073 "size_one_dim_odd_stride",
2074 &[1, 3][..],
2075 &[12, 1][..],
2076 4,
2077 &[4.0, 5.0, 6.0][..],
2078 ),
2079 (
2080 "size_one_inner_dim",
2081 &[3, 1][..],
2082 &[1, 7][..],
2083 0,
2084 &[0.0, 1.0, 2.0][..],
2085 ),
2086 (
2087 "expand_inner",
2088 &[2, 3, 2][..],
2089 &[0, 1, 0][..],
2090 3,
2091 &[3.0, 3.0, 4.0, 4.0, 5.0, 5.0, 3.0, 3.0, 4.0, 4.0, 5.0, 5.0][..],
2092 ),
2093 ("empty_leading", &[0, 3][..], &[3, 1][..], 30, &[][..]),
2094 (
2095 "empty_trailing_at_end",
2096 &[2, 0][..],
2097 &[0, 1][..],
2098 32,
2099 &[][..],
2100 ),
2101 ];
2102 let source = fixture_source();
2103 for &(name, shape, stride, offset, expected) in cases {
2104 let to_i64 = |v: &[usize]| v.iter().map(|&x| x as i64).collect::<Vec<_>>();
2105 let tensor = rebuild(rebuild_args(
2106 "FloatStorage",
2107 "0",
2108 32,
2109 offset as i64,
2110 &to_i64(shape),
2111 &to_i64(stride),
2112 &source,
2113 ))
2114 .unwrap_or_else(|e| panic!("{name}: {e}"));
2115 let bytes = tensor.read().unwrap_or_else(|e| panic!("{name}: {e}"));
2116 let data: Vec<f32> = bytemuck::pod_collect_to_vec(&bytes);
2117 assert_eq!(data, expected, "{name}");
2118 }
2119 }
2120
2121 #[test]
2122 fn strided_views_for_other_element_sizes() {
2123 let source = fixture_source();
2126
2127 let tensor = rebuild(rebuild_args(
2128 "ShortStorage",
2129 "0",
2130 64,
2131 1,
2132 &[2, 3],
2133 &[1, 2],
2134 &source,
2135 ))
2136 .unwrap();
2137 assert_eq!(read_as::<i16>(&tensor), [0, 16256, 16384, 0, 0, 0]);
2138
2139 let tensor = rebuild(rebuild_args(
2140 "ByteStorage",
2141 "0",
2142 128,
2143 2,
2144 &[2, 4],
2145 &[1, 4],
2146 &source,
2147 ))
2148 .unwrap();
2149 assert_eq!(read_as::<u8>(&tensor), [0, 128, 0, 64, 0, 63, 64, 64]);
2150
2151 let tensor = rebuild(rebuild_args(
2152 "DoubleStorage",
2153 "0",
2154 16,
2155 0,
2156 &[2, 2],
2157 &[1, 2],
2158 &source,
2159 ))
2160 .unwrap();
2161 let bits: Vec<u64> = read_as::<f64>(&tensor)
2162 .iter()
2163 .map(|v| v.to_bits())
2164 .collect();
2165 assert_eq!(
2166 bits,
2167 vec![
2168 4575657221408423936,
2169 4656722015783223296,
2170 4629700418010611712,
2171 4674736414296899584
2172 ]
2173 );
2174 }
2175
2176 #[test]
2177 fn stride_rejects_non_integer_values() {
2178 let invalid_stride = Object::Tuple(vec![Object::String("one".to_string())]);
2179 assert!(matches!(
2180 parse_dims(&invalid_stride, "stride"),
2181 Err(PickleError::InvalidData(msg)) if msg == "stride must be an int, got str"
2182 ));
2183 }
2184
2185 #[test]
2186 fn view_beyond_declared_storage_fails_at_parse_time() {
2187 let source = fixture_source();
2190 let err = rebuild(rebuild_args(
2191 "FloatStorage",
2192 "0",
2193 32,
2194 30,
2195 &[2, 3],
2196 &[3, 1],
2197 &source,
2198 ))
2199 .unwrap_err();
2200 assert!(matches!(
2201 err,
2202 PickleError::InvalidData(msg) if msg.contains("needs 36 elements, but storage '0' declares 32")
2203 ));
2204 }
2205
2206 #[test]
2207 fn declared_storage_larger_than_file_fails_at_read_time() {
2208 let source = fixture_source();
2209 let tensor = rebuild(rebuild_args(
2210 "FloatStorage",
2211 "0",
2212 64,
2213 40,
2214 &[2, 3],
2215 &[3, 1],
2216 &source,
2217 ))
2218 .unwrap();
2219 let err = tensor.read().unwrap_err();
2220 assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
2221 assert!(
2222 err.to_string()
2223 .contains("requires 46 elements from storage '0', but only 32 are available"),
2224 "{err}"
2225 );
2226 }
2227
2228 #[test]
2229 fn broadcast_view_above_limit_is_refused() {
2230 let source = fixture_source();
2231 let err = rebuild(rebuild_args(
2232 "FloatStorage",
2233 "0",
2234 32,
2235 0,
2236 &[1 << 21, 1 << 21],
2237 &[0, 0],
2238 &source,
2239 ))
2240 .unwrap_err();
2241 assert!(matches!(
2242 err,
2243 PickleError::InvalidData(msg) if msg.contains("byte limit")
2244 ));
2245 }
2246
2247 #[test]
2248 fn small_broadcast_view_loads() {
2249 let source = fixture_source();
2250 let tensor = rebuild(rebuild_args(
2251 "FloatStorage",
2252 "0",
2253 32,
2254 1,
2255 &[2, 3],
2256 &[0, 1],
2257 &source,
2258 ))
2259 .unwrap();
2260 assert_eq!(read_as::<f32>(&tensor), [1.0, 2.0, 3.0, 1.0, 2.0, 3.0]);
2261 }
2262
2263 #[test]
2264 fn legacy_rebuild_tensor_loads_contiguous_stride() {
2265 let source = fixture_source();
2266 let tensor = rebuild(rebuild_args(
2267 "FloatStorage",
2268 "0",
2269 32,
2270 5,
2271 &[2, 3],
2272 &[3, 1],
2273 &source,
2274 ))
2275 .unwrap();
2276 assert_eq!(read_as::<f32>(&tensor), [5.0, 6.0, 7.0, 8.0, 9.0, 10.0]);
2277 }
2278
2279 #[test]
2280 fn legacy_rebuild_tensor_loads_permuted_stride() {
2281 let source = fixture_source();
2282 let tensor = rebuild(rebuild_args(
2283 "FloatStorage",
2284 "0",
2285 32,
2286 5,
2287 &[2, 4, 3],
2288 &[12, 1, 4],
2289 &source,
2290 ))
2291 .unwrap();
2292 assert_eq!(
2293 read_as::<f32>(&tensor),
2294 [
2295 5.0, 9.0, 13.0, 6.0, 10.0, 14.0, 7.0, 11.0, 15.0, 8.0, 12.0, 16.0, 17.0, 21.0,
2296 25.0, 18.0, 22.0, 26.0, 19.0, 23.0, 27.0, 20.0, 24.0, 28.0,
2297 ]
2298 );
2299 }
2300
2301 #[test]
2302 fn legacy_rebuild_tensor_loads_scalar_at_offset() {
2303 let source = fixture_source();
2304 let tensor = rebuild(rebuild_args("FloatStorage", "0", 32, 5, &[], &[], &source)).unwrap();
2305 assert_eq!(read_as::<f32>(&tensor), [5.0]);
2306 }
2307
2308 #[test]
2309 fn storage_view_offset_is_applied() {
2310 let source = fixture_source();
2311 let mut pid = match storage_pid("FloatStorage", "0", 32) {
2312 Object::Tuple(fields) => fields,
2313 _ => unreachable!(),
2314 };
2315 pid.push(Object::Tuple(vec![
2316 Object::String("7".to_string()),
2317 Object::Int(4),
2318 Object::Int(10),
2319 ]));
2320 let storage = resolve_storage_id(&pid, &source).unwrap();
2321 assert_eq!(storage.view_offset, 4);
2322 let tensor = build_tensor(storage, DType::F32, 1, vec![3], vec![1]).unwrap();
2323 assert_eq!(read_as::<f32>(&tensor), [5.0, 6.0, 7.0]);
2324 }
2325
2326 #[test]
2327 fn rebuild_v3_reads_untyped_storage_with_explicit_dtype() {
2328 let source = fixture_source();
2329 let pid = [
2330 Object::String("storage".to_string()),
2331 Object::Class {
2332 module_name: "torch".to_string(),
2333 name: "UntypedStorage".to_string(),
2334 },
2335 Object::String("0".to_string()),
2336 Object::String("cpu".to_string()),
2337 Object::Int(128),
2338 ];
2339 let storage = resolve_storage_id(&pid, &source).unwrap();
2340 assert_eq!(storage.dtype, None);
2341 let args = Object::Tuple(vec![
2342 Object::Storage(storage),
2343 Object::Int(0),
2344 Object::Tuple(vec![Object::Int(2)]),
2345 Object::Tuple(vec![Object::Int(1)]),
2346 Object::Bool(false),
2347 Object::None,
2348 Object::Class {
2349 module_name: "torch".to_string(),
2350 name: "uint32".to_string(),
2351 },
2352 ]);
2353 let Object::Tensor(tensor) = rebuild_tensor(args, TensorRebuild::V3).unwrap() else {
2354 panic!("expected tensor");
2355 };
2356 assert_eq!(tensor.dtype(), DType::U32);
2357 assert_eq!(read_as::<u32>(&tensor), [0, 1.0f32.to_bits()]);
2359 }
2360
2361 #[test]
2362 fn rebuild_from_type_v2_dispatches_inner_call() {
2363 let source = fixture_source();
2365 let callable = Object::Class {
2366 module_name: "torch._tensor".to_string(),
2367 name: "_rebuild_from_type_v2".to_string(),
2368 };
2369 let args = Object::Tuple(vec![
2370 Object::Class {
2371 module_name: "torch._utils".to_string(),
2372 name: "_rebuild_tensor".to_string(),
2373 },
2374 Object::Class {
2375 module_name: "torch".to_string(),
2376 name: "Tensor".to_string(),
2377 },
2378 rebuild_args("FloatStorage", "0", 32, 5, &[2, 3], &[3, 1], &source),
2379 Object::Dict(HashMap::new()),
2380 ]);
2381 let Object::Tensor(tensor) = reduce(callable, args).unwrap() else {
2382 panic!("expected tensor");
2383 };
2384 assert_eq!(read_as::<f32>(&tensor), [5.0, 6.0, 7.0, 8.0, 9.0, 10.0]);
2385 }
2386
2387 #[test]
2388 fn bool_bytes_are_normalized() {
2389 let source = fixture_source();
2391 let pid = storage_pid("BoolStorage", "0", 128);
2392 let Object::Tuple(pid) = pid else {
2393 unreachable!()
2394 };
2395 let storage = resolve_storage_id(&pid, &source).unwrap();
2396 let tensor = build_tensor(storage, DType::Bool, 0, vec![8], vec![1]).unwrap();
2397 assert_eq!(tensor.read().unwrap(), [0, 0, 0, 0, 0, 0, 1, 1]);
2398 }
2399
2400 #[test]
2401 fn untyped_storage_with_view_metadata_is_rejected() {
2402 let source = fixture_source();
2403 let pid = [
2404 Object::String("storage".to_string()),
2405 Object::Class {
2406 module_name: "torch".to_string(),
2407 name: "UntypedStorage".to_string(),
2408 },
2409 Object::String("0".to_string()),
2410 Object::String("cpu".to_string()),
2411 Object::Int(128),
2412 Object::Tuple(vec![
2413 Object::String("7".to_string()),
2414 Object::Int(4),
2415 Object::Int(10),
2416 ]),
2417 ];
2418 assert!(matches!(
2419 resolve_storage_id(&pid, &source),
2420 Err(PickleError::InvalidData(msg)) if msg.contains("untyped but carries view metadata")
2421 ));
2422 }
2423
2424 #[test]
2425 fn long_at_i64_boundary_does_not_fit() {
2426 assert!(int_from_le_bytes(&[0, 0, 0, 0, 0, 0, 0, 0x80, 0]).is_none());
2428 assert!(
2429 int_from_le_bytes(&[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0xff]).is_none()
2430 );
2431 assert_eq!(
2432 int_from_le_bytes(&[0, 0, 0, 0, 0, 0, 0, 0x80, 0xff]).unwrap(),
2433 i64::MIN
2434 );
2435 }
2436
2437 #[test]
2438 fn deep_nesting_is_rejected() {
2439 let mut bytes = vec![0x80, 0x02, b']'];
2441 bytes.extend(std::iter::repeat_n(0x85, 1500));
2442 bytes.push(b'.');
2443 assert!(matches!(
2444 plain(&bytes).unwrap_err(),
2445 PickleError::InvalidData(msg) if msg.contains("nesting exceeds")
2446 ));
2447
2448 let mut bytes = vec![0x80, 0x02, b']'];
2450 bytes.extend(std::iter::repeat_n(0x85, 900));
2451 bytes.push(b'.');
2452 assert!(plain(&bytes).is_ok());
2453 }
2454
2455 #[test]
2456 fn mark_never_becomes_a_value() {
2457 assert!(matches!(
2459 plain(b"\x80\x02(\x85.").unwrap_err(),
2460 PickleError::InvalidData(msg) if msg.contains("found MARK")
2461 ));
2462 assert!(plain(b"\x80\x02(q\x00.").is_err());
2464 assert!(plain(b"\x80\x02K\x01(.").is_err());
2466 assert!(matches!(
2468 plain(b"\x80\x02K\x01(0.").unwrap(),
2469 Object::Int(1)
2470 ));
2471 }
2472
2473 #[test]
2474 fn build_on_a_tensor_is_an_error() {
2475 let source = fixture_source();
2476 let tensor = rebuild(rebuild_args(
2477 "FloatStorage",
2478 "0",
2479 32,
2480 0,
2481 &[2],
2482 &[1],
2483 &source,
2484 ))
2485 .unwrap();
2486 let err = opaque(&Object::Tensor(tensor), &Object::Dict(HashMap::new())).unwrap_err();
2487 assert!(matches!(err, PickleError::UnsupportedType(_)));
2488 }
2489
2490 const MODULE_PICKLE_PROTO2: &[u8] = b"\x80\x02c__main__\nM\nq\x00)\x81q\x01}q\x02(X\x0b\x00\x00\x00_parametersq\x03}q\x04X\x04\x00\x00\x00biasq\x05NsX\x08\x00\x00\x00_buffersq\x06}q\x07(X\x07\x00\x00\x00runningq\x08K\x02X\x04\x00\x00\x00maskq\x09K\x03uX\x1b\x00\x00\x00_non_persistent_buffers_setq\x0ac__builtin__\nset\nq\x0b]q\x0ch\x09a\x85q\x0dRq\x0eX\x08\x00\x00\x00_modulesq\x0f}q\x10(X\x02\x00\x00\x00fcq\x11h\x00)\x81q\x12}q\x13(h\x03}q\x14X\x06\x00\x00\x00weightq\x15K\x01sh\x06}q\x16h\x0f}q\x17ubX\x04\x00\x00\x00goneq\x18NuX\x08\x00\x00\x00trainingq\x19\x88X\x05\x00\x00\x00scaleq\x1aK\x04ub.";
2496 const MODULE_PICKLE_PROTO4: &[u8] = b"\x80\x04\x95\xcf\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x01M\x94\x93\x94)\x81\x94}\x94(\x8c\x0b_parameters\x94}\x94\x8c\x04bias\x94Ns\x8c\x08_buffers\x94}\x94(\x8c\x07running\x94K\x02\x8c\x04mask\x94K\x03u\x8c\x1b_non_persistent_buffers_set\x94\x8f\x94(h\x0b\x90\x8c\x08_modules\x94}\x94(\x8c\x02fc\x94h\x02)\x81\x94}\x94(h\x05}\x94\x8c\x06weight\x94K\x01sh\x08}\x94h\x0e}\x94ub\x8c\x04gone\x94Nu\x8c\x08training\x94\x88\x8c\x05scale\x94K\x04ub.";
2497
2498 #[test]
2499 fn module_build_becomes_its_state_dict() {
2500 for bytes in [MODULE_PICKLE_PROTO2, MODULE_PICKLE_PROTO4] {
2501 let Object::Dict(dict) = plain(bytes).unwrap_or_else(|e| panic!("{e}")) else {
2502 panic!("expected dict");
2503 };
2504 let mut keys: Vec<_> = dict.keys().cloned().collect();
2506 keys.sort();
2507 assert_eq!(keys, ["fc", "running"]);
2508 assert!(matches!(dict["running"], Object::Int(2)));
2509 let Object::Dict(child) = &dict["fc"] else {
2510 panic!("expected the child module as a dict");
2511 };
2512 assert_eq!(child.len(), 1);
2513 assert!(matches!(child["weight"], Object::Int(1)));
2514 }
2515 }
2516
2517 #[test]
2518 fn module_shared_under_two_names_is_built_under_both() {
2519 let bytes = b"\x80\x02}q\x00(X\x05\x00\x00\x00modelq\x01c__main__\nM\nq\x02)\x81q\x03}q\x04(X\x0b\x00\x00\x00_parametersq\x05}q\x06X\x08\x00\x00\x00_buffersq\x07}q\x08X\x08\x00\x00\x00_modulesq\x09}q\n(X\x01\x00\x00\x00aq\x0bh\x02)\x81q\x0c}q\x0d(h\x05}q\x0eX\x06\x00\x00\x00weightq\x0fK\x01sh\x07}q\x10h\x09}q\x11ubX\x01\x00\x00\x00bq\x12h\x0cuubX\x03\x00\x00\x00emaq\x13h\x03u.";
2523 let Object::Dict(checkpoint) = plain(bytes).unwrap_or_else(|e| panic!("{e}")) else {
2524 panic!("expected dict");
2525 };
2526 for key in ["model", "ema"] {
2527 let Object::Dict(root) = &checkpoint[key] else {
2528 panic!("expected {key} as a dict, got {:?}", checkpoint[key]);
2529 };
2530 for child in ["a", "b"] {
2531 assert!(
2532 matches!(&root[child], Object::Dict(d) if matches!(d["weight"], Object::Int(1))),
2533 "{key}.{child}: {:?}",
2534 root[child]
2535 );
2536 }
2537 }
2538 }
2539
2540 #[test]
2541 fn object_with_a_tensor_attribute_is_refused() {
2542 let bytes = b"\x80\x02c__main__\nC\nq\x00)\x81q\x01}q\x02X\x01\x00\x00\x00wq\x03ctorch._utils\n_rebuild_tensor_v2\nq\x04((X\x07\x00\x00\x00storageq\x05ctorch\nFloatStorage\nq\x06X\x01\x00\x00\x000q\x07X\x03\x00\x00\x00cpuq\x08K tq\x09QK\x00K\x02\x85q\nK\x01\x85q\x0b\x89ccollections\nOrderedDict\nq\x0c)Rq\x0dtq\x0eRq\x0fsb.";
2545 let ids = PersistentIds::Storages(fixture_source());
2546 assert!(matches!(
2547 read_pickle(&mut Cursor::new(&bytes[..]), &ids),
2548 Err(PickleError::UnsupportedType(name)) if name == "__main__.C"
2549 ));
2550 }
2551
2552 #[test]
2553 fn object_without_module_tables_stays_opaque() {
2554 let bytes = b"\x80\x02c__main__\nM\nq\x00)\x81q\x01}q\x02X\x01\x00\x00\x00xq\x03K\x01sb.";
2556 assert!(matches!(
2557 plain(bytes).unwrap(),
2558 Object::Opaque(name) if name == "__main__.M"
2559 ));
2560 }
2561
2562 #[test]
2563 fn module_registering_a_name_twice_is_an_error() {
2564 let table = |value| Object::Dict(HashMap::from([("w".to_string(), value)]));
2565 let attrs = HashMap::from([
2566 ("_parameters".to_string(), table(Object::Int(1))),
2567 ("_buffers".to_string(), table(Object::Int(2))),
2568 ("_modules".to_string(), Object::Dict(HashMap::new())),
2569 ]);
2570 assert!(matches!(
2571 module_state_dict(attrs),
2572 Err(PickleError::InvalidData(msg)) if msg.contains("'w' more than once")
2573 ));
2574 }
2575
2576 #[test]
2577 fn module_state_dict_rejects_a_malformed_buffer_set() {
2578 let mut attrs =
2579 HashMap::from([("_non_persistent_buffers_set".to_string(), Object::Int(1))]);
2580 for table in MODULE_TABLES {
2581 attrs.insert(table.to_string(), Object::Dict(HashMap::new()));
2582 }
2583 assert!(matches!(
2584 module_state_dict(attrs),
2585 Err(PickleError::InvalidData(msg)) if msg.contains("must be a set, got int")
2586 ));
2587 }
2588
2589 #[test]
2590 fn set_reduce_is_read_as_a_list() {
2591 let items = match plain(
2593 b"\x80\x02c__builtin__\nset\nq\x00]q\x01X\x04\x00\x00\x00maskq\x02a\x85q\x03Rq\x04.",
2594 )
2595 .unwrap()
2596 {
2597 Object::List(items) => items,
2598 other => panic!("expected list, got {other:?}"),
2599 };
2600 assert!(matches!(&items[..], [Object::String(s)] if s == "mask"));
2601 assert!(matches!(
2602 plain(b"\x80\x02c__builtin__\nfrozenset\nq\x00]q\x01\x85q\x02Rq\x03.").unwrap(),
2603 Object::List(items) if items.is_empty()
2604 ));
2605 }
2606
2607 #[test]
2608 fn ordered_dict_rejects_non_list_argument() {
2609 let args = Object::Tuple(vec![Object::Int(3)]);
2610 assert!(matches!(
2611 ordered_dict(args),
2612 Err(PickleError::InvalidData(msg)) if msg.contains("must be a list")
2613 ));
2614 }
2615
2616 #[test]
2617 fn call_with_extra_arguments_is_an_error() {
2618 let args = Object::Tuple(vec![Object::List(Vec::new()), Object::Int(1)]);
2620 assert!(matches!(
2621 sequence_arg(args, "set"),
2622 Err(PickleError::InvalidData(msg)) if msg == "set takes one argument, got 2"
2623 ));
2624 }
2625
2626 #[test]
2627 fn missing_storage_returns_contextual_error() {
2628 let source = fixture_source();
2629 let tensor = rebuild(rebuild_args(
2630 "FloatStorage",
2631 "missing",
2632 6,
2633 0,
2634 &[2, 3],
2635 &[3, 1],
2636 &source,
2637 ))
2638 .unwrap();
2639 let err = tensor.read().unwrap_err();
2640 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
2641 assert!(
2642 err.to_string()
2643 .contains("Failed to read storage 'missing' for tensor with shape [2, 3]"),
2644 "{err}"
2645 );
2646 }
2647
2648 #[test]
2649 fn os_errors_keep_their_kind() {
2650 use std::io::Read;
2655 let dir = tempfile::tempdir().unwrap();
2656 let file = std::fs::File::create(dir.path().join("write_only.pt")).unwrap();
2657 let expected = (&file).read(&mut [0u8; 8]).unwrap_err();
2658 assert!(expected.raw_os_error().is_some(), "{expected}");
2659
2660 let source = Arc::new(StorageSource::Legacy(crate::storage::LegacySource::new(
2661 file,
2662 )));
2663 let args = rebuild_args("FloatStorage", "0", 3, 0, &[3], &[1], &source);
2664 let StorageSource::Legacy(legacy) = &*source else {
2665 unreachable!()
2666 };
2667 legacy.finish(&["0".to_string()], 0, 8 + 12).unwrap();
2668 let tensor = rebuild(args).unwrap();
2669
2670 let err = tensor.read().unwrap_err();
2671 assert_eq!(err.kind(), expected.kind(), "{err}");
2672 assert!(
2673 err.to_string()
2674 .contains("Failed to read storage '0' for tensor with shape [3]"),
2675 "{err}"
2676 );
2677 }
2678
2679 #[test]
2680 fn tensors_inside_lists_and_tuples_get_indexed_names() {
2681 let source = fixture_source();
2684 let w1 = rebuild(rebuild_args(
2685 "FloatStorage",
2686 "0",
2687 4,
2688 0,
2689 &[2, 2],
2690 &[2, 1],
2691 &source,
2692 ))
2693 .unwrap();
2694 let w2 = rebuild(rebuild_args(
2695 "FloatStorage",
2696 "0",
2697 9,
2698 0,
2699 &[3, 3],
2700 &[3, 1],
2701 &source,
2702 ))
2703 .unwrap();
2704 let b = rebuild(rebuild_args("FloatStorage", "0", 4, 0, &[4], &[1], &source)).unwrap();
2705
2706 let mut nested = HashMap::new();
2707 nested.insert("w".to_string(), Object::Tensor(w2));
2708
2709 let mut dict = HashMap::new();
2710 dict.insert(
2712 "weights".to_string(),
2713 Object::List(vec![
2714 Object::Tensor(w1),
2715 Object::Int(7),
2716 Object::Dict(nested),
2717 ]),
2718 );
2719 dict.insert("bias".to_string(), Object::Tuple(vec![Object::Tensor(b)]));
2720
2721 let mut tensors = extract_tensors(dict);
2722 assert_eq!(tensors.len(), 3);
2723 assert!(tensors.contains_key("weights.0"));
2724 assert!(tensors.contains_key("weights.2.w"));
2725 assert!(tensors.contains_key("bias.0"));
2726
2727 let tensor = tensors.remove("weights.0").unwrap();
2729 assert_eq!(read_as::<f32>(&tensor), [0.0, 1.0, 2.0, 3.0]);
2730 }
2731}