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 if byte_len == 0 {
887 return Vec::new();
888 }
889 match element_size {
892 1 => gather_elements::<1>(data, shape, stride),
893 2 => gather_elements::<2>(data, shape, stride),
894 4 => gather_elements::<4>(data, shape, stride),
895 8 => gather_elements::<8>(data, shape, stride),
896 _ => unreachable!("no dtype is {element_size} bytes wide"),
897 }
898}
899
900fn gather_elements<const N: usize>(data: &[u8], shape: &[usize], stride: &[usize]) -> Vec<u8> {
902 let (elements, _) = data.as_chunks::<N>();
903
904 if let Some(([rows, cols], _)) = contiguous_transpose(shape, stride) {
907 return transpose(elements, rows, cols).into_flattened();
908 }
909
910 let ((&inner_len, outer_shape), (&inner_step, outer_stride)) = (
911 shape.split_last().unwrap_or((&1, &[])),
912 stride.split_last().unwrap_or((&0, &[])),
913 );
914 let mut out = Vec::with_capacity(shape.iter().product());
915 out.extend(
916 strided_offsets(outer_shape, outer_stride)
917 .flat_map(|base| (0..inner_len).map(move |k| elements[base + k * inner_step])),
918 );
919 out.into_flattened()
920}
921
922fn strided_offsets<'a>(
932 shape: &'a [usize],
933 stride: &'a [usize],
934) -> Box<dyn Iterator<Item = usize> + 'a> {
935 shape.iter().zip(stride).filter(|&(&dim, _)| dim != 1).fold(
936 Box::new(core::iter::once(0)),
937 |offsets, (&dim, &step)| {
938 Box::new(offsets.flat_map(move |base| (0..dim).map(move |i| base + i * step)))
939 },
940 )
941}
942
943fn transpose<const N: usize>(src: &[[u8; N]], rows: usize, cols: usize) -> Vec<[u8; N]> {
951 const TILE: usize = 32;
952
953 debug_assert!(
954 src.len() >= rows * cols,
955 "source is smaller than [{rows}, {cols}]"
956 );
957 let mut out = vec![[0; N]; rows * cols];
958 out.chunks_mut(TILE.min(cols) * rows)
962 .zip((0..cols).step_by(TILE))
963 .for_each(|(band, col_start)| {
964 src.chunks(TILE.min(rows) * cols)
965 .zip((0..rows).step_by(TILE))
966 .for_each(|(block, row_start)| {
967 band.chunks_exact_mut(rows)
968 .zip(col_start..)
969 .for_each(|(out_row, col)| {
970 out_row[row_start..]
971 .iter_mut()
972 .zip(block.chunks_exact(cols))
973 .for_each(|(dst, src_row)| *dst = src_row[col]);
974 });
975 });
976 });
977 out
978}
979
980fn to_native_endian(bytes: &mut [u8], element_size: usize) {
982 if cfg!(target_endian = "big") && element_size > 1 {
983 for element in bytes.chunks_exact_mut(element_size) {
984 element.reverse();
985 }
986 }
987}
988
989pub(crate) fn build_tensor(
994 storage: StorageRef,
995 dtype: DType,
996 storage_offset: usize,
997 shape: Vec<usize>,
998 stride: Vec<usize>,
999) -> Result<Tensor> {
1000 let element_size = dtype.size();
1001 let storage_offset = storage_offset
1002 .checked_add(storage.view_offset)
1003 .ok_or_else(|| PickleError::InvalidData("Storage offset overflows usize".to_string()))?;
1004 let num_elements = tensor_num_elements(&shape)?;
1005 let extent = tensor_storage_extent(&shape, &stride, storage_offset)?;
1006 let byte_len = num_elements.checked_mul(element_size).ok_or_else(|| {
1007 PickleError::InvalidData("Tensor byte length overflows usize".to_string())
1008 })?;
1009
1010 let declared_elements = storage.reachable_bytes / element_size;
1011 if extent > declared_elements {
1012 return Err(PickleError::InvalidData(format!(
1013 "Tensor with shape {shape:?} and stride {stride:?} at offset {storage_offset} needs {extent} elements, but storage '{}' declares {declared_elements}",
1014 storage.key
1015 )));
1016 }
1017 if byte_len > MAX_TENSOR_SIZE {
1019 return Err(PickleError::InvalidData(format!(
1020 "Tensor with shape {shape:?} would materialize {byte_len} bytes, above the {MAX_TENSOR_SIZE} byte limit"
1021 )));
1022 }
1023
1024 let StorageRef { source, key, .. } = storage;
1025 let window = Arc::new(StorageWindow {
1028 source,
1029 key,
1030 shape: shape.clone(),
1031 dtype,
1032 start: storage_offset.saturating_mul(element_size),
1033 len: (extent - storage_offset).saturating_mul(element_size),
1034 extent,
1035 });
1036 let transposed = contiguous_transpose(&shape, &stride).map(|(shape, stride)| {
1037 let window = Arc::clone(&window);
1038 move || window.read(&shape, &stride)
1039 });
1040 let read = {
1041 let shape = shape.clone();
1042 move || window.read(&shape, &stride)
1043 };
1044
1045 let tensor = Tensor::new(String::new(), dtype, shape, read);
1048 Ok(match transposed {
1049 Some(read) => tensor.with_transposed(read),
1050 None => tensor,
1051 })
1052}
1053
1054fn contiguous_transpose(shape: &[usize], stride: &[usize]) -> Option<([usize; 2], [usize; 2])> {
1057 let (&[rows, cols], &[row_step, col_step]) = (shape, stride) else {
1058 return None;
1059 };
1060 let (transposed_shape, transposed_stride) = ([cols, rows], [col_step, row_step]);
1061
1062 (!is_contiguous_stride(shape, stride)
1063 && is_contiguous_stride(&transposed_shape, &transposed_stride))
1064 .then_some((transposed_shape, transposed_stride))
1065}
1066
1067struct StorageWindow {
1070 source: Arc<StorageSource>,
1071 key: String,
1072 shape: Vec<usize>,
1074 dtype: DType,
1075 start: usize,
1077 len: usize,
1079 extent: usize,
1081}
1082
1083impl StorageWindow {
1084 fn read(&self, shape: &[usize], stride: &[usize]) -> io::Result<Vec<u8>> {
1090 let Self {
1091 source,
1092 key,
1093 shape: declared_shape,
1094 dtype,
1095 start,
1096 len,
1097 extent,
1098 } = self;
1099 let element_size = dtype.size();
1100 let byte_len = shape.iter().product::<usize>() * element_size;
1101
1102 let (mut data, skipped) = source.read(key, *start, *len).map_err(|err| {
1103 let kind = match err.kind() {
1108 kind if err.raw_os_error().is_some() => kind,
1109 io::ErrorKind::UnexpectedEof => io::ErrorKind::UnexpectedEof,
1110 _ => io::ErrorKind::InvalidData,
1111 };
1112 io::Error::new(
1113 kind,
1114 format!(
1115 "Failed to read storage '{key}' for tensor with shape {declared_shape:?}: {err}"
1116 ),
1117 )
1118 })?;
1119
1120 let available = (skipped + data.len()) / element_size;
1122 if *extent > available {
1123 return Err(io::Error::new(
1124 io::ErrorKind::UnexpectedEof,
1125 format!(
1126 "Tensor with shape {declared_shape:?} requires {extent} elements from storage '{key}', but only {available} are available"
1127 ),
1128 ));
1129 }
1130
1131 let mut bytes = if is_contiguous_stride(shape, stride) {
1132 data.truncate(byte_len);
1133 data
1134 } else {
1135 gather_strided(&data, shape, stride, element_size, byte_len)
1136 };
1137
1138 to_native_endian(&mut bytes, element_size);
1139 if *dtype == DType::Bool {
1140 for byte in &mut bytes {
1143 *byte = u8::from(*byte != 0);
1144 }
1145 }
1146
1147 Ok(bytes)
1148 }
1149}
1150
1151const MAX_COPIED_NODES: usize = 1_000_000;
1158
1159const MAX_COPIED_BYTES: usize = 64 << 20;
1162
1163const MAX_NESTING_DEPTH: u32 = 1000;
1169
1170#[derive(Clone)]
1172struct Entry {
1173 object: Object,
1174 depth: u32,
1175 memo: Option<u32>,
1178}
1179
1180struct Unpickler<'a> {
1181 stack: Vec<Entry>,
1182 marks: Vec<usize>,
1184 memo: HashMap<u32, Entry>,
1185 copied_nodes: usize,
1186 copied_bytes: usize,
1187 ids: &'a PersistentIds,
1188}
1189
1190impl<'a> Unpickler<'a> {
1191 fn new(ids: &'a PersistentIds) -> Self {
1192 Self {
1193 stack: Vec::new(),
1194 marks: Vec::new(),
1195 memo: HashMap::new(),
1196 copied_nodes: 0,
1197 copied_bytes: 0,
1198 ids,
1199 }
1200 }
1201
1202 fn push_scalar(&mut self, object: Object) {
1203 self.stack.push(Entry {
1204 object,
1205 depth: 0,
1206 memo: None,
1207 });
1208 }
1209
1210 fn push(&mut self, object: Object, depth: u32) -> Result<()> {
1211 if depth > MAX_NESTING_DEPTH {
1212 return Err(PickleError::InvalidData(format!(
1213 "pickle nesting exceeds {MAX_NESTING_DEPTH} levels"
1214 )));
1215 }
1216 self.stack.push(Entry {
1217 object,
1218 depth,
1219 memo: None,
1220 });
1221 Ok(())
1222 }
1223
1224 fn mark_on_top(&self) -> bool {
1226 self.marks.last() == Some(&self.stack.len())
1227 }
1228
1229 fn pop(&mut self) -> Result<Entry> {
1230 if self.mark_on_top() {
1231 return Err(PickleError::InvalidData(
1232 "expected a value on the stack, found MARK".to_string(),
1233 ));
1234 }
1235 self.stack.pop().ok_or(PickleError::StackUnderflow)
1236 }
1237
1238 fn pop_object(&mut self) -> Result<Object> {
1239 self.pop().map(|entry| entry.object)
1240 }
1241
1242 fn top(&self) -> Result<&Entry> {
1243 if self.mark_on_top() {
1244 return Err(PickleError::InvalidData(
1245 "expected a value on the stack, found MARK".to_string(),
1246 ));
1247 }
1248 self.stack.last().ok_or(PickleError::StackUnderflow)
1249 }
1250
1251 fn top_mut(&mut self) -> Result<&mut Entry> {
1252 if self.mark_on_top() {
1253 return Err(PickleError::InvalidData(
1254 "expected a value on the stack, found MARK".to_string(),
1255 ));
1256 }
1257 self.stack.last_mut().ok_or(PickleError::StackUnderflow)
1258 }
1259
1260 fn pop_to_mark(&mut self) -> Result<(Vec<Object>, u32)> {
1264 let mark = self
1265 .marks
1266 .pop()
1267 .ok_or_else(|| PickleError::InvalidData("MARK not found on stack".to_string()))?;
1268 let entries = self.stack.split_off(mark);
1269 let depth = entries.iter().map(|entry| entry.depth).max().unwrap_or(0);
1270 Ok((
1271 entries.into_iter().map(|entry| entry.object).collect(),
1272 depth,
1273 ))
1274 }
1275
1276 fn charge_copy(&mut self, object: &Object) -> Result<()> {
1278 self.copied_nodes += object.node_count();
1279 self.copied_bytes += object.payload_bytes();
1280 if self.copied_nodes > MAX_COPIED_NODES || self.copied_bytes > MAX_COPIED_BYTES {
1281 return Err(PickleError::InvalidData(format!(
1282 "Pickle memo bomb detected: copies exceeded {MAX_COPIED_NODES} nodes or {MAX_COPIED_BYTES} bytes"
1283 )));
1284 }
1285 Ok(())
1286 }
1287
1288 fn memo_get(&mut self, idx: u32) -> Result<Entry> {
1289 let entry = self
1290 .memo
1291 .get(&idx)
1292 .ok_or(PickleError::MemoNotFound(idx))?
1293 .clone();
1294 self.charge_copy(&entry.object)?;
1295 Ok(entry)
1296 }
1297
1298 fn memo_put(&mut self, idx: u32) -> Result<()> {
1299 let top = self.top_mut()?;
1300 top.memo = Some(idx);
1301 let entry = top.clone();
1302 self.charge_copy(&entry.object)?;
1303 self.memo.insert(idx, entry);
1304 Ok(())
1305 }
1306
1307 fn append(&mut self, items: Vec<Object>, items_depth: u32, op: OpCode) -> Result<()> {
1309 let top = self.top_mut()?;
1310 let Object::List(list) = &mut top.object else {
1311 return Err(PickleError::UnexpectedOpCode(op));
1312 };
1313 list.extend(items);
1314 let depth = top.depth.max(items_depth + 1);
1315 top.depth = depth;
1316 if depth > MAX_NESTING_DEPTH {
1317 return Err(PickleError::InvalidData(format!(
1318 "pickle nesting exceeds {MAX_NESTING_DEPTH} levels"
1319 )));
1320 }
1321 Ok(())
1322 }
1323
1324 fn set_items(&mut self, items: Vec<Object>, items_depth: u32, op: OpCode) -> Result<()> {
1326 let top = self.top_mut()?;
1327 let Object::Dict(dict) = &mut top.object else {
1328 return Err(PickleError::UnexpectedOpCode(op));
1329 };
1330 insert_pairs(dict, items)?;
1331 let depth = top.depth.max(items_depth + 1);
1332 top.depth = depth;
1333 if depth > MAX_NESTING_DEPTH {
1334 return Err(PickleError::InvalidData(format!(
1335 "pickle nesting exceeds {MAX_NESTING_DEPTH} levels"
1336 )));
1337 }
1338 Ok(())
1339 }
1340
1341 fn run<R: BufRead>(mut self, r: &mut R) -> Result<Object> {
1342 loop {
1343 let byte = r.read_u8()?;
1344 let op = OpCode::try_from(byte).map_err(PickleError::InvalidOpCode)?;
1345 match op {
1346 OpCode::Proto => {
1347 let version = r.read_u8()?;
1348 if version > 5 {
1349 return Err(PickleError::InvalidProtocol(version));
1350 }
1351 }
1352 OpCode::Frame => {
1353 r.read_u64::<LittleEndian>()?;
1355 }
1356 OpCode::Stop => break,
1357
1358 OpCode::None => self.push_scalar(Object::None),
1360 OpCode::NewTrue => self.push_scalar(Object::Bool(true)),
1361 OpCode::NewFalse => self.push_scalar(Object::Bool(false)),
1362 OpCode::Int => {
1363 let text = read_line(r)?;
1364 let value = match text.as_str() {
1366 "00" => Object::Bool(false),
1367 "01" => Object::Bool(true),
1368 _ => Object::Int(parse_int(&text)?),
1369 };
1370 self.push_scalar(value);
1371 }
1372 OpCode::Long => {
1373 let text = read_line(r)?;
1374 let text = text.strip_suffix('L').unwrap_or(&text);
1375 self.push_scalar(parse_long(text)?);
1376 }
1377 OpCode::Float => {
1378 let text = read_line(r)?;
1379 let value = text.parse::<f64>().map_err(|e| {
1380 PickleError::InvalidData(format!("Invalid FLOAT value '{text}': {e}"))
1381 })?;
1382 self.push_scalar(Object::Float(value));
1383 }
1384 OpCode::Unicode => {
1385 let text = read_raw_unicode_escape_line(r)?;
1386 self.push_scalar(Object::String(text));
1387 }
1388 OpCode::BinInt => {
1389 let v = r.read_i32::<LittleEndian>()?;
1390 self.push_scalar(Object::Int(v as i64));
1391 }
1392 OpCode::BinInt1 => {
1393 let v = r.read_u8()?;
1394 self.push_scalar(Object::Int(v as i64));
1395 }
1396 OpCode::BinInt2 => {
1397 let v = r.read_u16::<LittleEndian>()?;
1398 self.push_scalar(Object::Int(v as i64));
1399 }
1400 OpCode::BinFloat => {
1401 let v = r.read_f64::<BigEndian>()?;
1402 self.push_scalar(Object::Float(v));
1403 }
1404 OpCode::Long1 => {
1405 let len = r.read_u8()? as u64;
1406 let bytes = read_exact_len(r, len, STRING_PREALLOC_BOUND)?;
1407 self.push_scalar(long_object(&bytes));
1408 }
1409 OpCode::Long4 => {
1410 let len = r.read_u32::<LittleEndian>()? as u64;
1411 let bytes = read_exact_len(r, len, STRING_PREALLOC_BOUND)?;
1412 self.push_scalar(long_object(&bytes));
1413 }
1414
1415 OpCode::ShortBinUnicode => {
1417 let len = r.read_u8()? as u64;
1418 self.push_scalar(read_string(r, len)?);
1419 }
1420 OpCode::BinUnicode => {
1421 let len = r.read_u32::<LittleEndian>()? as u64;
1422 self.push_scalar(read_string(r, len)?);
1423 }
1424 OpCode::ShortBinString => {
1425 let len = r.read_u8()? as u64;
1426 self.push_scalar(read_py2_string(r, len)?);
1427 }
1428 OpCode::BinString => {
1429 let len = r.read_u32::<LittleEndian>()? as u64;
1430 self.push_scalar(read_py2_string(r, len)?);
1431 }
1432 OpCode::BinUnicode8 => {
1433 let len = r.read_u64::<LittleEndian>()?;
1434 self.push_scalar(read_string(r, len)?);
1435 }
1436 OpCode::ShortBinBytes => {
1437 let len = r.read_u8()? as u64;
1438 self.push_scalar(Object::Bytes(read_bytes(r, len)?));
1439 }
1440 OpCode::BinBytes => {
1441 let len = r.read_u32::<LittleEndian>()? as u64;
1442 self.push_scalar(Object::Bytes(read_bytes(r, len)?));
1443 }
1444 OpCode::BinBytes8 | OpCode::ByteArray8 => {
1445 let len = r.read_u64::<LittleEndian>()?;
1446 self.push_scalar(Object::Bytes(read_bytes(r, len)?));
1447 }
1448
1449 OpCode::Mark => self.marks.push(self.stack.len()),
1451 OpCode::EmptyTuple => self.push_scalar(Object::Tuple(Vec::new())),
1452 OpCode::EmptyList | OpCode::EmptySet => self.push_scalar(Object::List(Vec::new())),
1453 OpCode::EmptyDict => self.push_scalar(Object::Dict(HashMap::new())),
1454 OpCode::Tuple => {
1455 let (items, depth) = self.pop_to_mark()?;
1456 self.push(Object::Tuple(items), depth + 1)?;
1457 }
1458 OpCode::Tuple1 => {
1459 let a = self.pop()?;
1460 self.push(Object::Tuple(vec![a.object]), a.depth + 1)?;
1461 }
1462 OpCode::Tuple2 => {
1463 let b = self.pop()?;
1464 let a = self.pop()?;
1465 let depth = a.depth.max(b.depth) + 1;
1466 self.push(Object::Tuple(vec![a.object, b.object]), depth)?;
1467 }
1468 OpCode::Tuple3 => {
1469 let c = self.pop()?;
1470 let b = self.pop()?;
1471 let a = self.pop()?;
1472 let depth = a.depth.max(b.depth).max(c.depth) + 1;
1473 self.push(Object::Tuple(vec![a.object, b.object, c.object]), depth)?;
1474 }
1475 OpCode::List | OpCode::FrozenSet => {
1476 let (items, depth) = self.pop_to_mark()?;
1477 self.push(Object::List(items), depth + 1)?;
1478 }
1479 OpCode::Append => {
1480 let value = self.pop()?;
1481 self.append(vec![value.object], value.depth, op)?;
1482 }
1483 OpCode::Appends | OpCode::AddItems => {
1484 let (items, depth) = self.pop_to_mark()?;
1485 self.append(items, depth, op)?;
1486 }
1487 OpCode::Dict => {
1488 let (items, depth) = self.pop_to_mark()?;
1489 let mut dict = HashMap::with_capacity(items.len() / 2);
1490 insert_pairs(&mut dict, items)?;
1491 self.push(Object::Dict(dict), depth + 1)?;
1492 }
1493 OpCode::SetItem => {
1494 let value = self.pop()?;
1495 let key = self.pop()?;
1496 let depth = key.depth.max(value.depth);
1497 self.set_items(vec![key.object, value.object], depth, op)?;
1498 }
1499 OpCode::SetItems => {
1500 let (items, depth) = self.pop_to_mark()?;
1501 self.set_items(items, depth, op)?;
1502 }
1503
1504 OpCode::Pop => {
1506 if self.mark_on_top() {
1508 self.marks.pop();
1509 } else {
1510 self.pop()?;
1511 }
1512 }
1513 OpCode::PopMark => {
1514 self.pop_to_mark()?;
1515 }
1516 OpCode::Dup => {
1517 let top = self.top()?.clone();
1518 self.charge_copy(&top.object)?;
1519 self.stack.push(top);
1520 }
1521
1522 OpCode::Get => {
1524 let idx = parse_index(&read_line(r)?)?;
1525 let entry = self.memo_get(idx)?;
1526 self.stack.push(entry);
1527 }
1528 OpCode::BinGet => {
1529 let idx = r.read_u8()? as u32;
1530 let entry = self.memo_get(idx)?;
1531 self.stack.push(entry);
1532 }
1533 OpCode::LongBinGet => {
1534 let idx = r.read_u32::<LittleEndian>()?;
1535 let entry = self.memo_get(idx)?;
1536 self.stack.push(entry);
1537 }
1538 OpCode::Put => {
1539 let idx = parse_index(&read_line(r)?)?;
1540 self.memo_put(idx)?;
1541 }
1542 OpCode::BinPut => {
1543 let idx = r.read_u8()? as u32;
1544 self.memo_put(idx)?;
1545 }
1546 OpCode::LongBinPut => {
1547 let idx = r.read_u32::<LittleEndian>()?;
1548 self.memo_put(idx)?;
1549 }
1550 OpCode::Memoize => {
1551 let idx = self.memo.len() as u32;
1552 self.memo_put(idx)?;
1553 }
1554
1555 OpCode::Global => {
1557 let module_name = read_line(r)?;
1558 let name = read_line(r)?;
1559 self.push_scalar(Object::Class { module_name, name });
1560 }
1561 OpCode::StackGlobal => {
1562 let name = self.pop_object()?;
1563 let module_name = self.pop_object()?;
1564 match (module_name, name) {
1565 (Object::String(module_name), Object::String(name)) => {
1566 self.push_scalar(Object::Class { module_name, name })
1567 }
1568 _ => {
1569 return Err(PickleError::InvalidData(
1570 "STACK_GLOBAL expects two strings".to_string(),
1571 ));
1572 }
1573 }
1574 }
1575 OpCode::PersId => {
1576 let pid = Object::String(read_line(r)?);
1577 let obj = resolve_persistent_id(pid, self.ids)?;
1578 self.push_scalar(obj);
1579 }
1580 OpCode::BinPersId => {
1581 let pid = self.pop_object()?;
1582 let obj = resolve_persistent_id(pid, self.ids)?;
1583 self.push_scalar(obj);
1584 }
1585 OpCode::Reduce => {
1586 let args = self.pop()?;
1587 let callable = self.pop_object()?;
1588 let obj = reduce(callable, args.object)?;
1590 self.push(obj, args.depth)?;
1591 }
1592 OpCode::NewObj => {
1593 let args = self.pop_object()?;
1594 let cls = self.pop_object()?;
1595 let obj = opaque(&cls, &args)?;
1596 self.push_scalar(obj);
1597 }
1598 OpCode::NewObjEx => {
1599 let kwargs = self.pop_object()?;
1600 let args = self.pop_object()?;
1601 let cls = self.pop_object()?;
1602 opaque(&cls, &kwargs)?;
1603 let obj = opaque(&cls, &args)?;
1604 self.push_scalar(obj);
1605 }
1606 OpCode::Build => {
1607 let state = self.pop()?;
1608 let object = self.pop()?;
1609 let memo = object.memo;
1610 let (built, depth) = match (object.object, state.object) {
1611 (Object::Dict(mut dict), Object::Dict(update)) => {
1612 dict.extend(update);
1613 (Object::Dict(dict), object.depth.max(state.depth))
1614 }
1615 (dict @ Object::Dict(_), state) => {
1618 if state.holds_tensor_data() {
1619 return Err(PickleError::UnsupportedType(
1620 "dict with tensor-bearing state".to_string(),
1621 ));
1622 }
1623 (dict, object.depth)
1624 }
1625 (Object::Opaque(_), Object::Dict(attrs)) if is_module_state(&attrs) => {
1628 (module_state_dict(attrs)?, state.depth)
1629 }
1630 (object, state) => (opaque(&object, &state)?, 0),
1631 };
1632 if let Some(idx) = memo {
1637 self.charge_copy(&built)?;
1638 self.memo.insert(
1639 idx,
1640 Entry {
1641 object: built.clone(),
1642 depth,
1643 memo: Some(idx),
1644 },
1645 );
1646 }
1647 self.push(built, depth)?;
1648 }
1649
1650 OpCode::Ext1
1651 | OpCode::Ext2
1652 | OpCode::Ext4
1653 | OpCode::NextBuffer
1654 | OpCode::ReadonlyBuffer => return Err(PickleError::UnsupportedOpCode(op)),
1655 }
1656 }
1657
1658 self.pop_object()
1659 }
1660}
1661
1662fn read_line<R: BufRead>(r: &mut R) -> Result<String> {
1663 let mut data = Vec::with_capacity(32);
1664 r.read_until(b'\n', &mut data)?;
1665 if data.pop() != Some(b'\n') {
1666 return Err(PickleError::Io(io::Error::new(
1667 io::ErrorKind::UnexpectedEof,
1668 "unterminated text field",
1669 )));
1670 }
1671 if data.last() == Some(&b'\r') {
1672 data.pop();
1673 }
1674 String::from_utf8(data).map_err(|e| PickleError::InvalidData(format!("Invalid UTF-8: {e}")))
1675}
1676
1677fn read_raw_unicode_escape_line<R: BufRead>(r: &mut R) -> Result<String> {
1680 let mut data = Vec::with_capacity(32);
1681 r.read_until(b'\n', &mut data)?;
1682 if data.pop() != Some(b'\n') {
1683 return Err(PickleError::Io(io::Error::new(
1684 io::ErrorKind::UnexpectedEof,
1685 "unterminated text field",
1686 )));
1687 }
1688
1689 let mut text = String::with_capacity(data.len());
1690 let mut i = 0;
1691 while i < data.len() {
1692 let byte = data[i];
1693 let escape_len = match (byte, data.get(i + 1)) {
1694 (b'\\', Some(b'u')) => Some(4),
1695 (b'\\', Some(b'U')) => Some(8),
1696 _ => None,
1697 };
1698 match escape_len {
1699 Some(digits) => {
1700 let hex = data
1701 .get(i + 2..i + 2 + digits)
1702 .and_then(|hex| std::str::from_utf8(hex).ok())
1703 .and_then(|hex| u32::from_str_radix(hex, 16).ok())
1704 .and_then(char::from_u32)
1705 .ok_or_else(|| {
1706 PickleError::InvalidData("Invalid raw-unicode-escape sequence".to_string())
1707 })?;
1708 text.push(hex);
1709 i += 2 + digits;
1710 }
1711 None => {
1712 text.push(char::from(byte));
1713 i += 1;
1714 }
1715 }
1716 }
1717 Ok(text)
1718}
1719
1720fn read_bytes<R: BufRead>(r: &mut R, len: u64) -> Result<Vec<u8>> {
1721 Ok(read_exact_len(r, len, STRING_PREALLOC_BOUND)?)
1722}
1723
1724fn read_string<R: BufRead>(r: &mut R, len: u64) -> Result<Object> {
1725 let s = String::from_utf8(read_bytes(r, len)?)
1726 .map_err(|e| PickleError::InvalidData(format!("Invalid UTF-8: {e}")))?;
1727 Ok(Object::String(s))
1728}
1729
1730fn read_py2_string<R: BufRead>(r: &mut R, len: u64) -> Result<Object> {
1733 Ok(match String::from_utf8(read_bytes(r, len)?) {
1734 Ok(text) => Object::String(text),
1735 Err(err) => Object::Bytes(err.into_bytes()),
1736 })
1737}
1738
1739fn parse_int(text: &str) -> Result<i64> {
1740 text.parse::<i64>()
1741 .map_err(|e| PickleError::InvalidData(format!("Invalid integer '{text}': {e}")))
1742}
1743
1744fn parse_index(text: &str) -> Result<u32> {
1745 text.parse::<u32>()
1746 .map_err(|e| PickleError::InvalidData(format!("Invalid memo index '{text}': {e}")))
1747}
1748
1749fn long_object(bytes: &[u8]) -> Object {
1756 int_from_le_bytes(bytes).map_or_else(|| Object::Opaque("int".to_string()), Object::Int)
1757}
1758
1759fn parse_long(text: &str) -> Result<Object> {
1762 use std::num::IntErrorKind;
1763
1764 match text.parse::<i64>() {
1765 Ok(value) => Ok(Object::Int(value)),
1766 Err(e)
1767 if matches!(
1768 e.kind(),
1769 IntErrorKind::PosOverflow | IntErrorKind::NegOverflow
1770 ) =>
1771 {
1772 Ok(Object::Opaque("int".to_string()))
1773 }
1774 Err(e) => Err(PickleError::InvalidData(format!(
1775 "Invalid integer '{text}': {e}"
1776 ))),
1777 }
1778}
1779
1780fn int_from_le_bytes(bytes: &[u8]) -> Option<i64> {
1782 let negative = bytes.last().is_some_and(|b| b & 0x80 != 0);
1783 let fill = if negative { 0xff } else { 0x00 };
1784 let fits = bytes.len() <= 8
1787 || (bytes[8..].iter().all(|&b| b == fill) && (bytes[7] & 0x80 != 0) == negative);
1788 if !fits {
1789 return None;
1790 }
1791 let mut buf = [fill; 8];
1792 let len = bytes.len().min(8);
1793 buf[..len].copy_from_slice(&bytes[..len]);
1794 Some(i64::from_le_bytes(buf))
1795}
1796
1797fn dict_key(key: Object) -> Result<String> {
1798 match key {
1799 Object::String(s) => Ok(s),
1800 other => key_string(&other, "dict key"),
1801 }
1802}
1803
1804fn insert_pairs(dict: &mut HashMap<String, Object>, items: Vec<Object>) -> Result<()> {
1805 if !items.len().is_multiple_of(2) {
1806 return Err(PickleError::InvalidData(
1807 "dict items must come in key/value pairs".to_string(),
1808 ));
1809 }
1810 let mut items = items.into_iter();
1811 while let (Some(key), Some(value)) = (items.next(), items.next()) {
1812 dict.insert(dict_key(key)?, value);
1813 }
1814 Ok(())
1815}
1816
1817pub(crate) fn extract_tensors(dict: HashMap<String, Object>) -> HashMap<String, Tensor> {
1830 fn walk(obj: Object, path: &mut Vec<String>, tensors: &mut HashMap<String, Tensor>) {
1831 match obj {
1832 Object::Dict(dict) => {
1833 for (key, value) in dict {
1834 path.push(key);
1835 walk(value, path, tensors);
1836 path.pop();
1837 }
1838 }
1839 Object::List(items) | Object::Tuple(items) => {
1840 for (index, item) in items.into_iter().enumerate() {
1841 path.push(index.to_string());
1842 walk(item, path, tensors);
1843 path.pop();
1844 }
1845 }
1846 Object::Tensor(mut tensor) => {
1847 tensor.name = path.join(".");
1848 tensors.insert(tensor.name.clone(), tensor);
1849 }
1850 _ => {}
1851 }
1852 }
1853 let mut tensors = HashMap::new();
1854 walk(Object::Dict(dict), &mut Vec::new(), &mut tensors);
1855 tensors
1856}
1857
1858#[cfg(test)]
1859mod tests {
1860 use super::*;
1861 use crate::reader::Container;
1862 use crate::tests::{read_as, test_data_path};
1863 use std::io::Cursor;
1864
1865 fn fixture_source() -> Arc<StorageSource> {
1866 let path = test_data_path("non_contiguous.pt");
1867 Arc::new(StorageSource::Zip(
1868 crate::storage::ZipSource::open(&Container::File(path)).unwrap(),
1869 ))
1870 }
1871
1872 fn storage_pid(storage_type: &str, key: &str, numel: i64) -> Object {
1873 Object::Tuple(vec![
1874 Object::String("storage".to_string()),
1875 Object::Class {
1876 module_name: "torch".to_string(),
1877 name: storage_type.to_string(),
1878 },
1879 Object::String(key.to_string()),
1880 Object::String("cpu".to_string()),
1881 Object::Int(numel),
1882 ])
1883 }
1884
1885 fn rebuild_args(
1886 storage_type: &str,
1887 key: &str,
1888 numel: i64,
1889 offset: i64,
1890 shape: &[i64],
1891 stride: &[i64],
1892 source: &Arc<StorageSource>,
1893 ) -> Object {
1894 let storage = match &storage_pid(storage_type, key, numel) {
1895 Object::Tuple(fields) => resolve_storage_id(fields, source).unwrap(),
1896 _ => unreachable!(),
1897 };
1898 Object::Tuple(vec![
1899 Object::Storage(storage),
1900 Object::Int(offset),
1901 Object::Tuple(shape.iter().copied().map(Object::Int).collect()),
1902 Object::Tuple(stride.iter().copied().map(Object::Int).collect()),
1903 ])
1904 }
1905
1906 fn rebuild(args: Object) -> Result<Tensor> {
1907 match rebuild_tensor(args, TensorRebuild::Legacy)? {
1908 Object::Tensor(tensor) => Ok(tensor),
1909 other => panic!("expected tensor, got {other:?}"),
1910 }
1911 }
1912
1913 fn plain(bytes: &[u8]) -> Result<Object> {
1914 read_pickle(&mut Cursor::new(bytes), &PersistentIds::Unavailable)
1915 }
1916
1917 #[test]
1918 fn test_memo_bomb_mitigation() {
1919 let mut poc = vec![0x89, b'q', 0x00];
1921 let n = 20; for _ in 0..n {
1923 poc.extend_from_slice(&[b'h', 0x00, b'h', 0x00, 0x86, b'q', 0x00]);
1924 }
1925 poc.push(b'.');
1926
1927 let err = plain(&poc).unwrap_err();
1928 assert!(matches!(err, PickleError::InvalidData(msg) if msg.contains("memo bomb")));
1929 }
1930
1931 #[test]
1932 fn memo_copies_of_large_payloads_are_bounded() {
1933 let payload = vec![0xabu8; 2 << 20];
1935 let mut bytes = vec![0x80, 0x02, b'B'];
1936 bytes.extend_from_slice(&(payload.len() as u32).to_le_bytes());
1937 bytes.extend_from_slice(&payload);
1938 bytes.extend_from_slice(b"q\x00(");
1939 for _ in 0..40 {
1940 bytes.extend_from_slice(b"h\x00");
1941 }
1942 bytes.extend_from_slice(b"t.");
1943 assert!(matches!(
1944 plain(&bytes).unwrap_err(),
1945 PickleError::InvalidData(msg) if msg.contains("memo bomb")
1946 ));
1947 }
1948
1949 #[test]
1950 fn dup_doubling_is_bounded() {
1951 let mut bytes = vec![0x80, 0x02, b'K', 0x01];
1953 for _ in 0..40 {
1954 bytes.extend_from_slice(b"2\x86");
1955 }
1956 bytes.push(b'.');
1957 assert!(matches!(
1958 plain(&bytes).unwrap_err(),
1959 PickleError::InvalidData(msg) if msg.contains("memo bomb")
1960 ));
1961 }
1962
1963 #[test]
1964 fn unicode_opcode_decodes_raw_unicode_escape() {
1965 let bytes = b"V\xe9t\xe9 \\u6a21\np0\n.";
1967 assert!(matches!(
1968 plain(bytes).unwrap(),
1969 Object::String(s) if s == "\u{e9}t\u{e9} \u{6a21}"
1970 ));
1971 assert!(plain(b"V\\u12.\n.").is_err());
1972 }
1973
1974 #[test]
1975 fn rebuild_from_type_v2_rejects_tensor_bearing_state() {
1976 let source = fixture_source();
1977 let callable = Object::Class {
1978 module_name: "torch._tensor".to_string(),
1979 name: "_rebuild_from_type_v2".to_string(),
1980 };
1981 let mut state = HashMap::new();
1982 state.insert(
1983 "extra".to_string(),
1984 rebuild_args("FloatStorage", "0", 32, 0, &[2], &[1], &source),
1985 );
1986 let args = Object::Tuple(vec![
1987 Object::Class {
1988 module_name: "torch._utils".to_string(),
1989 name: "_rebuild_tensor".to_string(),
1990 },
1991 Object::Class {
1992 module_name: "torch".to_string(),
1993 name: "Tensor".to_string(),
1994 },
1995 rebuild_args("FloatStorage", "0", 32, 5, &[2, 3], &[3, 1], &source),
1996 Object::Dict(state),
1997 ]);
1998 assert!(matches!(
1999 reduce(callable, args).unwrap_err(),
2000 PickleError::UnsupportedType(_)
2001 ));
2002 }
2003
2004 #[test]
2005 fn protocol_4_pickle_parses() {
2006 let bytes: &[u8] = &[
2008 0x80, 0x04, 0x95, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7d, 0x94, 0x28,
2009 0x8c, 0x01, 0x61, 0x94, 0x4b, 0x01, 0x8c, 0x01, 0x62, 0x94, 0x5d, 0x94, 0x28, 0x47,
2010 0x3f, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x88, 0x4e, 0x65, 0x8c, 0x01, 0x63,
2011 0x94, 0x43, 0x02, 0x78, 0x79, 0x94, 0x8c, 0x01, 0x64, 0x94, 0x7d, 0x94, 0x8c, 0x01,
2012 0x65, 0x94, 0x8c, 0x01, 0x66, 0x94, 0x73, 0x75, 0x2e,
2013 ];
2014 let Object::Dict(dict) = plain(bytes).unwrap() else {
2015 panic!("expected dict");
2016 };
2017 assert!(matches!(dict["a"], Object::Int(1)));
2018 let Object::List(items) = &dict["b"] else {
2019 panic!("expected list");
2020 };
2021 assert!(
2022 matches!(items[..], [Object::Float(f), Object::Bool(true), Object::None] if f == 1.5)
2023 );
2024 assert!(matches!(&dict["c"], Object::Bytes(b) if b == b"xy"));
2025 let Object::Dict(inner) = &dict["d"] else {
2026 panic!("expected dict");
2027 };
2028 assert!(matches!(&inner["e"], Object::String(s) if s == "f"));
2029 }
2030
2031 #[test]
2032 fn stack_global_builds_class() {
2033 let bytes: &[u8] = &[
2035 0x80, 0x04, 0x95, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8c, 0x0b, b'c',
2036 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',
2037 b'r', b'd', b'e', b'r', b'e', b'd', b'D', b'i', b'c', b't', 0x94, 0x93, 0x94, 0x2e,
2038 ];
2039 assert!(matches!(
2040 plain(bytes).unwrap(),
2041 Object::Class { module_name, name } if module_name == "collections" && name == "OrderedDict"
2042 ));
2043 }
2044
2045 #[test]
2046 fn protocol_0_text_opcodes_parse() {
2047 let bytes =
2049 b"(dp0\nVn\np1\nI-3\nsVf\np2\nF2.5\nsVt\np3\nI01\nsVbig\np4\nL1099511627776L\ns.";
2050 let Object::Dict(dict) = plain(bytes).unwrap_or_else(|e| panic!("{e}")) else {
2051 panic!("expected dict");
2052 };
2053 assert!(matches!(dict["n"], Object::Int(-3)));
2054 assert!(matches!(dict["f"], Object::Float(f) if f == 2.5));
2055 assert!(matches!(dict["t"], Object::Bool(true)));
2056 assert!(matches!(dict["big"], Object::Int(1099511627776)));
2057 }
2058
2059 #[test]
2060 fn long1_handles_sign_and_width() {
2061 assert_eq!(int_from_le_bytes(&[]).unwrap(), 0);
2062 assert_eq!(int_from_le_bytes(&[0xff]).unwrap(), -1);
2063 assert_eq!(int_from_le_bytes(&[0x00, 0x01]).unwrap(), 256);
2064 assert_eq!(
2065 int_from_le_bytes(&[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0x00]).unwrap(),
2066 i64::MAX
2067 );
2068 assert!(
2069 int_from_le_bytes(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01]).is_none()
2070 );
2071 }
2072
2073 #[test]
2074 fn oversized_int_beside_weights_stays_opaque() {
2075 for bytes in [
2077 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(),
2078 b"(dp0\nVseed\np1\nL18446744073709551616L\nsVn\np2\nI7\ns.".as_slice(),
2079 ] {
2080 let Object::Dict(dict) = plain(bytes).unwrap_or_else(|e| panic!("{e}")) else {
2081 panic!("expected dict");
2082 };
2083 assert!(matches!(&dict["seed"], Object::Opaque(name) if name == "int"));
2084 assert!(matches!(dict["n"], Object::Int(7)));
2085 }
2086 }
2087
2088 #[test]
2089 fn a_long_that_is_not_a_number_is_an_error() {
2090 assert!(plain(b"Lnope\n.").is_err());
2092 }
2093
2094 #[test]
2095 fn truncated_string_length_does_not_allocate() {
2096 let bytes: &[u8] = &[0x80, 0x02, b'X', 0xff, 0xff, 0xff, 0xff, b'h', b'i', b'.'];
2098 assert!(matches!(plain(bytes).unwrap_err(), PickleError::Io(_)));
2099 }
2100
2101 #[test]
2102 fn unknown_reduce_is_kept_opaque() {
2103 let bytes = b"\x80\x02cnumpy.core.multiarray\nscalar\nU\x02f8U\x08\x00\x00\x00\x00\x00\x00\xe0?\x86R.";
2105 assert!(matches!(
2106 plain(bytes).unwrap(),
2107 Object::Opaque(name) if name == "numpy.core.multiarray.scalar"
2108 ));
2109 }
2110
2111 #[test]
2112 fn unknown_call_consuming_tensor_data_is_an_error() {
2113 let source = fixture_source();
2114 let callable = Object::Class {
2115 module_name: "torch._utils".to_string(),
2116 name: "_rebuild_qtensor".to_string(),
2117 };
2118 let args = rebuild_args("FloatStorage", "0", 32, 0, &[2], &[1], &source);
2119 assert!(matches!(
2120 reduce(callable, args).unwrap_err(),
2121 PickleError::UnsupportedType(name) if name == "torch._utils._rebuild_qtensor"
2122 ));
2123 }
2124
2125 #[test]
2126 fn persistent_id_without_source_is_an_error() {
2127 let bytes = b"\x80\x02U\x07storageQ.";
2128 assert!(matches!(
2129 plain(bytes).unwrap_err(),
2130 PickleError::NoDataSource
2131 ));
2132 }
2133
2134 #[test]
2135 fn int_dict_keys_are_accepted_everywhere() {
2136 assert!(
2138 matches!(plain(b"\x80\x02}K\x01K\x02s.").unwrap(), Object::Dict(d) if d.contains_key("1"))
2139 );
2140 assert!(
2141 matches!(plain(b"\x80\x02(K\x03K\x04d.").unwrap(), Object::Dict(d) if d.contains_key("3"))
2142 );
2143 }
2144
2145 #[test]
2146 fn stride_rejects_negative_values() {
2147 let negative_stride = Object::Tuple(vec![Object::Int(-1)]);
2148 assert!(matches!(
2149 parse_dims(&negative_stride, "stride"),
2150 Err(PickleError::InvalidData(msg)) if msg == "stride must be non-negative, got -1"
2151 ));
2152 }
2153
2154 #[test]
2155 fn storage_extent_rejects_rank_mismatch() {
2156 assert!(matches!(
2157 tensor_storage_extent(&[2, 3], &[3], 0),
2158 Err(PickleError::InvalidData(msg))
2159 if msg == "Tensor stride rank 1 does not match shape rank 2"
2160 ));
2161 }
2162
2163 #[test]
2164 fn storage_extent_rejects_overflow() {
2165 assert!(matches!(
2166 tensor_storage_extent(&[usize::MAX], &[2], 0),
2167 Err(PickleError::InvalidData(msg))
2168 if msg == "Tensor stride calculation overflows usize"
2169 ));
2170 }
2171
2172 #[test]
2173 fn strided_views_match_reference() {
2174 type Case = (
2180 &'static str,
2181 &'static [usize],
2182 &'static [usize],
2183 usize,
2184 &'static [f32],
2185 );
2186 let cases: &[Case] = &[
2187 (
2188 "channels_last",
2189 &[2, 3, 2, 2][..],
2190 &[12, 1, 6, 3][..],
2191 0,
2192 &[
2193 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,
2194 21.0, 13.0, 16.0, 19.0, 22.0, 14.0, 17.0, 20.0, 23.0,
2195 ][..],
2196 ),
2197 (
2198 "transposed",
2199 &[3, 4][..],
2200 &[1, 3][..],
2201 2,
2202 &[
2203 2.0, 5.0, 8.0, 11.0, 3.0, 6.0, 9.0, 12.0, 4.0, 7.0, 10.0, 13.0,
2204 ][..],
2205 ),
2206 (
2207 "step_slice",
2208 &[5][..],
2209 &[2][..],
2210 1,
2211 &[1.0, 3.0, 5.0, 7.0, 9.0][..],
2212 ),
2213 (
2214 "overlapping_windows",
2215 &[4, 3][..],
2216 &[1, 1][..],
2217 0,
2218 &[0.0, 1.0, 2.0, 1.0, 2.0, 3.0, 2.0, 3.0, 4.0, 3.0, 4.0, 5.0][..],
2219 ),
2220 (
2221 "size_one_dim_odd_stride",
2222 &[1, 3][..],
2223 &[12, 1][..],
2224 4,
2225 &[4.0, 5.0, 6.0][..],
2226 ),
2227 (
2228 "size_one_inner_dim",
2229 &[3, 1][..],
2230 &[1, 7][..],
2231 0,
2232 &[0.0, 1.0, 2.0][..],
2233 ),
2234 (
2235 "expand_inner",
2236 &[2, 3, 2][..],
2237 &[0, 1, 0][..],
2238 3,
2239 &[3.0, 3.0, 4.0, 4.0, 5.0, 5.0, 3.0, 3.0, 4.0, 4.0, 5.0, 5.0][..],
2240 ),
2241 ("empty_leading", &[0, 3][..], &[3, 1][..], 30, &[][..]),
2242 (
2243 "empty_trailing_at_end",
2244 &[2, 0][..],
2245 &[0, 1][..],
2246 32,
2247 &[][..],
2248 ),
2249 ];
2250 let source = fixture_source();
2251 for &(name, shape, stride, offset, expected) in cases {
2252 let to_i64 = |v: &[usize]| v.iter().map(|&x| x as i64).collect::<Vec<_>>();
2253 let tensor = rebuild(rebuild_args(
2254 "FloatStorage",
2255 "0",
2256 32,
2257 offset as i64,
2258 &to_i64(shape),
2259 &to_i64(stride),
2260 &source,
2261 ))
2262 .unwrap_or_else(|e| panic!("{name}: {e}"));
2263 let bytes = tensor.read().unwrap_or_else(|e| panic!("{name}: {e}"));
2264 let data: Vec<f32> = bytemuck::pod_collect_to_vec(&bytes);
2265 assert_eq!(data, expected, "{name}");
2266 }
2267 }
2268
2269 #[test]
2273 fn gather_strided_matches_the_definition() {
2274 fn reference(data: &[u8], shape: &[usize], stride: &[usize], size: usize) -> Vec<u8> {
2275 let num_elements: usize = shape.iter().product();
2276 (0..num_elements)
2277 .flat_map(|linear| {
2278 let index: usize = shape
2279 .iter()
2280 .zip(stride)
2281 .rev()
2282 .scan(linear, |remaining, (&dim, &step)| {
2283 let digit = *remaining % dim;
2284 *remaining /= dim;
2285 Some(digit * step)
2286 })
2287 .sum();
2288 data[index * size..][..size].iter().copied()
2289 })
2290 .collect()
2291 }
2292
2293 let cases: &[(&[usize], &[usize])] = &[
2294 (&[37, 70], &[1, 37]),
2295 (&[70, 37], &[1, 70]),
2296 (&[32, 64], &[1, 32]),
2297 (&[3, 5, 7], &[1, 3, 15]),
2298 (&[2, 3, 4, 5], &[60, 1, 15, 3]),
2299 (&[4, 6], &[12, 2]),
2300 (&[3, 4], &[0, 1]),
2301 (&[3, 1, 4, 1], &[1, 7, 3, 11]),
2302 (&[9], &[3]),
2303 ];
2304 for size in [1, 2, 4, 8] {
2305 let data: Vec<u8> = (0..30_000u32)
2306 .map(|i| u8::try_from(i % 251).unwrap())
2307 .collect();
2308 for &(shape, stride) in cases {
2309 let byte_len = shape.iter().product::<usize>() * size;
2310 assert_eq!(
2311 gather_strided(&data, shape, stride, size, byte_len),
2312 reference(&data, shape, stride, size),
2313 "{size}-byte elements, shape {shape:?}, stride {stride:?}"
2314 );
2315 }
2316 }
2317 }
2318
2319 #[test]
2322 fn transposed_is_offered_only_for_views_stored_transposed() {
2323 let source = fixture_source();
2324 let to_i64 = |dims: &[usize]| -> Vec<i64> {
2325 dims.iter().map(|&d| i64::try_from(d).unwrap()).collect()
2326 };
2327 let view = |offset: usize, shape: &[usize], stride: &[usize]| {
2328 rebuild(rebuild_args(
2329 "FloatStorage",
2330 "0",
2331 32,
2332 i64::try_from(offset).unwrap(),
2333 &to_i64(shape),
2334 &to_i64(stride),
2335 &source,
2336 ))
2337 .unwrap()
2338 };
2339
2340 for (offset, rows, cols) in [(0, 3, 4), (2, 3, 4), (5, 4, 6)] {
2341 let tensor = view(offset, &[rows, cols], &[1, rows]);
2342 let transposed = tensor.transposed().expect("a `.t()` view has a transpose");
2343 assert_eq!(transposed.shape(), [cols, rows]);
2344 assert!(transposed.transposed().is_none());
2345
2346 let gathered = &read_as::<f32>(&tensor);
2347 let expected: Vec<f32> = (0..cols)
2348 .flat_map(|j| (0..rows).map(move |i| gathered[i * cols + j]))
2349 .collect();
2350 assert_eq!(read_as::<f32>(&transposed), expected, "offset {offset}");
2351 }
2352
2353 for (shape, stride) in [
2354 (&[3, 4][..], &[4, 1][..]),
2355 (&[3, 1][..], &[1, 7][..]),
2356 (&[1, 3][..], &[12, 1][..]),
2357 (&[4, 3][..], &[1, 1][..]),
2358 (&[2, 3][..], &[1, 4][..]),
2359 (&[2, 3, 2][..], &[1, 2, 6][..]),
2360 ] {
2361 assert!(
2362 view(0, shape, stride).transposed().is_none(),
2363 "shape {shape:?}, stride {stride:?}"
2364 );
2365 }
2366 }
2367
2368 #[test]
2369 fn strided_views_for_other_element_sizes() {
2370 let source = fixture_source();
2373
2374 let tensor = rebuild(rebuild_args(
2375 "ShortStorage",
2376 "0",
2377 64,
2378 1,
2379 &[2, 3],
2380 &[1, 2],
2381 &source,
2382 ))
2383 .unwrap();
2384 assert_eq!(read_as::<i16>(&tensor), [0, 16256, 16384, 0, 0, 0]);
2385
2386 let tensor = rebuild(rebuild_args(
2387 "ByteStorage",
2388 "0",
2389 128,
2390 2,
2391 &[2, 4],
2392 &[1, 4],
2393 &source,
2394 ))
2395 .unwrap();
2396 assert_eq!(read_as::<u8>(&tensor), [0, 128, 0, 64, 0, 63, 64, 64]);
2397
2398 let tensor = rebuild(rebuild_args(
2399 "DoubleStorage",
2400 "0",
2401 16,
2402 0,
2403 &[2, 2],
2404 &[1, 2],
2405 &source,
2406 ))
2407 .unwrap();
2408 let bits: Vec<u64> = read_as::<f64>(&tensor)
2409 .iter()
2410 .map(|v| v.to_bits())
2411 .collect();
2412 assert_eq!(
2413 bits,
2414 vec![
2415 4575657221408423936,
2416 4656722015783223296,
2417 4629700418010611712,
2418 4674736414296899584
2419 ]
2420 );
2421 }
2422
2423 #[test]
2424 fn stride_rejects_non_integer_values() {
2425 let invalid_stride = Object::Tuple(vec![Object::String("one".to_string())]);
2426 assert!(matches!(
2427 parse_dims(&invalid_stride, "stride"),
2428 Err(PickleError::InvalidData(msg)) if msg == "stride must be an int, got str"
2429 ));
2430 }
2431
2432 #[test]
2433 fn view_beyond_declared_storage_fails_at_parse_time() {
2434 let source = fixture_source();
2437 let err = rebuild(rebuild_args(
2438 "FloatStorage",
2439 "0",
2440 32,
2441 30,
2442 &[2, 3],
2443 &[3, 1],
2444 &source,
2445 ))
2446 .unwrap_err();
2447 assert!(matches!(
2448 err,
2449 PickleError::InvalidData(msg) if msg.contains("needs 36 elements, but storage '0' declares 32")
2450 ));
2451 }
2452
2453 #[test]
2454 fn declared_storage_larger_than_file_fails_at_read_time() {
2455 let source = fixture_source();
2456 let tensor = rebuild(rebuild_args(
2457 "FloatStorage",
2458 "0",
2459 64,
2460 40,
2461 &[2, 3],
2462 &[3, 1],
2463 &source,
2464 ))
2465 .unwrap();
2466 let err = tensor.read().unwrap_err();
2467 assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
2468 assert!(
2469 err.to_string()
2470 .contains("requires 46 elements from storage '0', but only 32 are available"),
2471 "{err}"
2472 );
2473 }
2474
2475 #[test]
2478 fn truncated_transposed_read_reports_the_declared_shape() {
2479 let source = fixture_source();
2480 let tensor = rebuild(rebuild_args(
2481 "FloatStorage",
2482 "0",
2483 64,
2484 30,
2485 &[2, 3],
2486 &[1, 2],
2487 &source,
2488 ))
2489 .unwrap();
2490 let err = tensor.transposed().unwrap().read().unwrap_err();
2491 assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
2492 assert!(
2493 err.to_string()
2494 .contains("Tensor with shape [2, 3] requires"),
2495 "{err}"
2496 );
2497 }
2498
2499 #[test]
2502 fn gather_strided_handles_high_rank_singleton_views() {
2503 let rank = 100_000;
2504 let mut shape = vec![1; rank];
2505 let mut stride = vec![1; rank];
2506 shape.push(2);
2507 stride.push(2);
2508 let data = [7u8, 0, 9];
2509 assert_eq!(gather_strided(&data, &shape, &stride, 1, 2), [7, 9]);
2510 }
2511
2512 #[test]
2513 fn broadcast_view_above_limit_is_refused() {
2514 let source = fixture_source();
2515 let err = rebuild(rebuild_args(
2516 "FloatStorage",
2517 "0",
2518 32,
2519 0,
2520 &[1 << 21, 1 << 21],
2521 &[0, 0],
2522 &source,
2523 ))
2524 .unwrap_err();
2525 assert!(matches!(
2526 err,
2527 PickleError::InvalidData(msg) if msg.contains("byte limit")
2528 ));
2529 }
2530
2531 #[test]
2532 fn small_broadcast_view_loads() {
2533 let source = fixture_source();
2534 let tensor = rebuild(rebuild_args(
2535 "FloatStorage",
2536 "0",
2537 32,
2538 1,
2539 &[2, 3],
2540 &[0, 1],
2541 &source,
2542 ))
2543 .unwrap();
2544 assert_eq!(read_as::<f32>(&tensor), [1.0, 2.0, 3.0, 1.0, 2.0, 3.0]);
2545 }
2546
2547 #[test]
2548 fn legacy_rebuild_tensor_loads_contiguous_stride() {
2549 let source = fixture_source();
2550 let tensor = rebuild(rebuild_args(
2551 "FloatStorage",
2552 "0",
2553 32,
2554 5,
2555 &[2, 3],
2556 &[3, 1],
2557 &source,
2558 ))
2559 .unwrap();
2560 assert_eq!(read_as::<f32>(&tensor), [5.0, 6.0, 7.0, 8.0, 9.0, 10.0]);
2561 }
2562
2563 #[test]
2564 fn legacy_rebuild_tensor_loads_permuted_stride() {
2565 let source = fixture_source();
2566 let tensor = rebuild(rebuild_args(
2567 "FloatStorage",
2568 "0",
2569 32,
2570 5,
2571 &[2, 4, 3],
2572 &[12, 1, 4],
2573 &source,
2574 ))
2575 .unwrap();
2576 assert_eq!(
2577 read_as::<f32>(&tensor),
2578 [
2579 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,
2580 25.0, 18.0, 22.0, 26.0, 19.0, 23.0, 27.0, 20.0, 24.0, 28.0,
2581 ]
2582 );
2583 }
2584
2585 #[test]
2586 fn legacy_rebuild_tensor_loads_scalar_at_offset() {
2587 let source = fixture_source();
2588 let tensor = rebuild(rebuild_args("FloatStorage", "0", 32, 5, &[], &[], &source)).unwrap();
2589 assert_eq!(read_as::<f32>(&tensor), [5.0]);
2590 }
2591
2592 #[test]
2593 fn storage_view_offset_is_applied() {
2594 let source = fixture_source();
2595 let mut pid = match storage_pid("FloatStorage", "0", 32) {
2596 Object::Tuple(fields) => fields,
2597 _ => unreachable!(),
2598 };
2599 pid.push(Object::Tuple(vec![
2600 Object::String("7".to_string()),
2601 Object::Int(4),
2602 Object::Int(10),
2603 ]));
2604 let storage = resolve_storage_id(&pid, &source).unwrap();
2605 assert_eq!(storage.view_offset, 4);
2606 let tensor = build_tensor(storage, DType::F32, 1, vec![3], vec![1]).unwrap();
2607 assert_eq!(read_as::<f32>(&tensor), [5.0, 6.0, 7.0]);
2608 }
2609
2610 #[test]
2611 fn rebuild_v3_reads_untyped_storage_with_explicit_dtype() {
2612 let source = fixture_source();
2613 let pid = [
2614 Object::String("storage".to_string()),
2615 Object::Class {
2616 module_name: "torch".to_string(),
2617 name: "UntypedStorage".to_string(),
2618 },
2619 Object::String("0".to_string()),
2620 Object::String("cpu".to_string()),
2621 Object::Int(128),
2622 ];
2623 let storage = resolve_storage_id(&pid, &source).unwrap();
2624 assert_eq!(storage.dtype, None);
2625 let args = Object::Tuple(vec![
2626 Object::Storage(storage),
2627 Object::Int(0),
2628 Object::Tuple(vec![Object::Int(2)]),
2629 Object::Tuple(vec![Object::Int(1)]),
2630 Object::Bool(false),
2631 Object::None,
2632 Object::Class {
2633 module_name: "torch".to_string(),
2634 name: "uint32".to_string(),
2635 },
2636 ]);
2637 let Object::Tensor(tensor) = rebuild_tensor(args, TensorRebuild::V3).unwrap() else {
2638 panic!("expected tensor");
2639 };
2640 assert_eq!(tensor.dtype(), DType::U32);
2641 assert_eq!(read_as::<u32>(&tensor), [0, 1.0f32.to_bits()]);
2643 }
2644
2645 #[test]
2646 fn rebuild_from_type_v2_dispatches_inner_call() {
2647 let source = fixture_source();
2649 let callable = Object::Class {
2650 module_name: "torch._tensor".to_string(),
2651 name: "_rebuild_from_type_v2".to_string(),
2652 };
2653 let args = Object::Tuple(vec![
2654 Object::Class {
2655 module_name: "torch._utils".to_string(),
2656 name: "_rebuild_tensor".to_string(),
2657 },
2658 Object::Class {
2659 module_name: "torch".to_string(),
2660 name: "Tensor".to_string(),
2661 },
2662 rebuild_args("FloatStorage", "0", 32, 5, &[2, 3], &[3, 1], &source),
2663 Object::Dict(HashMap::new()),
2664 ]);
2665 let Object::Tensor(tensor) = reduce(callable, args).unwrap() else {
2666 panic!("expected tensor");
2667 };
2668 assert_eq!(read_as::<f32>(&tensor), [5.0, 6.0, 7.0, 8.0, 9.0, 10.0]);
2669 }
2670
2671 #[test]
2672 fn bool_bytes_are_normalized() {
2673 let source = fixture_source();
2675 let pid = storage_pid("BoolStorage", "0", 128);
2676 let Object::Tuple(pid) = pid else {
2677 unreachable!()
2678 };
2679 let storage = resolve_storage_id(&pid, &source).unwrap();
2680 let tensor = build_tensor(storage, DType::Bool, 0, vec![8], vec![1]).unwrap();
2681 assert_eq!(tensor.read().unwrap(), [0, 0, 0, 0, 0, 0, 1, 1]);
2682 }
2683
2684 #[test]
2685 fn untyped_storage_with_view_metadata_is_rejected() {
2686 let source = fixture_source();
2687 let pid = [
2688 Object::String("storage".to_string()),
2689 Object::Class {
2690 module_name: "torch".to_string(),
2691 name: "UntypedStorage".to_string(),
2692 },
2693 Object::String("0".to_string()),
2694 Object::String("cpu".to_string()),
2695 Object::Int(128),
2696 Object::Tuple(vec![
2697 Object::String("7".to_string()),
2698 Object::Int(4),
2699 Object::Int(10),
2700 ]),
2701 ];
2702 assert!(matches!(
2703 resolve_storage_id(&pid, &source),
2704 Err(PickleError::InvalidData(msg)) if msg.contains("untyped but carries view metadata")
2705 ));
2706 }
2707
2708 #[test]
2709 fn long_at_i64_boundary_does_not_fit() {
2710 assert!(int_from_le_bytes(&[0, 0, 0, 0, 0, 0, 0, 0x80, 0]).is_none());
2712 assert!(
2713 int_from_le_bytes(&[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0xff]).is_none()
2714 );
2715 assert_eq!(
2716 int_from_le_bytes(&[0, 0, 0, 0, 0, 0, 0, 0x80, 0xff]).unwrap(),
2717 i64::MIN
2718 );
2719 }
2720
2721 #[test]
2722 fn deep_nesting_is_rejected() {
2723 let mut bytes = vec![0x80, 0x02, b']'];
2725 bytes.extend(std::iter::repeat_n(0x85, 1500));
2726 bytes.push(b'.');
2727 assert!(matches!(
2728 plain(&bytes).unwrap_err(),
2729 PickleError::InvalidData(msg) if msg.contains("nesting exceeds")
2730 ));
2731
2732 let mut bytes = vec![0x80, 0x02, b']'];
2734 bytes.extend(std::iter::repeat_n(0x85, 900));
2735 bytes.push(b'.');
2736 assert!(plain(&bytes).is_ok());
2737 }
2738
2739 #[test]
2740 fn mark_never_becomes_a_value() {
2741 assert!(matches!(
2743 plain(b"\x80\x02(\x85.").unwrap_err(),
2744 PickleError::InvalidData(msg) if msg.contains("found MARK")
2745 ));
2746 assert!(plain(b"\x80\x02(q\x00.").is_err());
2748 assert!(plain(b"\x80\x02K\x01(.").is_err());
2750 assert!(matches!(
2752 plain(b"\x80\x02K\x01(0.").unwrap(),
2753 Object::Int(1)
2754 ));
2755 }
2756
2757 #[test]
2758 fn build_on_a_tensor_is_an_error() {
2759 let source = fixture_source();
2760 let tensor = rebuild(rebuild_args(
2761 "FloatStorage",
2762 "0",
2763 32,
2764 0,
2765 &[2],
2766 &[1],
2767 &source,
2768 ))
2769 .unwrap();
2770 let err = opaque(&Object::Tensor(tensor), &Object::Dict(HashMap::new())).unwrap_err();
2771 assert!(matches!(err, PickleError::UnsupportedType(_)));
2772 }
2773
2774 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.";
2780 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.";
2781
2782 #[test]
2783 fn module_build_becomes_its_state_dict() {
2784 for bytes in [MODULE_PICKLE_PROTO2, MODULE_PICKLE_PROTO4] {
2785 let Object::Dict(dict) = plain(bytes).unwrap_or_else(|e| panic!("{e}")) else {
2786 panic!("expected dict");
2787 };
2788 let mut keys: Vec<_> = dict.keys().cloned().collect();
2790 keys.sort();
2791 assert_eq!(keys, ["fc", "running"]);
2792 assert!(matches!(dict["running"], Object::Int(2)));
2793 let Object::Dict(child) = &dict["fc"] else {
2794 panic!("expected the child module as a dict");
2795 };
2796 assert_eq!(child.len(), 1);
2797 assert!(matches!(child["weight"], Object::Int(1)));
2798 }
2799 }
2800
2801 #[test]
2802 fn module_shared_under_two_names_is_built_under_both() {
2803 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.";
2807 let Object::Dict(checkpoint) = plain(bytes).unwrap_or_else(|e| panic!("{e}")) else {
2808 panic!("expected dict");
2809 };
2810 for key in ["model", "ema"] {
2811 let Object::Dict(root) = &checkpoint[key] else {
2812 panic!("expected {key} as a dict, got {:?}", checkpoint[key]);
2813 };
2814 for child in ["a", "b"] {
2815 assert!(
2816 matches!(&root[child], Object::Dict(d) if matches!(d["weight"], Object::Int(1))),
2817 "{key}.{child}: {:?}",
2818 root[child]
2819 );
2820 }
2821 }
2822 }
2823
2824 #[test]
2825 fn object_with_a_tensor_attribute_is_refused() {
2826 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.";
2829 let ids = PersistentIds::Storages(fixture_source());
2830 assert!(matches!(
2831 read_pickle(&mut Cursor::new(&bytes[..]), &ids),
2832 Err(PickleError::UnsupportedType(name)) if name == "__main__.C"
2833 ));
2834 }
2835
2836 #[test]
2837 fn object_without_module_tables_stays_opaque() {
2838 let bytes = b"\x80\x02c__main__\nM\nq\x00)\x81q\x01}q\x02X\x01\x00\x00\x00xq\x03K\x01sb.";
2840 assert!(matches!(
2841 plain(bytes).unwrap(),
2842 Object::Opaque(name) if name == "__main__.M"
2843 ));
2844 }
2845
2846 #[test]
2847 fn module_registering_a_name_twice_is_an_error() {
2848 let table = |value| Object::Dict(HashMap::from([("w".to_string(), value)]));
2849 let attrs = HashMap::from([
2850 ("_parameters".to_string(), table(Object::Int(1))),
2851 ("_buffers".to_string(), table(Object::Int(2))),
2852 ("_modules".to_string(), Object::Dict(HashMap::new())),
2853 ]);
2854 assert!(matches!(
2855 module_state_dict(attrs),
2856 Err(PickleError::InvalidData(msg)) if msg.contains("'w' more than once")
2857 ));
2858 }
2859
2860 #[test]
2861 fn module_state_dict_rejects_a_malformed_buffer_set() {
2862 let mut attrs =
2863 HashMap::from([("_non_persistent_buffers_set".to_string(), Object::Int(1))]);
2864 for table in MODULE_TABLES {
2865 attrs.insert(table.to_string(), Object::Dict(HashMap::new()));
2866 }
2867 assert!(matches!(
2868 module_state_dict(attrs),
2869 Err(PickleError::InvalidData(msg)) if msg.contains("must be a set, got int")
2870 ));
2871 }
2872
2873 #[test]
2874 fn set_reduce_is_read_as_a_list() {
2875 let items = match plain(
2877 b"\x80\x02c__builtin__\nset\nq\x00]q\x01X\x04\x00\x00\x00maskq\x02a\x85q\x03Rq\x04.",
2878 )
2879 .unwrap()
2880 {
2881 Object::List(items) => items,
2882 other => panic!("expected list, got {other:?}"),
2883 };
2884 assert!(matches!(&items[..], [Object::String(s)] if s == "mask"));
2885 assert!(matches!(
2886 plain(b"\x80\x02c__builtin__\nfrozenset\nq\x00]q\x01\x85q\x02Rq\x03.").unwrap(),
2887 Object::List(items) if items.is_empty()
2888 ));
2889 }
2890
2891 #[test]
2892 fn ordered_dict_rejects_non_list_argument() {
2893 let args = Object::Tuple(vec![Object::Int(3)]);
2894 assert!(matches!(
2895 ordered_dict(args),
2896 Err(PickleError::InvalidData(msg)) if msg.contains("must be a list")
2897 ));
2898 }
2899
2900 #[test]
2901 fn call_with_extra_arguments_is_an_error() {
2902 let args = Object::Tuple(vec![Object::List(Vec::new()), Object::Int(1)]);
2904 assert!(matches!(
2905 sequence_arg(args, "set"),
2906 Err(PickleError::InvalidData(msg)) if msg == "set takes one argument, got 2"
2907 ));
2908 }
2909
2910 #[test]
2911 fn missing_storage_returns_contextual_error() {
2912 let source = fixture_source();
2913 let tensor = rebuild(rebuild_args(
2914 "FloatStorage",
2915 "missing",
2916 6,
2917 0,
2918 &[2, 3],
2919 &[3, 1],
2920 &source,
2921 ))
2922 .unwrap();
2923 let err = tensor.read().unwrap_err();
2924 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
2925 assert!(
2926 err.to_string()
2927 .contains("Failed to read storage 'missing' for tensor with shape [2, 3]"),
2928 "{err}"
2929 );
2930 }
2931
2932 #[test]
2933 fn os_errors_keep_their_kind() {
2934 use std::io::Read;
2939 let dir = tempfile::tempdir().unwrap();
2940 let file = std::fs::File::create(dir.path().join("write_only.pt")).unwrap();
2941 let expected = (&file).read(&mut [0u8; 8]).unwrap_err();
2942 assert!(expected.raw_os_error().is_some(), "{expected}");
2943
2944 let source = Arc::new(StorageSource::Legacy(crate::storage::LegacySource::new(
2945 crate::storage::Backing::File(file),
2946 )));
2947 let args = rebuild_args("FloatStorage", "0", 3, 0, &[3], &[1], &source);
2948 let StorageSource::Legacy(legacy) = &*source else {
2949 unreachable!()
2950 };
2951 legacy.finish(&["0".to_string()], 0, 8 + 12).unwrap();
2952 let tensor = rebuild(args).unwrap();
2953
2954 let err = tensor.read().unwrap_err();
2955 assert_eq!(err.kind(), expected.kind(), "{err}");
2956 assert!(
2957 err.to_string()
2958 .contains("Failed to read storage '0' for tensor with shape [3]"),
2959 "{err}"
2960 );
2961 }
2962
2963 #[test]
2964 fn tensors_inside_lists_and_tuples_get_indexed_names() {
2965 let source = fixture_source();
2968 let w1 = rebuild(rebuild_args(
2969 "FloatStorage",
2970 "0",
2971 4,
2972 0,
2973 &[2, 2],
2974 &[2, 1],
2975 &source,
2976 ))
2977 .unwrap();
2978 let w2 = rebuild(rebuild_args(
2979 "FloatStorage",
2980 "0",
2981 9,
2982 0,
2983 &[3, 3],
2984 &[3, 1],
2985 &source,
2986 ))
2987 .unwrap();
2988 let b = rebuild(rebuild_args("FloatStorage", "0", 4, 0, &[4], &[1], &source)).unwrap();
2989
2990 let mut nested = HashMap::new();
2991 nested.insert("w".to_string(), Object::Tensor(w2));
2992
2993 let mut dict = HashMap::new();
2994 dict.insert(
2996 "weights".to_string(),
2997 Object::List(vec![
2998 Object::Tensor(w1),
2999 Object::Int(7),
3000 Object::Dict(nested),
3001 ]),
3002 );
3003 dict.insert("bias".to_string(), Object::Tuple(vec![Object::Tensor(b)]));
3004
3005 let mut tensors = extract_tensors(dict);
3006 assert_eq!(tensors.len(), 3);
3007 assert!(tensors.contains_key("weights.0"));
3008 assert!(tensors.contains_key("weights.2.w"));
3009 assert!(tensors.contains_key("bias.0"));
3010
3011 let tensor = tensors.remove("weights.0").unwrap();
3013 assert_eq!(read_as::<f32>(&tensor), [0.0, 1.0, 2.0, 3.0]);
3014 }
3015}