1use crate::storage::{StorageSource, read_exact_len};
15use crate::{DType, MAX_TENSOR_SIZE, Tensor};
16use byteorder::{BigEndian, LittleEndian, ReadBytesExt};
17use std::collections::HashMap;
18use std::io::{self, BufRead};
19use std::sync::Arc;
20
21const STRING_PREALLOC_BOUND: usize = 1 << 16;
24
25#[derive(Debug)]
27#[non_exhaustive]
28pub enum PickleError {
29 Io(io::Error),
30 InvalidOpCode(u8),
32 UnsupportedOpCode(OpCode),
35 InvalidProtocol(u8),
36 UnexpectedOpCode(OpCode),
37 UnsupportedType(String),
38 InvalidData(String),
39 StackUnderflow,
40 MemoNotFound(u32),
41 NoDataSource,
43}
44
45impl From<io::Error> for PickleError {
46 fn from(e: io::Error) -> Self {
47 PickleError::Io(e)
48 }
49}
50
51impl std::fmt::Display for PickleError {
52 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53 match self {
54 PickleError::Io(e) => write!(f, "IO error: {}", e),
55 PickleError::InvalidOpCode(code) => write!(
56 f,
57 "Invalid pickle opcode: 0x{:02x}. The file may be corrupted or use an unsupported pickle feature.",
58 code
59 ),
60 PickleError::UnsupportedOpCode(op) => {
61 write!(f, "Unsupported pickle opcode {:?}", op)
62 }
63 PickleError::InvalidProtocol(proto) => write!(
64 f,
65 "Invalid or unsupported pickle protocol version: {}. Supported versions are 0-5.",
66 proto
67 ),
68 PickleError::UnexpectedOpCode(op) => {
69 write!(f, "Unexpected pickle opcode {:?} in current context", op)
70 }
71 PickleError::UnsupportedType(ty) => write!(
72 f,
73 "Unsupported Python type '{}'. This may indicate a full model save rather than a state_dict.",
74 ty
75 ),
76 PickleError::InvalidData(msg) => write!(f, "Invalid data in pickle file: {}", msg),
77 PickleError::StackUnderflow => {
78 write!(f, "Pickle stack underflow - the file may be corrupted")
79 }
80 PickleError::MemoNotFound(idx) => write!(
81 f,
82 "Pickle memo reference {} not found - the file may be corrupted",
83 idx
84 ),
85 PickleError::NoDataSource => write!(
86 f,
87 "Pickle references tensor storages but no tensor data is available. Tensors can only be loaded from a PyTorch checkpoint file, not a plain pickle."
88 ),
89 }
90 }
91}
92
93impl std::error::Error for PickleError {}
94
95type Result<T> = std::result::Result<T, PickleError>;
96
97#[repr(u8)]
99#[derive(Debug, Eq, PartialEq, Clone, Copy)]
100#[non_exhaustive]
101pub enum OpCode {
102 Int = b'I',
104 Long = b'L',
105 Float = b'F',
106 Unicode = b'V',
107 Get = b'g',
108 Put = b'p',
109 PersId = b'P',
110 Global = b'c',
111 Mark = b'(',
112 Stop = b'.',
113 Pop = b'0',
114 PopMark = b'1',
115 Dup = b'2',
116 None = b'N',
117 Reduce = b'R',
118 Build = b'b',
119 Dict = b'd',
120 List = b'l',
121 Tuple = b't',
122 SetItem = b's',
123 Append = b'a',
124 BinInt = b'J',
126 BinInt1 = b'K',
127 BinInt2 = b'M',
128 BinFloat = b'G',
129 BinString = b'T',
130 ShortBinString = b'U',
131 BinUnicode = b'X',
132 EmptyTuple = b')',
133 EmptyList = b']',
134 EmptyDict = b'}',
135 Appends = b'e',
136 SetItems = b'u',
137 BinGet = b'h',
138 LongBinGet = b'j',
139 BinPut = b'q',
140 LongBinPut = b'r',
141 BinPersId = b'Q',
142 Proto = 0x80,
144 NewObj = 0x81,
145 Ext1 = 0x82,
146 Ext2 = 0x83,
147 Ext4 = 0x84,
148 Tuple1 = 0x85,
149 Tuple2 = 0x86,
150 Tuple3 = 0x87,
151 NewTrue = 0x88,
152 NewFalse = 0x89,
153 Long1 = 0x8a,
154 Long4 = 0x8b,
155 BinBytes = b'B',
157 ShortBinBytes = b'C',
158 ShortBinUnicode = 0x8c,
160 BinUnicode8 = 0x8d,
161 BinBytes8 = 0x8e,
162 EmptySet = 0x8f,
163 AddItems = 0x90,
164 FrozenSet = 0x91,
165 NewObjEx = 0x92,
166 StackGlobal = 0x93,
167 Memoize = 0x94,
168 Frame = 0x95,
169 ByteArray8 = 0x96,
171 NextBuffer = 0x97,
172 ReadonlyBuffer = 0x98,
173}
174
175impl TryFrom<u8> for OpCode {
176 type Error = u8;
177 fn try_from(value: u8) -> std::result::Result<Self, Self::Error> {
178 use OpCode::*;
179 Ok(match value {
180 b'I' => Int,
181 b'L' => Long,
182 b'F' => Float,
183 b'V' => Unicode,
184 b'g' => Get,
185 b'p' => Put,
186 b'P' => PersId,
187 b'c' => Global,
188 b'(' => Mark,
189 b'.' => Stop,
190 b'0' => Pop,
191 b'1' => PopMark,
192 b'2' => Dup,
193 b'N' => None,
194 b'R' => Reduce,
195 b'b' => Build,
196 b'd' => Dict,
197 b'l' => List,
198 b't' => Tuple,
199 b's' => SetItem,
200 b'a' => Append,
201 b'J' => BinInt,
202 b'K' => BinInt1,
203 b'M' => BinInt2,
204 b'G' => BinFloat,
205 b'T' => BinString,
206 b'U' => ShortBinString,
207 b'X' => BinUnicode,
208 b')' => EmptyTuple,
209 b']' => EmptyList,
210 b'}' => EmptyDict,
211 b'e' => Appends,
212 b'u' => SetItems,
213 b'h' => BinGet,
214 b'j' => LongBinGet,
215 b'q' => BinPut,
216 b'r' => LongBinPut,
217 b'Q' => BinPersId,
218 0x80 => Proto,
219 0x81 => NewObj,
220 0x82 => Ext1,
221 0x83 => Ext2,
222 0x84 => Ext4,
223 0x85 => Tuple1,
224 0x86 => Tuple2,
225 0x87 => Tuple3,
226 0x88 => NewTrue,
227 0x89 => NewFalse,
228 0x8a => Long1,
229 0x8b => Long4,
230 b'B' => BinBytes,
231 b'C' => ShortBinBytes,
232 0x8c => ShortBinUnicode,
233 0x8d => BinUnicode8,
234 0x8e => BinBytes8,
235 0x8f => EmptySet,
236 0x90 => AddItems,
237 0x91 => FrozenSet,
238 0x92 => NewObjEx,
239 0x93 => StackGlobal,
240 0x94 => Memoize,
241 0x95 => Frame,
242 0x96 => ByteArray8,
243 0x97 => NextBuffer,
244 0x98 => ReadonlyBuffer,
245 other => return Err(other),
246 })
247 }
248}
249
250#[derive(Debug, Clone)]
252pub struct StorageRef {
253 pub(crate) source: Arc<StorageSource>,
255 pub(crate) key: String,
257 pub(crate) dtype: Option<DType>,
259 pub(crate) reachable_bytes: usize,
262 pub(crate) view_offset: usize,
265}
266
267#[derive(Debug, Clone)]
268pub enum Object {
269 None,
270 Bool(bool),
271 Int(i64),
272 Float(f64),
273 String(String),
274 Bytes(Vec<u8>),
275 Tuple(Vec<Object>),
276 List(Vec<Object>),
277 Dict(HashMap<String, Object>),
278 Class {
279 module_name: String,
280 name: String,
281 },
282 Storage(StorageRef),
283 Opaque(String),
287 Tensor(Tensor),
288}
289
290impl Object {
291 fn node_count(&self) -> usize {
294 match self {
295 Object::Tuple(v) | Object::List(v) => {
296 1 + v.iter().map(|o| o.node_count()).sum::<usize>()
297 }
298 Object::Dict(m) => 1 + m.values().map(|o| o.node_count()).sum::<usize>(),
299 _ => 1,
300 }
301 }
302
303 fn payload_bytes(&self) -> usize {
305 match self {
306 Object::String(s) => s.len(),
307 Object::Bytes(b) => b.len(),
308 Object::Opaque(name) => name.len(),
309 Object::Tuple(v) | Object::List(v) => v.iter().map(|o| o.payload_bytes()).sum(),
310 Object::Dict(m) => m.values().map(|o| o.payload_bytes()).sum(),
311 _ => 0,
312 }
313 }
314
315 fn holds_tensor_data(&self) -> bool {
317 match self {
318 Object::Storage(_) | Object::Tensor(_) => true,
319 Object::Tuple(v) | Object::List(v) => v.iter().any(Object::holds_tensor_data),
320 Object::Dict(m) => m.values().any(Object::holds_tensor_data),
321 _ => false,
322 }
323 }
324
325 fn type_name(&self) -> &'static str {
326 match self {
327 Object::None => "None",
328 Object::Bool(_) => "bool",
329 Object::Int(_) => "int",
330 Object::Float(_) => "float",
331 Object::String(_) => "str",
332 Object::Bytes(_) => "bytes",
333 Object::Tuple(_) => "tuple",
334 Object::List(_) => "list",
335 Object::Dict(_) => "dict",
336 Object::Class { .. } => "class",
337 Object::Storage(_) => "storage",
338 Object::Opaque(_) => "object",
339 Object::Tensor(_) => "tensor",
340 }
341 }
342
343 pub(crate) fn python_type_name(&self) -> String {
346 match self {
347 Object::Class { module_name, name } => format!("{module_name}.{name}"),
348 Object::Opaque(name) => name.clone(),
349 Object::Tensor(_) => "torch.Tensor".to_string(),
350 Object::Storage(StorageRef { dtype: Some(_), .. }) => "torch.TypedStorage".to_string(),
351 Object::Storage(_) => "torch.UntypedStorage".to_string(),
352 other => other.type_name().to_string(),
353 }
354 }
355}
356
357pub(crate) enum PersistentIds {
359 Unavailable,
361 Storages(Arc<StorageSource>),
363 Tensors(HashMap<String, Tensor>),
365}
366
367pub(crate) fn read_pickle<R: BufRead>(r: &mut R, ids: &PersistentIds) -> Result<Object> {
369 Unpickler::new(ids).run(r)
370}
371
372pub(crate) fn storage_type_to_dtype(storage_type: &str) -> Result<DType> {
378 match storage_type {
379 "FloatStorage" => Ok(DType::F32),
380 "DoubleStorage" => Ok(DType::F64),
381 "HalfStorage" => Ok(DType::F16),
382 "BFloat16Storage" => Ok(DType::BF16),
383 "LongStorage" => Ok(DType::I64),
384 "IntStorage" => Ok(DType::I32),
385 "ShortStorage" => Ok(DType::I16),
386 "CharStorage" => Ok(DType::I8),
387 "ByteStorage" => Ok(DType::U8),
388 "BoolStorage" => Ok(DType::Bool),
389 _ => Err(PickleError::UnsupportedType(format!(
390 "torch.{storage_type}"
391 ))),
392 }
393}
394
395fn torch_dtype_to_dtype(name: &str) -> Result<DType> {
397 match name {
398 "float32" | "float" => Ok(DType::F32),
399 "float64" | "double" => Ok(DType::F64),
400 "float16" | "half" => Ok(DType::F16),
401 "bfloat16" => Ok(DType::BF16),
402 "int64" | "long" => Ok(DType::I64),
403 "int32" | "int" => Ok(DType::I32),
404 "int16" | "short" => Ok(DType::I16),
405 "int8" => Ok(DType::I8),
406 "uint8" => Ok(DType::U8),
407 "uint16" => Ok(DType::U16),
408 "uint32" => Ok(DType::U32),
409 "uint64" => Ok(DType::U64),
410 "bool" => Ok(DType::Bool),
411 _ => Err(PickleError::UnsupportedType(format!("torch.{name}"))),
412 }
413}
414
415pub(crate) fn non_negative(value: &Object, what: &str) -> Result<usize> {
417 match value {
418 Object::Int(i) => usize::try_from(*i)
419 .map_err(|_| PickleError::InvalidData(format!("{what} must be non-negative, got {i}"))),
420 other => Err(PickleError::InvalidData(format!(
421 "{what} must be an int, got {}",
422 other.type_name()
423 ))),
424 }
425}
426
427pub(crate) fn key_string(value: &Object, what: &str) -> Result<String> {
429 match value {
430 Object::String(s) => Ok(s.clone()),
431 Object::Int(i) => Ok(i.to_string()),
432 other => Err(PickleError::InvalidData(format!(
433 "{what} must be a str or int, got {}",
434 other.type_name()
435 ))),
436 }
437}
438
439fn resolve_storage_id(pid: &[Object], source: &Arc<StorageSource>) -> Result<StorageRef> {
441 let [tag, storage_type, key, _location, numel, view @ ..] = pid else {
442 return Err(PickleError::InvalidData(format!(
443 "storage persistent id has {} fields, expected at least 5",
444 pid.len()
445 )));
446 };
447 if !matches!(tag, Object::String(tag) if tag == "storage") {
448 return Err(PickleError::InvalidData(format!(
449 "persistent id tag must be 'storage', got {}",
450 tag.type_name()
451 )));
452 }
453
454 let type_name = match storage_type {
455 Object::Class { name, .. } | Object::String(name) => name.as_str(),
456 other => {
457 return Err(PickleError::InvalidData(format!(
458 "storage type must be a class, got {}",
459 other.type_name()
460 )));
461 }
462 };
463 let dtype = if type_name == "UntypedStorage" {
464 None
465 } else {
466 Some(storage_type_to_dtype(type_name)?)
467 };
468
469 let key = key_string(key, "storage key")?;
470 let numel = non_negative(numel, "storage element count")?;
471 let element_size = dtype.map_or(1, |dtype| dtype.size());
472 let byte_len = numel.checked_mul(element_size).ok_or_else(|| {
473 PickleError::InvalidData(format!("storage '{key}' byte length overflows usize"))
474 })?;
475
476 let (view_offset, reachable) = match view.first() {
480 None | Some(Object::None) => (0, numel),
481 Some(Object::Tuple(view)) if view.len() == 3 => {
482 if dtype.is_none() {
483 return Err(PickleError::InvalidData(format!(
484 "storage '{key}' is untyped but carries view metadata"
485 )));
486 }
487 let offset = non_negative(&view[1], "storage view offset")?;
488 let size = non_negative(&view[2], "storage view size")?;
489 let end = offset
490 .checked_add(size)
491 .filter(|&end| end <= numel)
492 .ok_or_else(|| {
493 PickleError::InvalidData(format!(
494 "storage '{key}' has a view of {size} elements at offset {offset}, past its {numel} elements"
495 ))
496 })?;
497 (offset, end)
498 }
499 Some(other) => {
500 return Err(PickleError::InvalidData(format!(
501 "storage view metadata must be None or a 3-tuple, got {}",
502 other.type_name()
503 )));
504 }
505 };
506
507 source
508 .declare(&key, byte_len, element_size)
509 .map_err(|err| PickleError::InvalidData(err.to_string()))?;
510
511 Ok(StorageRef {
512 source: source.clone(),
513 key,
514 dtype,
515 reachable_bytes: reachable * element_size,
517 view_offset,
518 })
519}
520
521fn resolve_persistent_id(pid: Object, ids: &PersistentIds) -> Result<Object> {
522 match ids {
523 PersistentIds::Unavailable => Err(PickleError::NoDataSource),
524 PersistentIds::Storages(source) => match pid {
525 Object::Tuple(fields) => Ok(Object::Storage(resolve_storage_id(&fields, source)?)),
526 other => Err(PickleError::InvalidData(format!(
527 "persistent id must be a tuple, got {}",
528 other.type_name()
529 ))),
530 },
531 PersistentIds::Tensors(tensors) => {
532 let key = key_string(&pid, "persistent id")?;
533 tensors
534 .get(&key)
535 .cloned()
536 .map(Object::Tensor)
537 .ok_or_else(|| {
538 PickleError::InvalidData(format!(
539 "persistent id '{key}' does not name a tensor in the archive"
540 ))
541 })
542 }
543 }
544}
545
546fn reduce(callable: Object, args: Object) -> Result<Object> {
552 let (module_name, name) = match &callable {
553 Object::Class { module_name, name } => (module_name.as_str(), name.as_str()),
554 _ => return opaque(&callable, &args),
555 };
556
557 match (module_name, name) {
558 ("collections", "OrderedDict") => ordered_dict(args),
559 ("torch._utils", "_rebuild_tensor") => rebuild_tensor(args, TensorRebuild::Legacy),
560 ("torch._utils", "_rebuild_tensor_v2") => rebuild_tensor(args, TensorRebuild::V2),
561 ("torch._utils", "_rebuild_tensor_v3") => rebuild_tensor(args, TensorRebuild::V3),
562 ("torch._utils", "_rebuild_parameter" | "_rebuild_parameter_with_state") => match args {
565 Object::Tuple(mut fields) if !fields.is_empty() => {
566 if fields[1..].iter().any(Object::holds_tensor_data) {
567 return Err(PickleError::UnsupportedType(format!(
568 "{name} with tensor-bearing state"
569 )));
570 }
571 Ok(fields.swap_remove(0))
572 }
573 other => Err(PickleError::InvalidData(format!(
574 "{name}: expected a non-empty argument tuple, got {}",
575 other.type_name()
576 ))),
577 },
578 ("torch._tensor", "_rebuild_from_type_v2") => match args {
582 Object::Tuple(mut fields) if fields.len() >= 3 => {
583 if fields.get(3).is_some_and(Object::holds_tensor_data) {
584 return Err(PickleError::UnsupportedType(
585 "tensor subclass with tensor-bearing state".to_string(),
586 ));
587 }
588 let inner_args = fields.swap_remove(2);
589 let func = fields.swap_remove(0);
590 reduce(func, inner_args)
591 }
592 other => Err(PickleError::InvalidData(format!(
593 "_rebuild_from_type_v2: expected at least 3 arguments, got {}",
594 other.type_name()
595 ))),
596 },
597 _ => opaque(&callable, &args),
598 }
599}
600
601fn opaque(callable: &Object, args: &Object) -> Result<Object> {
609 let name = callable.python_type_name();
610 if callable.holds_tensor_data() || args.holds_tensor_data() {
611 return Err(PickleError::UnsupportedType(name));
612 }
613 Ok(Object::Opaque(name))
614}
615
616fn ordered_dict(args: Object) -> Result<Object> {
618 let items = match args {
619 Object::Tuple(fields) if fields.is_empty() => Vec::new(),
620 Object::Tuple(mut fields) => match fields.swap_remove(0) {
621 Object::List(items) | Object::Tuple(items) => items,
622 other => {
623 return Err(PickleError::InvalidData(format!(
624 "OrderedDict argument must be a list of pairs, got {}",
625 other.type_name()
626 )));
627 }
628 },
629 other => {
630 return Err(PickleError::InvalidData(format!(
631 "OrderedDict arguments must be a tuple, got {}",
632 other.type_name()
633 )));
634 }
635 };
636
637 let mut dict = HashMap::with_capacity(items.len());
638 for item in items {
639 match item {
640 Object::List(mut pair) | Object::Tuple(mut pair) if pair.len() == 2 => {
641 let value = pair.pop().expect("pair has two items");
642 let key = pair.pop().expect("pair has two items");
643 dict.insert(dict_key(key)?, value);
644 }
645 other => {
646 return Err(PickleError::InvalidData(format!(
647 "OrderedDict items must be (key, value) pairs, got {}",
648 other.type_name()
649 )));
650 }
651 }
652 }
653 Ok(Object::Dict(dict))
654}
655
656#[derive(Clone, Copy)]
657enum TensorRebuild {
658 Legacy,
660 V2,
662 V3,
666}
667
668impl TensorRebuild {
669 fn name(self) -> &'static str {
670 match self {
671 Self::Legacy => "_rebuild_tensor",
672 Self::V2 => "_rebuild_tensor_v2",
673 Self::V3 => "_rebuild_tensor_v3",
674 }
675 }
676
677 fn arity(self) -> usize {
678 match self {
679 Self::Legacy => 4,
680 Self::V2 => 6,
681 Self::V3 => 7,
682 }
683 }
684}
685
686fn rebuild_tensor(args: Object, kind: TensorRebuild) -> Result<Object> {
687 let fn_name = kind.name();
688 let mut fields = match args {
689 Object::Tuple(fields) => fields,
690 other => {
691 return Err(PickleError::InvalidData(format!(
692 "{fn_name}: expected an argument tuple, got {}",
693 other.type_name()
694 )));
695 }
696 };
697 if fields.len() < kind.arity() {
698 return Err(PickleError::InvalidData(format!(
699 "{fn_name}: expected at least {} arguments, got {}",
700 kind.arity(),
701 fields.len()
702 )));
703 }
704
705 let storage = match std::mem::replace(&mut fields[0], Object::None) {
706 Object::Storage(storage) => storage,
707 other => {
708 return Err(PickleError::InvalidData(format!(
709 "{fn_name}: expected a storage, got {}",
710 other.type_name()
711 )));
712 }
713 };
714 let storage_offset = non_negative(&fields[1], "storage offset")?;
715 let shape = parse_dims(&fields[2], "shape")?;
716 let stride = parse_dims(&fields[3], "stride")?;
717
718 let dtype = match kind {
719 TensorRebuild::V3 => match &fields[6] {
720 Object::Class { name, .. } => torch_dtype_to_dtype(name)?,
721 other => {
722 return Err(PickleError::InvalidData(format!(
723 "{fn_name}: expected a torch dtype, got {}",
724 other.type_name()
725 )));
726 }
727 },
728 _ => storage.dtype.ok_or_else(|| {
729 PickleError::InvalidData(format!(
730 "{fn_name}: untyped storage '{}' needs an explicit dtype",
731 storage.key
732 ))
733 })?,
734 };
735 if let Some(storage_dtype) = storage.dtype
736 && storage_dtype != dtype
737 {
738 return Err(PickleError::InvalidData(format!(
739 "{fn_name}: storage '{}' holds {storage_dtype:?} but the tensor is {dtype:?}",
740 storage.key
741 )));
742 }
743
744 build_tensor(storage, dtype, storage_offset, shape, stride).map(Object::Tensor)
745}
746
747fn parse_dims(value: &Object, what: &str) -> Result<Vec<usize>> {
748 match value {
749 Object::Tuple(items) | Object::List(items) => {
750 items.iter().map(|item| non_negative(item, what)).collect()
751 }
752 other => Err(PickleError::InvalidData(format!(
753 "{what} must be a tuple, got {}",
754 other.type_name()
755 ))),
756 }
757}
758
759fn tensor_num_elements(shape: &[usize]) -> Result<usize> {
765 shape.iter().try_fold(1usize, |total, dim| {
766 total.checked_mul(*dim).ok_or_else(|| {
767 PickleError::InvalidData("Tensor shape element count overflows usize".to_string())
768 })
769 })
770}
771
772fn tensor_storage_extent(
777 shape: &[usize],
778 stride: &[usize],
779 storage_offset: usize,
780) -> Result<usize> {
781 if shape.len() != stride.len() {
782 return Err(PickleError::InvalidData(format!(
783 "Tensor stride rank {} does not match shape rank {}",
784 stride.len(),
785 shape.len()
786 )));
787 }
788
789 if tensor_num_elements(shape)? == 0 {
790 return Ok(storage_offset);
791 }
792
793 let max_index = shape
794 .iter()
795 .zip(stride)
796 .try_fold(storage_offset, |index, (&dim, &step)| {
797 let offset = (dim - 1).checked_mul(step).ok_or_else(|| {
798 PickleError::InvalidData("Tensor stride calculation overflows usize".to_string())
799 })?;
800 index.checked_add(offset).ok_or_else(|| {
801 PickleError::InvalidData("Tensor storage extent overflows usize".to_string())
802 })
803 })?;
804
805 max_index.checked_add(1).ok_or_else(|| {
806 PickleError::InvalidData("Tensor storage extent overflows usize".to_string())
807 })
808}
809
810fn is_contiguous_stride(shape: &[usize], stride: &[usize]) -> bool {
814 let mut expected = 1usize;
815 for (&dim, &step) in shape.iter().zip(stride).rev() {
816 if dim > 1 && step != expected {
817 return false;
818 }
819 let Some(next) = expected.checked_mul(dim) else {
820 return false;
821 };
822 expected = next;
823 }
824 true
825}
826
827fn gather_strided(
832 data: &[u8],
833 shape: &[usize],
834 stride: &[usize],
835 element_size: usize,
836 byte_len: usize,
837) -> Vec<u8> {
838 let mut out = Vec::with_capacity(byte_len);
839 let num_elements = byte_len / element_size;
840 for linear_index in 0..num_elements {
841 let mut remaining = linear_index;
842 let mut storage_index = 0;
843 for (&dim, &step) in shape.iter().zip(stride).rev() {
844 storage_index += (remaining % dim) * step;
845 remaining /= dim;
846 }
847 let start = storage_index * element_size;
848 out.extend_from_slice(&data[start..start + element_size]);
849 }
850 out
851}
852
853fn to_native_endian(bytes: &mut [u8], element_size: usize) {
855 if cfg!(target_endian = "big") && element_size > 1 {
856 for element in bytes.chunks_exact_mut(element_size) {
857 element.reverse();
858 }
859 }
860}
861
862pub(crate) fn build_tensor(
867 storage: StorageRef,
868 dtype: DType,
869 storage_offset: usize,
870 shape: Vec<usize>,
871 stride: Vec<usize>,
872) -> Result<Tensor> {
873 let element_size = dtype.size();
874 let storage_offset = storage_offset
875 .checked_add(storage.view_offset)
876 .ok_or_else(|| PickleError::InvalidData("Storage offset overflows usize".to_string()))?;
877 let num_elements = tensor_num_elements(&shape)?;
878 let extent = tensor_storage_extent(&shape, &stride, storage_offset)?;
879 let byte_len = num_elements.checked_mul(element_size).ok_or_else(|| {
880 PickleError::InvalidData("Tensor byte length overflows usize".to_string())
881 })?;
882
883 let declared_elements = storage.reachable_bytes / element_size;
884 if extent > declared_elements {
885 return Err(PickleError::InvalidData(format!(
886 "Tensor with shape {shape:?} and stride {stride:?} at offset {storage_offset} needs {extent} elements, but storage '{}' declares {declared_elements}",
887 storage.key
888 )));
889 }
890 if byte_len > MAX_TENSOR_SIZE {
892 return Err(PickleError::InvalidData(format!(
893 "Tensor with shape {shape:?} would materialize {byte_len} bytes, above the {MAX_TENSOR_SIZE} byte limit"
894 )));
895 }
896
897 let StorageRef { source, key, .. } = storage;
898 let provider_shape = shape.clone();
899 let window_start = storage_offset.saturating_mul(element_size);
902 let window_len = (extent - storage_offset).saturating_mul(element_size);
903 let provider = move || -> io::Result<Vec<u8>> {
904 let shape = &provider_shape;
905 let (mut data, skipped) = source.read(&key, window_start, window_len).map_err(|err| {
906 let kind = match err.kind() {
911 kind if err.raw_os_error().is_some() => kind,
912 io::ErrorKind::UnexpectedEof => io::ErrorKind::UnexpectedEof,
913 _ => io::ErrorKind::InvalidData,
914 };
915 io::Error::new(
916 kind,
917 format!("Failed to read storage '{key}' for tensor with shape {shape:?}: {err}"),
918 )
919 })?;
920
921 let available = (skipped + data.len()) / element_size;
923 if extent > available {
924 return Err(io::Error::new(
925 io::ErrorKind::UnexpectedEof,
926 format!(
927 "Tensor with shape {shape:?} requires {extent} elements from storage '{key}', but only {available} are available"
928 ),
929 ));
930 }
931
932 let mut bytes = if is_contiguous_stride(shape, &stride) {
933 data.truncate(byte_len);
934 data
935 } else {
936 gather_strided(&data, shape, &stride, element_size, byte_len)
937 };
938
939 to_native_endian(&mut bytes, element_size);
940 if dtype == DType::Bool {
941 for byte in &mut bytes {
944 *byte = u8::from(*byte != 0);
945 }
946 }
947
948 Ok(bytes)
949 };
950
951 Ok(Tensor::new(String::new(), dtype, shape, provider))
954}
955
956const MAX_COPIED_NODES: usize = 1_000_000;
963
964const MAX_COPIED_BYTES: usize = 64 << 20;
967
968const MAX_NESTING_DEPTH: u32 = 1000;
974
975#[derive(Clone)]
977struct Entry {
978 object: Object,
979 depth: u32,
980}
981
982struct Unpickler<'a> {
983 stack: Vec<Entry>,
984 marks: Vec<usize>,
986 memo: HashMap<u32, Entry>,
987 copied_nodes: usize,
988 copied_bytes: usize,
989 ids: &'a PersistentIds,
990}
991
992impl<'a> Unpickler<'a> {
993 fn new(ids: &'a PersistentIds) -> Self {
994 Self {
995 stack: Vec::new(),
996 marks: Vec::new(),
997 memo: HashMap::new(),
998 copied_nodes: 0,
999 copied_bytes: 0,
1000 ids,
1001 }
1002 }
1003
1004 fn push_scalar(&mut self, object: Object) {
1005 self.stack.push(Entry { object, depth: 0 });
1006 }
1007
1008 fn push(&mut self, object: Object, depth: u32) -> Result<()> {
1009 if depth > MAX_NESTING_DEPTH {
1010 return Err(PickleError::InvalidData(format!(
1011 "pickle nesting exceeds {MAX_NESTING_DEPTH} levels"
1012 )));
1013 }
1014 self.stack.push(Entry { object, depth });
1015 Ok(())
1016 }
1017
1018 fn mark_on_top(&self) -> bool {
1020 self.marks.last() == Some(&self.stack.len())
1021 }
1022
1023 fn pop(&mut self) -> Result<Entry> {
1024 if self.mark_on_top() {
1025 return Err(PickleError::InvalidData(
1026 "expected a value on the stack, found MARK".to_string(),
1027 ));
1028 }
1029 self.stack.pop().ok_or(PickleError::StackUnderflow)
1030 }
1031
1032 fn pop_object(&mut self) -> Result<Object> {
1033 self.pop().map(|entry| entry.object)
1034 }
1035
1036 fn top(&self) -> Result<&Entry> {
1037 if self.mark_on_top() {
1038 return Err(PickleError::InvalidData(
1039 "expected a value on the stack, found MARK".to_string(),
1040 ));
1041 }
1042 self.stack.last().ok_or(PickleError::StackUnderflow)
1043 }
1044
1045 fn top_mut(&mut self) -> Result<&mut Entry> {
1046 if self.mark_on_top() {
1047 return Err(PickleError::InvalidData(
1048 "expected a value on the stack, found MARK".to_string(),
1049 ));
1050 }
1051 self.stack.last_mut().ok_or(PickleError::StackUnderflow)
1052 }
1053
1054 fn pop_to_mark(&mut self) -> Result<(Vec<Object>, u32)> {
1058 let mark = self
1059 .marks
1060 .pop()
1061 .ok_or_else(|| PickleError::InvalidData("MARK not found on stack".to_string()))?;
1062 let entries = self.stack.split_off(mark);
1063 let depth = entries.iter().map(|entry| entry.depth).max().unwrap_or(0);
1064 Ok((
1065 entries.into_iter().map(|entry| entry.object).collect(),
1066 depth,
1067 ))
1068 }
1069
1070 fn charge_copy(&mut self, object: &Object) -> Result<()> {
1072 self.copied_nodes += object.node_count();
1073 self.copied_bytes += object.payload_bytes();
1074 if self.copied_nodes > MAX_COPIED_NODES || self.copied_bytes > MAX_COPIED_BYTES {
1075 return Err(PickleError::InvalidData(format!(
1076 "Pickle memo bomb detected: copies exceeded {MAX_COPIED_NODES} nodes or {MAX_COPIED_BYTES} bytes"
1077 )));
1078 }
1079 Ok(())
1080 }
1081
1082 fn memo_get(&mut self, idx: u32) -> Result<Entry> {
1083 let entry = self
1084 .memo
1085 .get(&idx)
1086 .ok_or(PickleError::MemoNotFound(idx))?
1087 .clone();
1088 self.charge_copy(&entry.object)?;
1089 Ok(entry)
1090 }
1091
1092 fn memo_put(&mut self, idx: u32) -> Result<()> {
1093 let entry = self.top()?.clone();
1094 self.charge_copy(&entry.object)?;
1095 self.memo.insert(idx, entry);
1096 Ok(())
1097 }
1098
1099 fn append(&mut self, items: Vec<Object>, items_depth: u32, op: OpCode) -> Result<()> {
1101 let top = self.top_mut()?;
1102 let Object::List(list) = &mut top.object else {
1103 return Err(PickleError::UnexpectedOpCode(op));
1104 };
1105 list.extend(items);
1106 let depth = top.depth.max(items_depth + 1);
1107 top.depth = depth;
1108 if depth > MAX_NESTING_DEPTH {
1109 return Err(PickleError::InvalidData(format!(
1110 "pickle nesting exceeds {MAX_NESTING_DEPTH} levels"
1111 )));
1112 }
1113 Ok(())
1114 }
1115
1116 fn set_items(&mut self, items: Vec<Object>, items_depth: u32, op: OpCode) -> Result<()> {
1118 let top = self.top_mut()?;
1119 let Object::Dict(dict) = &mut top.object else {
1120 return Err(PickleError::UnexpectedOpCode(op));
1121 };
1122 insert_pairs(dict, items)?;
1123 let depth = top.depth.max(items_depth + 1);
1124 top.depth = depth;
1125 if depth > MAX_NESTING_DEPTH {
1126 return Err(PickleError::InvalidData(format!(
1127 "pickle nesting exceeds {MAX_NESTING_DEPTH} levels"
1128 )));
1129 }
1130 Ok(())
1131 }
1132
1133 fn run<R: BufRead>(mut self, r: &mut R) -> Result<Object> {
1134 loop {
1135 let byte = r.read_u8()?;
1136 let op = OpCode::try_from(byte).map_err(PickleError::InvalidOpCode)?;
1137 match op {
1138 OpCode::Proto => {
1139 let version = r.read_u8()?;
1140 if version > 5 {
1141 return Err(PickleError::InvalidProtocol(version));
1142 }
1143 }
1144 OpCode::Frame => {
1145 r.read_u64::<LittleEndian>()?;
1147 }
1148 OpCode::Stop => break,
1149
1150 OpCode::None => self.push_scalar(Object::None),
1152 OpCode::NewTrue => self.push_scalar(Object::Bool(true)),
1153 OpCode::NewFalse => self.push_scalar(Object::Bool(false)),
1154 OpCode::Int => {
1155 let text = read_line(r)?;
1156 let value = match text.as_str() {
1158 "00" => Object::Bool(false),
1159 "01" => Object::Bool(true),
1160 _ => Object::Int(parse_int(&text)?),
1161 };
1162 self.push_scalar(value);
1163 }
1164 OpCode::Long => {
1165 let text = read_line(r)?;
1166 let text = text.strip_suffix('L').unwrap_or(&text);
1167 self.push_scalar(parse_long(text)?);
1168 }
1169 OpCode::Float => {
1170 let text = read_line(r)?;
1171 let value = text.parse::<f64>().map_err(|e| {
1172 PickleError::InvalidData(format!("Invalid FLOAT value '{text}': {e}"))
1173 })?;
1174 self.push_scalar(Object::Float(value));
1175 }
1176 OpCode::Unicode => {
1177 let text = read_raw_unicode_escape_line(r)?;
1178 self.push_scalar(Object::String(text));
1179 }
1180 OpCode::BinInt => {
1181 let v = r.read_i32::<LittleEndian>()?;
1182 self.push_scalar(Object::Int(v as i64));
1183 }
1184 OpCode::BinInt1 => {
1185 let v = r.read_u8()?;
1186 self.push_scalar(Object::Int(v as i64));
1187 }
1188 OpCode::BinInt2 => {
1189 let v = r.read_u16::<LittleEndian>()?;
1190 self.push_scalar(Object::Int(v as i64));
1191 }
1192 OpCode::BinFloat => {
1193 let v = r.read_f64::<BigEndian>()?;
1194 self.push_scalar(Object::Float(v));
1195 }
1196 OpCode::Long1 => {
1197 let len = r.read_u8()? as u64;
1198 let bytes = read_exact_len(r, len, STRING_PREALLOC_BOUND)?;
1199 self.push_scalar(long_object(&bytes));
1200 }
1201 OpCode::Long4 => {
1202 let len = r.read_u32::<LittleEndian>()? as u64;
1203 let bytes = read_exact_len(r, len, STRING_PREALLOC_BOUND)?;
1204 self.push_scalar(long_object(&bytes));
1205 }
1206
1207 OpCode::ShortBinUnicode => {
1209 let len = r.read_u8()? as u64;
1210 self.push_scalar(read_string(r, len)?);
1211 }
1212 OpCode::BinUnicode => {
1213 let len = r.read_u32::<LittleEndian>()? as u64;
1214 self.push_scalar(read_string(r, len)?);
1215 }
1216 OpCode::ShortBinString => {
1217 let len = r.read_u8()? as u64;
1218 self.push_scalar(read_py2_string(r, len)?);
1219 }
1220 OpCode::BinString => {
1221 let len = r.read_u32::<LittleEndian>()? as u64;
1222 self.push_scalar(read_py2_string(r, len)?);
1223 }
1224 OpCode::BinUnicode8 => {
1225 let len = r.read_u64::<LittleEndian>()?;
1226 self.push_scalar(read_string(r, len)?);
1227 }
1228 OpCode::ShortBinBytes => {
1229 let len = r.read_u8()? as u64;
1230 self.push_scalar(Object::Bytes(read_bytes(r, len)?));
1231 }
1232 OpCode::BinBytes => {
1233 let len = r.read_u32::<LittleEndian>()? as u64;
1234 self.push_scalar(Object::Bytes(read_bytes(r, len)?));
1235 }
1236 OpCode::BinBytes8 | OpCode::ByteArray8 => {
1237 let len = r.read_u64::<LittleEndian>()?;
1238 self.push_scalar(Object::Bytes(read_bytes(r, len)?));
1239 }
1240
1241 OpCode::Mark => self.marks.push(self.stack.len()),
1243 OpCode::EmptyTuple => self.push_scalar(Object::Tuple(Vec::new())),
1244 OpCode::EmptyList | OpCode::EmptySet => self.push_scalar(Object::List(Vec::new())),
1245 OpCode::EmptyDict => self.push_scalar(Object::Dict(HashMap::new())),
1246 OpCode::Tuple => {
1247 let (items, depth) = self.pop_to_mark()?;
1248 self.push(Object::Tuple(items), depth + 1)?;
1249 }
1250 OpCode::Tuple1 => {
1251 let a = self.pop()?;
1252 self.push(Object::Tuple(vec![a.object]), a.depth + 1)?;
1253 }
1254 OpCode::Tuple2 => {
1255 let b = self.pop()?;
1256 let a = self.pop()?;
1257 let depth = a.depth.max(b.depth) + 1;
1258 self.push(Object::Tuple(vec![a.object, b.object]), depth)?;
1259 }
1260 OpCode::Tuple3 => {
1261 let c = self.pop()?;
1262 let b = self.pop()?;
1263 let a = self.pop()?;
1264 let depth = a.depth.max(b.depth).max(c.depth) + 1;
1265 self.push(Object::Tuple(vec![a.object, b.object, c.object]), depth)?;
1266 }
1267 OpCode::List | OpCode::FrozenSet => {
1268 let (items, depth) = self.pop_to_mark()?;
1269 self.push(Object::List(items), depth + 1)?;
1270 }
1271 OpCode::Append => {
1272 let value = self.pop()?;
1273 self.append(vec![value.object], value.depth, op)?;
1274 }
1275 OpCode::Appends | OpCode::AddItems => {
1276 let (items, depth) = self.pop_to_mark()?;
1277 self.append(items, depth, op)?;
1278 }
1279 OpCode::Dict => {
1280 let (items, depth) = self.pop_to_mark()?;
1281 let mut dict = HashMap::with_capacity(items.len() / 2);
1282 insert_pairs(&mut dict, items)?;
1283 self.push(Object::Dict(dict), depth + 1)?;
1284 }
1285 OpCode::SetItem => {
1286 let value = self.pop()?;
1287 let key = self.pop()?;
1288 let depth = key.depth.max(value.depth);
1289 self.set_items(vec![key.object, value.object], depth, op)?;
1290 }
1291 OpCode::SetItems => {
1292 let (items, depth) = self.pop_to_mark()?;
1293 self.set_items(items, depth, op)?;
1294 }
1295
1296 OpCode::Pop => {
1298 if self.mark_on_top() {
1300 self.marks.pop();
1301 } else {
1302 self.pop()?;
1303 }
1304 }
1305 OpCode::PopMark => {
1306 self.pop_to_mark()?;
1307 }
1308 OpCode::Dup => {
1309 let top = self.top()?.clone();
1310 self.charge_copy(&top.object)?;
1311 self.stack.push(top);
1312 }
1313
1314 OpCode::Get => {
1316 let idx = parse_index(&read_line(r)?)?;
1317 let entry = self.memo_get(idx)?;
1318 self.stack.push(entry);
1319 }
1320 OpCode::BinGet => {
1321 let idx = r.read_u8()? as u32;
1322 let entry = self.memo_get(idx)?;
1323 self.stack.push(entry);
1324 }
1325 OpCode::LongBinGet => {
1326 let idx = r.read_u32::<LittleEndian>()?;
1327 let entry = self.memo_get(idx)?;
1328 self.stack.push(entry);
1329 }
1330 OpCode::Put => {
1331 let idx = parse_index(&read_line(r)?)?;
1332 self.memo_put(idx)?;
1333 }
1334 OpCode::BinPut => {
1335 let idx = r.read_u8()? as u32;
1336 self.memo_put(idx)?;
1337 }
1338 OpCode::LongBinPut => {
1339 let idx = r.read_u32::<LittleEndian>()?;
1340 self.memo_put(idx)?;
1341 }
1342 OpCode::Memoize => {
1343 let idx = self.memo.len() as u32;
1344 self.memo_put(idx)?;
1345 }
1346
1347 OpCode::Global => {
1349 let module_name = read_line(r)?;
1350 let name = read_line(r)?;
1351 self.push_scalar(Object::Class { module_name, name });
1352 }
1353 OpCode::StackGlobal => {
1354 let name = self.pop_object()?;
1355 let module_name = self.pop_object()?;
1356 match (module_name, name) {
1357 (Object::String(module_name), Object::String(name)) => {
1358 self.push_scalar(Object::Class { module_name, name })
1359 }
1360 _ => {
1361 return Err(PickleError::InvalidData(
1362 "STACK_GLOBAL expects two strings".to_string(),
1363 ));
1364 }
1365 }
1366 }
1367 OpCode::PersId => {
1368 let pid = Object::String(read_line(r)?);
1369 let obj = resolve_persistent_id(pid, self.ids)?;
1370 self.push_scalar(obj);
1371 }
1372 OpCode::BinPersId => {
1373 let pid = self.pop_object()?;
1374 let obj = resolve_persistent_id(pid, self.ids)?;
1375 self.push_scalar(obj);
1376 }
1377 OpCode::Reduce => {
1378 let args = self.pop()?;
1379 let callable = self.pop_object()?;
1380 let obj = reduce(callable, args.object)?;
1382 self.push(obj, args.depth)?;
1383 }
1384 OpCode::NewObj => {
1385 let args = self.pop_object()?;
1386 let cls = self.pop_object()?;
1387 let obj = opaque(&cls, &args)?;
1388 self.push_scalar(obj);
1389 }
1390 OpCode::NewObjEx => {
1391 let kwargs = self.pop_object()?;
1392 let args = self.pop_object()?;
1393 let cls = self.pop_object()?;
1394 opaque(&cls, &kwargs)?;
1395 let obj = opaque(&cls, &args)?;
1396 self.push_scalar(obj);
1397 }
1398 OpCode::Build => {
1399 let state = self.pop()?;
1400 let object = self.pop()?;
1401 match (object.object, state.object) {
1402 (Object::Dict(mut dict), Object::Dict(update)) => {
1403 dict.extend(update);
1404 self.push(Object::Dict(dict), object.depth.max(state.depth))?;
1405 }
1406 (dict @ Object::Dict(_), state) => {
1409 if state.holds_tensor_data() {
1410 return Err(PickleError::UnsupportedType(
1411 "dict with tensor-bearing state".to_string(),
1412 ));
1413 }
1414 self.push(dict, object.depth)?;
1415 }
1416 (object, state) => {
1417 let obj = opaque(&object, &state)?;
1418 self.push_scalar(obj);
1419 }
1420 }
1421 }
1422
1423 OpCode::Ext1
1424 | OpCode::Ext2
1425 | OpCode::Ext4
1426 | OpCode::NextBuffer
1427 | OpCode::ReadonlyBuffer => return Err(PickleError::UnsupportedOpCode(op)),
1428 }
1429 }
1430
1431 self.pop_object()
1432 }
1433}
1434
1435fn read_line<R: BufRead>(r: &mut R) -> Result<String> {
1436 let mut data = Vec::with_capacity(32);
1437 r.read_until(b'\n', &mut data)?;
1438 if data.pop() != Some(b'\n') {
1439 return Err(PickleError::Io(io::Error::new(
1440 io::ErrorKind::UnexpectedEof,
1441 "unterminated text field",
1442 )));
1443 }
1444 if data.last() == Some(&b'\r') {
1445 data.pop();
1446 }
1447 String::from_utf8(data).map_err(|e| PickleError::InvalidData(format!("Invalid UTF-8: {e}")))
1448}
1449
1450fn read_raw_unicode_escape_line<R: BufRead>(r: &mut R) -> Result<String> {
1453 let mut data = Vec::with_capacity(32);
1454 r.read_until(b'\n', &mut data)?;
1455 if data.pop() != Some(b'\n') {
1456 return Err(PickleError::Io(io::Error::new(
1457 io::ErrorKind::UnexpectedEof,
1458 "unterminated text field",
1459 )));
1460 }
1461
1462 let mut text = String::with_capacity(data.len());
1463 let mut i = 0;
1464 while i < data.len() {
1465 let byte = data[i];
1466 let escape_len = match (byte, data.get(i + 1)) {
1467 (b'\\', Some(b'u')) => Some(4),
1468 (b'\\', Some(b'U')) => Some(8),
1469 _ => None,
1470 };
1471 match escape_len {
1472 Some(digits) => {
1473 let hex = data
1474 .get(i + 2..i + 2 + digits)
1475 .and_then(|hex| std::str::from_utf8(hex).ok())
1476 .and_then(|hex| u32::from_str_radix(hex, 16).ok())
1477 .and_then(char::from_u32)
1478 .ok_or_else(|| {
1479 PickleError::InvalidData("Invalid raw-unicode-escape sequence".to_string())
1480 })?;
1481 text.push(hex);
1482 i += 2 + digits;
1483 }
1484 None => {
1485 text.push(char::from(byte));
1486 i += 1;
1487 }
1488 }
1489 }
1490 Ok(text)
1491}
1492
1493fn read_bytes<R: BufRead>(r: &mut R, len: u64) -> Result<Vec<u8>> {
1494 Ok(read_exact_len(r, len, STRING_PREALLOC_BOUND)?)
1495}
1496
1497fn read_string<R: BufRead>(r: &mut R, len: u64) -> Result<Object> {
1498 let s = String::from_utf8(read_bytes(r, len)?)
1499 .map_err(|e| PickleError::InvalidData(format!("Invalid UTF-8: {e}")))?;
1500 Ok(Object::String(s))
1501}
1502
1503fn read_py2_string<R: BufRead>(r: &mut R, len: u64) -> Result<Object> {
1506 Ok(match String::from_utf8(read_bytes(r, len)?) {
1507 Ok(text) => Object::String(text),
1508 Err(err) => Object::Bytes(err.into_bytes()),
1509 })
1510}
1511
1512fn parse_int(text: &str) -> Result<i64> {
1513 text.parse::<i64>()
1514 .map_err(|e| PickleError::InvalidData(format!("Invalid integer '{text}': {e}")))
1515}
1516
1517fn parse_index(text: &str) -> Result<u32> {
1518 text.parse::<u32>()
1519 .map_err(|e| PickleError::InvalidData(format!("Invalid memo index '{text}': {e}")))
1520}
1521
1522fn long_object(bytes: &[u8]) -> Object {
1529 int_from_le_bytes(bytes).map_or_else(|| Object::Opaque("int".to_string()), Object::Int)
1530}
1531
1532fn parse_long(text: &str) -> Result<Object> {
1535 use std::num::IntErrorKind;
1536
1537 match text.parse::<i64>() {
1538 Ok(value) => Ok(Object::Int(value)),
1539 Err(e)
1540 if matches!(
1541 e.kind(),
1542 IntErrorKind::PosOverflow | IntErrorKind::NegOverflow
1543 ) =>
1544 {
1545 Ok(Object::Opaque("int".to_string()))
1546 }
1547 Err(e) => Err(PickleError::InvalidData(format!(
1548 "Invalid integer '{text}': {e}"
1549 ))),
1550 }
1551}
1552
1553fn int_from_le_bytes(bytes: &[u8]) -> Option<i64> {
1555 let negative = bytes.last().is_some_and(|b| b & 0x80 != 0);
1556 let fill = if negative { 0xff } else { 0x00 };
1557 let fits = bytes.len() <= 8
1560 || (bytes[8..].iter().all(|&b| b == fill) && (bytes[7] & 0x80 != 0) == negative);
1561 if !fits {
1562 return None;
1563 }
1564 let mut buf = [fill; 8];
1565 let len = bytes.len().min(8);
1566 buf[..len].copy_from_slice(&bytes[..len]);
1567 Some(i64::from_le_bytes(buf))
1568}
1569
1570fn dict_key(key: Object) -> Result<String> {
1571 match key {
1572 Object::String(s) => Ok(s),
1573 other => key_string(&other, "dict key"),
1574 }
1575}
1576
1577fn insert_pairs(dict: &mut HashMap<String, Object>, items: Vec<Object>) -> Result<()> {
1578 if !items.len().is_multiple_of(2) {
1579 return Err(PickleError::InvalidData(
1580 "dict items must come in key/value pairs".to_string(),
1581 ));
1582 }
1583 let mut items = items.into_iter();
1584 while let (Some(key), Some(value)) = (items.next(), items.next()) {
1585 dict.insert(dict_key(key)?, value);
1586 }
1587 Ok(())
1588}
1589
1590pub(crate) fn extract_tensors(dict: HashMap<String, Object>) -> HashMap<String, Tensor> {
1603 fn walk(obj: Object, path: &mut Vec<String>, tensors: &mut HashMap<String, Tensor>) {
1604 match obj {
1605 Object::Dict(dict) => {
1606 for (key, value) in dict {
1607 path.push(key);
1608 walk(value, path, tensors);
1609 path.pop();
1610 }
1611 }
1612 Object::List(items) | Object::Tuple(items) => {
1613 for (index, item) in items.into_iter().enumerate() {
1614 path.push(index.to_string());
1615 walk(item, path, tensors);
1616 path.pop();
1617 }
1618 }
1619 Object::Tensor(mut tensor) => {
1620 tensor.name = path.join(".");
1621 tensors.insert(tensor.name.clone(), tensor);
1622 }
1623 _ => {}
1624 }
1625 }
1626 let mut tensors = HashMap::new();
1627 walk(Object::Dict(dict), &mut Vec::new(), &mut tensors);
1628 tensors
1629}
1630
1631#[cfg(test)]
1632mod tests {
1633 use super::*;
1634 use crate::tests::{read_as, test_data_path};
1635 use std::io::Cursor;
1636
1637 fn fixture_source() -> Arc<StorageSource> {
1638 let path = test_data_path("non_contiguous.pt");
1639 Arc::new(StorageSource::Zip(
1640 crate::storage::ZipSource::open(&path).unwrap(),
1641 ))
1642 }
1643
1644 fn storage_pid(storage_type: &str, key: &str, numel: i64) -> Object {
1645 Object::Tuple(vec![
1646 Object::String("storage".to_string()),
1647 Object::Class {
1648 module_name: "torch".to_string(),
1649 name: storage_type.to_string(),
1650 },
1651 Object::String(key.to_string()),
1652 Object::String("cpu".to_string()),
1653 Object::Int(numel),
1654 ])
1655 }
1656
1657 fn rebuild_args(
1658 storage_type: &str,
1659 key: &str,
1660 numel: i64,
1661 offset: i64,
1662 shape: &[i64],
1663 stride: &[i64],
1664 source: &Arc<StorageSource>,
1665 ) -> Object {
1666 let storage = match &storage_pid(storage_type, key, numel) {
1667 Object::Tuple(fields) => resolve_storage_id(fields, source).unwrap(),
1668 _ => unreachable!(),
1669 };
1670 Object::Tuple(vec![
1671 Object::Storage(storage),
1672 Object::Int(offset),
1673 Object::Tuple(shape.iter().copied().map(Object::Int).collect()),
1674 Object::Tuple(stride.iter().copied().map(Object::Int).collect()),
1675 ])
1676 }
1677
1678 fn rebuild(args: Object) -> Result<Tensor> {
1679 match rebuild_tensor(args, TensorRebuild::Legacy)? {
1680 Object::Tensor(tensor) => Ok(tensor),
1681 other => panic!("expected tensor, got {other:?}"),
1682 }
1683 }
1684
1685 fn plain(bytes: &[u8]) -> Result<Object> {
1686 read_pickle(&mut Cursor::new(bytes), &PersistentIds::Unavailable)
1687 }
1688
1689 #[test]
1690 fn test_memo_bomb_mitigation() {
1691 let mut poc = vec![0x89, b'q', 0x00];
1693 let n = 20; for _ in 0..n {
1695 poc.extend_from_slice(&[b'h', 0x00, b'h', 0x00, 0x86, b'q', 0x00]);
1696 }
1697 poc.push(b'.');
1698
1699 let err = plain(&poc).unwrap_err();
1700 assert!(matches!(err, PickleError::InvalidData(msg) if msg.contains("memo bomb")));
1701 }
1702
1703 #[test]
1704 fn memo_copies_of_large_payloads_are_bounded() {
1705 let payload = vec![0xabu8; 2 << 20];
1707 let mut bytes = vec![0x80, 0x02, b'B'];
1708 bytes.extend_from_slice(&(payload.len() as u32).to_le_bytes());
1709 bytes.extend_from_slice(&payload);
1710 bytes.extend_from_slice(b"q\x00(");
1711 for _ in 0..40 {
1712 bytes.extend_from_slice(b"h\x00");
1713 }
1714 bytes.extend_from_slice(b"t.");
1715 assert!(matches!(
1716 plain(&bytes).unwrap_err(),
1717 PickleError::InvalidData(msg) if msg.contains("memo bomb")
1718 ));
1719 }
1720
1721 #[test]
1722 fn dup_doubling_is_bounded() {
1723 let mut bytes = vec![0x80, 0x02, b'K', 0x01];
1725 for _ in 0..40 {
1726 bytes.extend_from_slice(b"2\x86");
1727 }
1728 bytes.push(b'.');
1729 assert!(matches!(
1730 plain(&bytes).unwrap_err(),
1731 PickleError::InvalidData(msg) if msg.contains("memo bomb")
1732 ));
1733 }
1734
1735 #[test]
1736 fn unicode_opcode_decodes_raw_unicode_escape() {
1737 let bytes = b"V\xe9t\xe9 \\u6a21\np0\n.";
1739 assert!(matches!(
1740 plain(bytes).unwrap(),
1741 Object::String(s) if s == "\u{e9}t\u{e9} \u{6a21}"
1742 ));
1743 assert!(plain(b"V\\u12.\n.").is_err());
1744 }
1745
1746 #[test]
1747 fn rebuild_from_type_v2_rejects_tensor_bearing_state() {
1748 let source = fixture_source();
1749 let callable = Object::Class {
1750 module_name: "torch._tensor".to_string(),
1751 name: "_rebuild_from_type_v2".to_string(),
1752 };
1753 let mut state = HashMap::new();
1754 state.insert(
1755 "extra".to_string(),
1756 rebuild_args("FloatStorage", "0", 32, 0, &[2], &[1], &source),
1757 );
1758 let args = Object::Tuple(vec![
1759 Object::Class {
1760 module_name: "torch._utils".to_string(),
1761 name: "_rebuild_tensor".to_string(),
1762 },
1763 Object::Class {
1764 module_name: "torch".to_string(),
1765 name: "Tensor".to_string(),
1766 },
1767 rebuild_args("FloatStorage", "0", 32, 5, &[2, 3], &[3, 1], &source),
1768 Object::Dict(state),
1769 ]);
1770 assert!(matches!(
1771 reduce(callable, args).unwrap_err(),
1772 PickleError::UnsupportedType(_)
1773 ));
1774 }
1775
1776 #[test]
1777 fn protocol_4_pickle_parses() {
1778 let bytes: &[u8] = &[
1780 0x80, 0x04, 0x95, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7d, 0x94, 0x28,
1781 0x8c, 0x01, 0x61, 0x94, 0x4b, 0x01, 0x8c, 0x01, 0x62, 0x94, 0x5d, 0x94, 0x28, 0x47,
1782 0x3f, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x88, 0x4e, 0x65, 0x8c, 0x01, 0x63,
1783 0x94, 0x43, 0x02, 0x78, 0x79, 0x94, 0x8c, 0x01, 0x64, 0x94, 0x7d, 0x94, 0x8c, 0x01,
1784 0x65, 0x94, 0x8c, 0x01, 0x66, 0x94, 0x73, 0x75, 0x2e,
1785 ];
1786 let Object::Dict(dict) = plain(bytes).unwrap() else {
1787 panic!("expected dict");
1788 };
1789 assert!(matches!(dict["a"], Object::Int(1)));
1790 let Object::List(items) = &dict["b"] else {
1791 panic!("expected list");
1792 };
1793 assert!(
1794 matches!(items[..], [Object::Float(f), Object::Bool(true), Object::None] if f == 1.5)
1795 );
1796 assert!(matches!(&dict["c"], Object::Bytes(b) if b == b"xy"));
1797 let Object::Dict(inner) = &dict["d"] else {
1798 panic!("expected dict");
1799 };
1800 assert!(matches!(&inner["e"], Object::String(s) if s == "f"));
1801 }
1802
1803 #[test]
1804 fn stack_global_builds_class() {
1805 let bytes: &[u8] = &[
1807 0x80, 0x04, 0x95, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8c, 0x0b, b'c',
1808 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',
1809 b'r', b'd', b'e', b'r', b'e', b'd', b'D', b'i', b'c', b't', 0x94, 0x93, 0x94, 0x2e,
1810 ];
1811 assert!(matches!(
1812 plain(bytes).unwrap(),
1813 Object::Class { module_name, name } if module_name == "collections" && name == "OrderedDict"
1814 ));
1815 }
1816
1817 #[test]
1818 fn protocol_0_text_opcodes_parse() {
1819 let bytes =
1821 b"(dp0\nVn\np1\nI-3\nsVf\np2\nF2.5\nsVt\np3\nI01\nsVbig\np4\nL1099511627776L\ns.";
1822 let Object::Dict(dict) = plain(bytes).unwrap_or_else(|e| panic!("{e}")) else {
1823 panic!("expected dict");
1824 };
1825 assert!(matches!(dict["n"], Object::Int(-3)));
1826 assert!(matches!(dict["f"], Object::Float(f) if f == 2.5));
1827 assert!(matches!(dict["t"], Object::Bool(true)));
1828 assert!(matches!(dict["big"], Object::Int(1099511627776)));
1829 }
1830
1831 #[test]
1832 fn long1_handles_sign_and_width() {
1833 assert_eq!(int_from_le_bytes(&[]).unwrap(), 0);
1834 assert_eq!(int_from_le_bytes(&[0xff]).unwrap(), -1);
1835 assert_eq!(int_from_le_bytes(&[0x00, 0x01]).unwrap(), 256);
1836 assert_eq!(
1837 int_from_le_bytes(&[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0x00]).unwrap(),
1838 i64::MAX
1839 );
1840 assert!(
1841 int_from_le_bytes(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01]).is_none()
1842 );
1843 }
1844
1845 #[test]
1846 fn oversized_int_beside_weights_stays_opaque() {
1847 for bytes in [
1849 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(),
1850 b"(dp0\nVseed\np1\nL18446744073709551616L\nsVn\np2\nI7\ns.".as_slice(),
1851 ] {
1852 let Object::Dict(dict) = plain(bytes).unwrap_or_else(|e| panic!("{e}")) else {
1853 panic!("expected dict");
1854 };
1855 assert!(matches!(&dict["seed"], Object::Opaque(name) if name == "int"));
1856 assert!(matches!(dict["n"], Object::Int(7)));
1857 }
1858 }
1859
1860 #[test]
1861 fn a_long_that_is_not_a_number_is_an_error() {
1862 assert!(plain(b"Lnope\n.").is_err());
1864 }
1865
1866 #[test]
1867 fn truncated_string_length_does_not_allocate() {
1868 let bytes: &[u8] = &[0x80, 0x02, b'X', 0xff, 0xff, 0xff, 0xff, b'h', b'i', b'.'];
1870 assert!(matches!(plain(bytes).unwrap_err(), PickleError::Io(_)));
1871 }
1872
1873 #[test]
1874 fn unknown_reduce_is_kept_opaque() {
1875 let bytes = b"\x80\x02cnumpy.core.multiarray\nscalar\nU\x02f8U\x08\x00\x00\x00\x00\x00\x00\xe0?\x86R.";
1877 assert!(matches!(
1878 plain(bytes).unwrap(),
1879 Object::Opaque(name) if name == "numpy.core.multiarray.scalar"
1880 ));
1881 }
1882
1883 #[test]
1884 fn unknown_call_consuming_tensor_data_is_an_error() {
1885 let source = fixture_source();
1886 let callable = Object::Class {
1887 module_name: "torch._utils".to_string(),
1888 name: "_rebuild_qtensor".to_string(),
1889 };
1890 let args = rebuild_args("FloatStorage", "0", 32, 0, &[2], &[1], &source);
1891 assert!(matches!(
1892 reduce(callable, args).unwrap_err(),
1893 PickleError::UnsupportedType(name) if name == "torch._utils._rebuild_qtensor"
1894 ));
1895 }
1896
1897 #[test]
1898 fn persistent_id_without_source_is_an_error() {
1899 let bytes = b"\x80\x02U\x07storageQ.";
1900 assert!(matches!(
1901 plain(bytes).unwrap_err(),
1902 PickleError::NoDataSource
1903 ));
1904 }
1905
1906 #[test]
1907 fn int_dict_keys_are_accepted_everywhere() {
1908 assert!(
1910 matches!(plain(b"\x80\x02}K\x01K\x02s.").unwrap(), Object::Dict(d) if d.contains_key("1"))
1911 );
1912 assert!(
1913 matches!(plain(b"\x80\x02(K\x03K\x04d.").unwrap(), Object::Dict(d) if d.contains_key("3"))
1914 );
1915 }
1916
1917 #[test]
1918 fn stride_rejects_negative_values() {
1919 let negative_stride = Object::Tuple(vec![Object::Int(-1)]);
1920 assert!(matches!(
1921 parse_dims(&negative_stride, "stride"),
1922 Err(PickleError::InvalidData(msg)) if msg == "stride must be non-negative, got -1"
1923 ));
1924 }
1925
1926 #[test]
1927 fn storage_extent_rejects_rank_mismatch() {
1928 assert!(matches!(
1929 tensor_storage_extent(&[2, 3], &[3], 0),
1930 Err(PickleError::InvalidData(msg))
1931 if msg == "Tensor stride rank 1 does not match shape rank 2"
1932 ));
1933 }
1934
1935 #[test]
1936 fn storage_extent_rejects_overflow() {
1937 assert!(matches!(
1938 tensor_storage_extent(&[usize::MAX], &[2], 0),
1939 Err(PickleError::InvalidData(msg))
1940 if msg == "Tensor stride calculation overflows usize"
1941 ));
1942 }
1943
1944 #[test]
1945 fn strided_views_match_reference() {
1946 type Case = (
1952 &'static str,
1953 &'static [usize],
1954 &'static [usize],
1955 usize,
1956 &'static [f32],
1957 );
1958 let cases: &[Case] = &[
1959 (
1960 "channels_last",
1961 &[2, 3, 2, 2][..],
1962 &[12, 1, 6, 3][..],
1963 0,
1964 &[
1965 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,
1966 21.0, 13.0, 16.0, 19.0, 22.0, 14.0, 17.0, 20.0, 23.0,
1967 ][..],
1968 ),
1969 (
1970 "transposed",
1971 &[3, 4][..],
1972 &[1, 3][..],
1973 2,
1974 &[
1975 2.0, 5.0, 8.0, 11.0, 3.0, 6.0, 9.0, 12.0, 4.0, 7.0, 10.0, 13.0,
1976 ][..],
1977 ),
1978 (
1979 "step_slice",
1980 &[5][..],
1981 &[2][..],
1982 1,
1983 &[1.0, 3.0, 5.0, 7.0, 9.0][..],
1984 ),
1985 (
1986 "overlapping_windows",
1987 &[4, 3][..],
1988 &[1, 1][..],
1989 0,
1990 &[0.0, 1.0, 2.0, 1.0, 2.0, 3.0, 2.0, 3.0, 4.0, 3.0, 4.0, 5.0][..],
1991 ),
1992 (
1993 "size_one_dim_odd_stride",
1994 &[1, 3][..],
1995 &[12, 1][..],
1996 4,
1997 &[4.0, 5.0, 6.0][..],
1998 ),
1999 (
2000 "size_one_inner_dim",
2001 &[3, 1][..],
2002 &[1, 7][..],
2003 0,
2004 &[0.0, 1.0, 2.0][..],
2005 ),
2006 (
2007 "expand_inner",
2008 &[2, 3, 2][..],
2009 &[0, 1, 0][..],
2010 3,
2011 &[3.0, 3.0, 4.0, 4.0, 5.0, 5.0, 3.0, 3.0, 4.0, 4.0, 5.0, 5.0][..],
2012 ),
2013 ("empty_leading", &[0, 3][..], &[3, 1][..], 30, &[][..]),
2014 (
2015 "empty_trailing_at_end",
2016 &[2, 0][..],
2017 &[0, 1][..],
2018 32,
2019 &[][..],
2020 ),
2021 ];
2022 let source = fixture_source();
2023 for &(name, shape, stride, offset, expected) in cases {
2024 let to_i64 = |v: &[usize]| v.iter().map(|&x| x as i64).collect::<Vec<_>>();
2025 let tensor = rebuild(rebuild_args(
2026 "FloatStorage",
2027 "0",
2028 32,
2029 offset as i64,
2030 &to_i64(shape),
2031 &to_i64(stride),
2032 &source,
2033 ))
2034 .unwrap_or_else(|e| panic!("{name}: {e}"));
2035 let bytes = tensor.read().unwrap_or_else(|e| panic!("{name}: {e}"));
2036 let data: Vec<f32> = bytemuck::pod_collect_to_vec(&bytes);
2037 assert_eq!(data, expected, "{name}");
2038 }
2039 }
2040
2041 #[test]
2042 fn strided_views_for_other_element_sizes() {
2043 let source = fixture_source();
2046
2047 let tensor = rebuild(rebuild_args(
2048 "ShortStorage",
2049 "0",
2050 64,
2051 1,
2052 &[2, 3],
2053 &[1, 2],
2054 &source,
2055 ))
2056 .unwrap();
2057 assert_eq!(read_as::<i16>(&tensor), [0, 16256, 16384, 0, 0, 0]);
2058
2059 let tensor = rebuild(rebuild_args(
2060 "ByteStorage",
2061 "0",
2062 128,
2063 2,
2064 &[2, 4],
2065 &[1, 4],
2066 &source,
2067 ))
2068 .unwrap();
2069 assert_eq!(read_as::<u8>(&tensor), [0, 128, 0, 64, 0, 63, 64, 64]);
2070
2071 let tensor = rebuild(rebuild_args(
2072 "DoubleStorage",
2073 "0",
2074 16,
2075 0,
2076 &[2, 2],
2077 &[1, 2],
2078 &source,
2079 ))
2080 .unwrap();
2081 let bits: Vec<u64> = read_as::<f64>(&tensor)
2082 .iter()
2083 .map(|v| v.to_bits())
2084 .collect();
2085 assert_eq!(
2086 bits,
2087 vec![
2088 4575657221408423936,
2089 4656722015783223296,
2090 4629700418010611712,
2091 4674736414296899584
2092 ]
2093 );
2094 }
2095
2096 #[test]
2097 fn stride_rejects_non_integer_values() {
2098 let invalid_stride = Object::Tuple(vec![Object::String("one".to_string())]);
2099 assert!(matches!(
2100 parse_dims(&invalid_stride, "stride"),
2101 Err(PickleError::InvalidData(msg)) if msg == "stride must be an int, got str"
2102 ));
2103 }
2104
2105 #[test]
2106 fn view_beyond_declared_storage_fails_at_parse_time() {
2107 let source = fixture_source();
2110 let err = rebuild(rebuild_args(
2111 "FloatStorage",
2112 "0",
2113 32,
2114 30,
2115 &[2, 3],
2116 &[3, 1],
2117 &source,
2118 ))
2119 .unwrap_err();
2120 assert!(matches!(
2121 err,
2122 PickleError::InvalidData(msg) if msg.contains("needs 36 elements, but storage '0' declares 32")
2123 ));
2124 }
2125
2126 #[test]
2127 fn declared_storage_larger_than_file_fails_at_read_time() {
2128 let source = fixture_source();
2129 let tensor = rebuild(rebuild_args(
2130 "FloatStorage",
2131 "0",
2132 64,
2133 40,
2134 &[2, 3],
2135 &[3, 1],
2136 &source,
2137 ))
2138 .unwrap();
2139 let err = tensor.read().unwrap_err();
2140 assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
2141 assert!(
2142 err.to_string()
2143 .contains("requires 46 elements from storage '0', but only 32 are available"),
2144 "{err}"
2145 );
2146 }
2147
2148 #[test]
2149 fn broadcast_view_above_limit_is_refused() {
2150 let source = fixture_source();
2151 let err = rebuild(rebuild_args(
2152 "FloatStorage",
2153 "0",
2154 32,
2155 0,
2156 &[1 << 21, 1 << 21],
2157 &[0, 0],
2158 &source,
2159 ))
2160 .unwrap_err();
2161 assert!(matches!(
2162 err,
2163 PickleError::InvalidData(msg) if msg.contains("byte limit")
2164 ));
2165 }
2166
2167 #[test]
2168 fn small_broadcast_view_loads() {
2169 let source = fixture_source();
2170 let tensor = rebuild(rebuild_args(
2171 "FloatStorage",
2172 "0",
2173 32,
2174 1,
2175 &[2, 3],
2176 &[0, 1],
2177 &source,
2178 ))
2179 .unwrap();
2180 assert_eq!(read_as::<f32>(&tensor), [1.0, 2.0, 3.0, 1.0, 2.0, 3.0]);
2181 }
2182
2183 #[test]
2184 fn legacy_rebuild_tensor_loads_contiguous_stride() {
2185 let source = fixture_source();
2186 let tensor = rebuild(rebuild_args(
2187 "FloatStorage",
2188 "0",
2189 32,
2190 5,
2191 &[2, 3],
2192 &[3, 1],
2193 &source,
2194 ))
2195 .unwrap();
2196 assert_eq!(read_as::<f32>(&tensor), [5.0, 6.0, 7.0, 8.0, 9.0, 10.0]);
2197 }
2198
2199 #[test]
2200 fn legacy_rebuild_tensor_loads_permuted_stride() {
2201 let source = fixture_source();
2202 let tensor = rebuild(rebuild_args(
2203 "FloatStorage",
2204 "0",
2205 32,
2206 5,
2207 &[2, 4, 3],
2208 &[12, 1, 4],
2209 &source,
2210 ))
2211 .unwrap();
2212 assert_eq!(
2213 read_as::<f32>(&tensor),
2214 [
2215 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,
2216 25.0, 18.0, 22.0, 26.0, 19.0, 23.0, 27.0, 20.0, 24.0, 28.0,
2217 ]
2218 );
2219 }
2220
2221 #[test]
2222 fn legacy_rebuild_tensor_loads_scalar_at_offset() {
2223 let source = fixture_source();
2224 let tensor = rebuild(rebuild_args("FloatStorage", "0", 32, 5, &[], &[], &source)).unwrap();
2225 assert_eq!(read_as::<f32>(&tensor), [5.0]);
2226 }
2227
2228 #[test]
2229 fn storage_view_offset_is_applied() {
2230 let source = fixture_source();
2231 let mut pid = match storage_pid("FloatStorage", "0", 32) {
2232 Object::Tuple(fields) => fields,
2233 _ => unreachable!(),
2234 };
2235 pid.push(Object::Tuple(vec![
2236 Object::String("7".to_string()),
2237 Object::Int(4),
2238 Object::Int(10),
2239 ]));
2240 let storage = resolve_storage_id(&pid, &source).unwrap();
2241 assert_eq!(storage.view_offset, 4);
2242 let tensor = build_tensor(storage, DType::F32, 1, vec![3], vec![1]).unwrap();
2243 assert_eq!(read_as::<f32>(&tensor), [5.0, 6.0, 7.0]);
2244 }
2245
2246 #[test]
2247 fn rebuild_v3_reads_untyped_storage_with_explicit_dtype() {
2248 let source = fixture_source();
2249 let pid = [
2250 Object::String("storage".to_string()),
2251 Object::Class {
2252 module_name: "torch".to_string(),
2253 name: "UntypedStorage".to_string(),
2254 },
2255 Object::String("0".to_string()),
2256 Object::String("cpu".to_string()),
2257 Object::Int(128),
2258 ];
2259 let storage = resolve_storage_id(&pid, &source).unwrap();
2260 assert_eq!(storage.dtype, None);
2261 let args = Object::Tuple(vec![
2262 Object::Storage(storage),
2263 Object::Int(0),
2264 Object::Tuple(vec![Object::Int(2)]),
2265 Object::Tuple(vec![Object::Int(1)]),
2266 Object::Bool(false),
2267 Object::None,
2268 Object::Class {
2269 module_name: "torch".to_string(),
2270 name: "uint32".to_string(),
2271 },
2272 ]);
2273 let Object::Tensor(tensor) = rebuild_tensor(args, TensorRebuild::V3).unwrap() else {
2274 panic!("expected tensor");
2275 };
2276 assert_eq!(tensor.dtype(), DType::U32);
2277 assert_eq!(read_as::<u32>(&tensor), [0, 1.0f32.to_bits()]);
2279 }
2280
2281 #[test]
2282 fn rebuild_from_type_v2_dispatches_inner_call() {
2283 let source = fixture_source();
2285 let callable = Object::Class {
2286 module_name: "torch._tensor".to_string(),
2287 name: "_rebuild_from_type_v2".to_string(),
2288 };
2289 let args = Object::Tuple(vec![
2290 Object::Class {
2291 module_name: "torch._utils".to_string(),
2292 name: "_rebuild_tensor".to_string(),
2293 },
2294 Object::Class {
2295 module_name: "torch".to_string(),
2296 name: "Tensor".to_string(),
2297 },
2298 rebuild_args("FloatStorage", "0", 32, 5, &[2, 3], &[3, 1], &source),
2299 Object::Dict(HashMap::new()),
2300 ]);
2301 let Object::Tensor(tensor) = reduce(callable, args).unwrap() else {
2302 panic!("expected tensor");
2303 };
2304 assert_eq!(read_as::<f32>(&tensor), [5.0, 6.0, 7.0, 8.0, 9.0, 10.0]);
2305 }
2306
2307 #[test]
2308 fn bool_bytes_are_normalized() {
2309 let source = fixture_source();
2311 let pid = storage_pid("BoolStorage", "0", 128);
2312 let Object::Tuple(pid) = pid else {
2313 unreachable!()
2314 };
2315 let storage = resolve_storage_id(&pid, &source).unwrap();
2316 let tensor = build_tensor(storage, DType::Bool, 0, vec![8], vec![1]).unwrap();
2317 assert_eq!(tensor.read().unwrap(), [0, 0, 0, 0, 0, 0, 1, 1]);
2318 }
2319
2320 #[test]
2321 fn untyped_storage_with_view_metadata_is_rejected() {
2322 let source = fixture_source();
2323 let pid = [
2324 Object::String("storage".to_string()),
2325 Object::Class {
2326 module_name: "torch".to_string(),
2327 name: "UntypedStorage".to_string(),
2328 },
2329 Object::String("0".to_string()),
2330 Object::String("cpu".to_string()),
2331 Object::Int(128),
2332 Object::Tuple(vec![
2333 Object::String("7".to_string()),
2334 Object::Int(4),
2335 Object::Int(10),
2336 ]),
2337 ];
2338 assert!(matches!(
2339 resolve_storage_id(&pid, &source),
2340 Err(PickleError::InvalidData(msg)) if msg.contains("untyped but carries view metadata")
2341 ));
2342 }
2343
2344 #[test]
2345 fn long_at_i64_boundary_does_not_fit() {
2346 assert!(int_from_le_bytes(&[0, 0, 0, 0, 0, 0, 0, 0x80, 0]).is_none());
2348 assert!(
2349 int_from_le_bytes(&[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0xff]).is_none()
2350 );
2351 assert_eq!(
2352 int_from_le_bytes(&[0, 0, 0, 0, 0, 0, 0, 0x80, 0xff]).unwrap(),
2353 i64::MIN
2354 );
2355 }
2356
2357 #[test]
2358 fn deep_nesting_is_rejected() {
2359 let mut bytes = vec![0x80, 0x02, b']'];
2361 bytes.extend(std::iter::repeat_n(0x85, 1500));
2362 bytes.push(b'.');
2363 assert!(matches!(
2364 plain(&bytes).unwrap_err(),
2365 PickleError::InvalidData(msg) if msg.contains("nesting exceeds")
2366 ));
2367
2368 let mut bytes = vec![0x80, 0x02, b']'];
2370 bytes.extend(std::iter::repeat_n(0x85, 900));
2371 bytes.push(b'.');
2372 assert!(plain(&bytes).is_ok());
2373 }
2374
2375 #[test]
2376 fn mark_never_becomes_a_value() {
2377 assert!(matches!(
2379 plain(b"\x80\x02(\x85.").unwrap_err(),
2380 PickleError::InvalidData(msg) if msg.contains("found MARK")
2381 ));
2382 assert!(plain(b"\x80\x02(q\x00.").is_err());
2384 assert!(plain(b"\x80\x02K\x01(.").is_err());
2386 assert!(matches!(
2388 plain(b"\x80\x02K\x01(0.").unwrap(),
2389 Object::Int(1)
2390 ));
2391 }
2392
2393 #[test]
2394 fn build_on_a_tensor_is_an_error() {
2395 let source = fixture_source();
2396 let tensor = rebuild(rebuild_args(
2397 "FloatStorage",
2398 "0",
2399 32,
2400 0,
2401 &[2],
2402 &[1],
2403 &source,
2404 ))
2405 .unwrap();
2406 let err = opaque(&Object::Tensor(tensor), &Object::Dict(HashMap::new())).unwrap_err();
2407 assert!(matches!(err, PickleError::UnsupportedType(_)));
2408 }
2409
2410 #[test]
2411 fn ordered_dict_rejects_non_list_argument() {
2412 let args = Object::Tuple(vec![Object::Int(3)]);
2413 assert!(matches!(
2414 ordered_dict(args),
2415 Err(PickleError::InvalidData(msg)) if msg.contains("list of pairs")
2416 ));
2417 }
2418
2419 #[test]
2420 fn missing_storage_returns_contextual_error() {
2421 let source = fixture_source();
2422 let tensor = rebuild(rebuild_args(
2423 "FloatStorage",
2424 "missing",
2425 6,
2426 0,
2427 &[2, 3],
2428 &[3, 1],
2429 &source,
2430 ))
2431 .unwrap();
2432 let err = tensor.read().unwrap_err();
2433 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
2434 assert!(
2435 err.to_string()
2436 .contains("Failed to read storage 'missing' for tensor with shape [2, 3]"),
2437 "{err}"
2438 );
2439 }
2440
2441 #[test]
2442 fn os_errors_keep_their_kind() {
2443 use std::io::Read;
2448 let dir = tempfile::tempdir().unwrap();
2449 let file = std::fs::File::create(dir.path().join("write_only.pt")).unwrap();
2450 let expected = (&file).read(&mut [0u8; 8]).unwrap_err();
2451 assert!(expected.raw_os_error().is_some(), "{expected}");
2452
2453 let source = Arc::new(StorageSource::Legacy(crate::storage::LegacySource::new(
2454 file,
2455 )));
2456 let args = rebuild_args("FloatStorage", "0", 3, 0, &[3], &[1], &source);
2457 let StorageSource::Legacy(legacy) = &*source else {
2458 unreachable!()
2459 };
2460 legacy.finish(&["0".to_string()], 0, 8 + 12).unwrap();
2461 let tensor = rebuild(args).unwrap();
2462
2463 let err = tensor.read().unwrap_err();
2464 assert_eq!(err.kind(), expected.kind(), "{err}");
2465 assert!(
2466 err.to_string()
2467 .contains("Failed to read storage '0' for tensor with shape [3]"),
2468 "{err}"
2469 );
2470 }
2471
2472 #[test]
2473 fn tensors_inside_lists_and_tuples_get_indexed_names() {
2474 let source = fixture_source();
2477 let w1 = rebuild(rebuild_args(
2478 "FloatStorage",
2479 "0",
2480 4,
2481 0,
2482 &[2, 2],
2483 &[2, 1],
2484 &source,
2485 ))
2486 .unwrap();
2487 let w2 = rebuild(rebuild_args(
2488 "FloatStorage",
2489 "0",
2490 9,
2491 0,
2492 &[3, 3],
2493 &[3, 1],
2494 &source,
2495 ))
2496 .unwrap();
2497 let b = rebuild(rebuild_args("FloatStorage", "0", 4, 0, &[4], &[1], &source)).unwrap();
2498
2499 let mut nested = HashMap::new();
2500 nested.insert("w".to_string(), Object::Tensor(w2));
2501
2502 let mut dict = HashMap::new();
2503 dict.insert(
2505 "weights".to_string(),
2506 Object::List(vec![
2507 Object::Tensor(w1),
2508 Object::Int(7),
2509 Object::Dict(nested),
2510 ]),
2511 );
2512 dict.insert("bias".to_string(), Object::Tuple(vec![Object::Tensor(b)]));
2513
2514 let mut tensors = extract_tensors(dict);
2515 assert_eq!(tensors.len(), 3);
2516 assert!(tensors.contains_key("weights.0"));
2517 assert!(tensors.contains_key("weights.2.w"));
2518 assert!(tensors.contains_key("bias.0"));
2519
2520 let tensor = tensors.remove("weights.0").unwrap();
2522 assert_eq!(read_as::<f32>(&tensor), [0.0, 1.0, 2.0, 3.0]);
2523 }
2524}