Skip to main content

pytorch_reader/
pickle_reader.rs

1//! Just enough pickle support to read PyTorch checkpoints.
2//!
3//! This implementation started from the candle project's pickle loader and has since been
4//! reworked around lazy tensor data, every pickle protocol a Python 3 writer emits, and a
5//! single tensor-building path shared by every container format.
6//!
7//! Original source: <https://github.com/huggingface/candle/blob/main/candle-core/src/pickle.rs>
8//!
9//! The parser is a stack machine like CPython's unpickler. Three hooks make it PyTorch aware:
10//! persistent ids resolve to storage references (or, for the old TAR container, to tensors
11//! built ahead of time), `REDUCE` calls of `torch._utils._rebuild_tensor*` turn a storage
12//! reference into a tensor whose bytes are read only when asked for, and a `BUILD` of a
13//! `torch.nn.Module` (a full-model save) is read as the module's `state_dict()`.
14
15use 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
23/// Cap on the up-front allocation for a length-prefixed string, bytes or long value;
24/// longer values grow as their bytes arrive.
25const STRING_PREALLOC_BOUND: usize = 1 << 16;
26
27/// Error type for pickle operations.
28#[derive(Debug, Error)]
29#[non_exhaustive]
30pub enum PickleError {
31    #[error("IO error: {0}")]
32    Io(#[from] io::Error),
33    /// A byte that is not a pickle opcode, or an opcode this reader does not implement.
34    #[error(
35        "invalid pickle opcode: 0x{0:02x}. The file may be corrupted or use an unsupported pickle feature."
36    )]
37    InvalidOpCode(u8),
38    /// An opcode that is understood but deliberately unsupported (extension registry,
39    /// out-of-band buffers).
40    #[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    /// The pickle references tensor storages but nothing can supply their bytes.
57    #[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// https://github.com/python/cpython/blob/main/Lib/pickletools.py
66#[repr(u8)]
67#[derive(Debug, Eq, PartialEq, Clone, Copy)]
68#[non_exhaustive]
69pub enum OpCode {
70    // Protocol 0
71    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    // Protocol 1
93    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    // Protocol 2
111    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    // Protocol 3
124    BinBytes = b'B',
125    ShortBinBytes = b'C',
126    // Protocol 4
127    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    // Protocol 5
138    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/// A storage referenced by a persistent id, before any tensor is built on it.
219#[derive(Debug, Clone)]
220pub struct StorageRef {
221    /// The container that holds the bytes.
222    pub(crate) source: Arc<StorageSource>,
223    /// Key of the storage within its container (`"0"`, `"1"`, ...).
224    pub(crate) key: String,
225    /// Element type of a typed storage; `None` for `torch.UntypedStorage`.
226    pub(crate) dtype: Option<DType>,
227    /// Bytes a tensor may reach in this storage: its declared size, or the end of the view
228    /// when the pickle describes one.
229    pub(crate) reachable_bytes: usize,
230    /// Element offset into the root storage when the persistent id describes a view.
231    /// Modern `torch.save` always writes `None` there; only early legacy files carry one.
232    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    /// A Python object this reader does not interpret: an unknown `REDUCE`, `NEWOBJ`,
252    /// `NEWOBJ_EX` or `BUILD`, or an int too wide for an `i64`. Only the name of its
253    /// Python type is kept, so a consumer can say what it could not read.
254    Opaque(String),
255    Tensor(Tensor),
256}
257
258impl Object {
259    /// Recursively counts the total number of nodes in the object tree.
260    /// Used to prevent CPU exhaustion from pickle memo bomb attacks.
261    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    /// Bytes of string and bytes payload in the object tree, which a clone duplicates.
272    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    /// Whether the object carries a storage or tensor anywhere inside it.
284    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    /// The Python type of the value, as a consumer would name it: the class itself, the
312    /// callable an opaque object was built from, the torch type of a tensor or storage.
313    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
325/// How persistent ids in a pickle are resolved.
326pub(crate) enum PersistentIds {
327    /// Nothing can supply storage bytes; any persistent id is an error.
328    Unavailable,
329    /// `('storage', type, key, location, numel[, view])` tuples, backed by a source.
330    Storages(Arc<StorageSource>),
331    /// Ids naming tensors built ahead of time (the TAR container).
332    Tensors(HashMap<String, Tensor>),
333}
334
335/// Parse one pickle from `r`, leaving it positioned just after the `STOP` opcode.
336pub(crate) fn read_pickle<R: BufRead>(r: &mut R, ids: &PersistentIds) -> Result<Object> {
337    Unpickler::new(ids).run(r)
338}
339
340// ---------------------------------------------------------------------------------------------
341// Persistent ids and REDUCE dispatch
342// ---------------------------------------------------------------------------------------------
343
344/// Convert a PyTorch storage class name to its element type.
345pub(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
363/// Convert a `torch.<dtype>` attribute name to its element type.
364fn 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
383/// Read a non-negative Python int as a `usize`.
384pub(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
395/// Read a Python str or int as a string key. PyTorch uses both for the same purpose.
396pub(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
407/// Resolve a `('storage', storage_type, key, location, numel[, view_metadata])` tuple.
408fn 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    // Early legacy files may describe a view of a root storage: (view_key, offset, view_size).
445    // A tensor may reach only as far as the view does, which is where it ends rather than
446    // where the root storage does.
447    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` never exceeds `numel`, whose byte length is checked above.
484        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
514/// Apply a `REDUCE`: call `callable` with `args`.
515///
516/// Only the calls PyTorch uses to rebuild tensors, and the `OrderedDict`, `set` and
517/// `frozenset` constructors, are interpreted. Any other call
518/// is kept as an opaque object (see [`opaque`] for the one exception) so a checkpoint
519/// carrying, say, numpy scalars or a device in its metadata still loads its tensors.
520fn 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        // Protocols before 4 spell a set as `set(items)`, and below 3 name the module
529        // `__builtin__` (the Python 2 name) rather than `builtins`. A set is read as a
530        // list, as the protocol 4 `EMPTY_SET` and `FROZENSET` opcodes are.
531        ("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        // _rebuild_parameter(data, requires_grad, backward_hooks[, state]): only the data
538        // is kept, so state holding tensors would go missing and is refused instead.
539        ("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        // _rebuild_from_type_v2(func, new_type, args, state): the tensor is func(*args).
554        // The subclass itself is not represented: a weight loader wants the data. State
555        // that holds tensors would be dropped, so that case is refused.
556        ("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
576/// Leave an uninterpreted call opaque, unless it consumed tensor data.
577///
578/// A call whose arguments hold a storage or tensor (sparse, quantized and nested tensors,
579/// or a tensor subclass) is a tensor this reader cannot represent. That is worth an error
580/// rather than an entry that silently goes missing. The same check guards `BUILD` state,
581/// so an object whose attributes hold tensors is refused rather than loaded empty; a
582/// `torch.nn.Module` is the one object read instead (see [`module_state_dict`]).
583fn 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
591/// The items of a call taking one sequence: `f()` or `f(items)`.
592fn 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
618/// `OrderedDict()` or `OrderedDict([(key, value), ...])`.
619fn 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
640/// The tables every `torch.nn.Module` keeps in its `__dict__`.
641const MODULE_TABLES: [&str; 3] = ["_parameters", "_buffers", "_modules"];
642
643/// Whether `attrs` is the `__dict__` of a `torch.nn.Module`: the three tables, each a dict.
644/// The class is not looked at, since a model is pickled under its own (`__main__.Net`).
645fn 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
651/// Read a pickled `torch.nn.Module` as its `state_dict()`.
652///
653/// `torch.save(model)` pickles the module object with its `__dict__` as `BUILD` state. The
654/// parameters and buffers are in there under `_parameters` and `_buffers`, and the children
655/// under `_modules`, each already read this way because a pickle builds inner objects
656/// first. The result is a dict with a dict per child, which [`extract_tensors`] flattens to
657/// the names `state_dict()` gives (`layer1.weight`), so a full-model save loads its
658/// parameters and buffers as its state_dict would. Also as in `state_dict()`, `None`
659/// entries, buffers in `_non_persistent_buffers_set` and every other attribute (`training`,
660/// hooks, a tensor assigned without `register_buffer`) are left out. What `state_dict()`
661/// computes rather than stores, `get_extra_state()` and the work of state dict hooks, is
662/// not in the pickle and so not here either.
663fn 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            // A module cannot register the same name twice, so a file that does has
692            // two values for one name and neither may silently win.
693            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    /// `_rebuild_tensor(storage, storage_offset, size, stride)`, PyTorch before 0.4.
707    Legacy,
708    /// `_rebuild_tensor_v2(storage, storage_offset, size, stride, requires_grad, hooks[, metadata])`.
709    V2,
710    /// `_rebuild_tensor_v3(storage, storage_offset, size, stride, requires_grad, hooks, dtype[, metadata])`,
711    /// used for element types without a typed storage class (uint16/32/64, float8 and
712    /// others). Only the unsigned ints have a [`DType`]; the rest are unsupported.
713    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
807// ---------------------------------------------------------------------------------------------
808// Tensor construction
809// ---------------------------------------------------------------------------------------------
810
811/// Return the number of logical elements, rejecting shape arithmetic overflow.
812fn 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
820/// Return the minimum number of storage elements required by a strided tensor view.
821///
822/// For a non-empty view, this is one past the largest storage index it reads. An empty view
823/// requires only its storage offset. Size-one dimensions still contribute no storage movement.
824fn 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
858/// Check whether a stride describes row-major contiguous storage.
859///
860/// PyTorch ignores strides for size-one dimensions because those dimensions cannot be stepped.
861fn 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
875/// Gather a strided view into logical row-major byte order.
876///
877/// `data` begins at the tensor's storage offset, and the caller has validated that every
878/// index the view touches lies within it.
879fn gather_strided(
880    data: &[u8],
881    shape: &[usize],
882    stride: &[usize],
883    element_size: usize,
884    byte_len: usize,
885) -> Vec<u8> {
886    let mut out = Vec::with_capacity(byte_len);
887    let num_elements = byte_len / element_size;
888    for linear_index in 0..num_elements {
889        let mut remaining = linear_index;
890        let mut storage_index = 0;
891        for (&dim, &step) in shape.iter().zip(stride).rev() {
892            storage_index += (remaining % dim) * step;
893            remaining /= dim;
894        }
895        let start = storage_index * element_size;
896        out.extend_from_slice(&data[start..start + element_size]);
897    }
898    out
899}
900
901/// Convert little-endian file bytes to the target's native byte order.
902fn to_native_endian(bytes: &mut [u8], element_size: usize) {
903    if cfg!(target_endian = "big") && element_size > 1 {
904        for element in bytes.chunks_exact_mut(element_size) {
905            element.reverse();
906        }
907    }
908}
909
910/// Build a tensor that reads its bytes from the storage's container on demand.
911///
912/// Everything derivable from the metadata is validated here, so a file whose declarations are
913/// inconsistent fails at parse time; the data itself is validated when it is read.
914pub(crate) fn build_tensor(
915    storage: StorageRef,
916    dtype: DType,
917    storage_offset: usize,
918    shape: Vec<usize>,
919    stride: Vec<usize>,
920) -> Result<Tensor> {
921    let element_size = dtype.size();
922    let storage_offset = storage_offset
923        .checked_add(storage.view_offset)
924        .ok_or_else(|| PickleError::InvalidData("Storage offset overflows usize".to_string()))?;
925    let num_elements = tensor_num_elements(&shape)?;
926    let extent = tensor_storage_extent(&shape, &stride, storage_offset)?;
927    let byte_len = num_elements.checked_mul(element_size).ok_or_else(|| {
928        PickleError::InvalidData("Tensor byte length overflows usize".to_string())
929    })?;
930
931    let declared_elements = storage.reachable_bytes / element_size;
932    if extent > declared_elements {
933        return Err(PickleError::InvalidData(format!(
934            "Tensor with shape {shape:?} and stride {stride:?} at offset {storage_offset} needs {extent} elements, but storage '{}' declares {declared_elements}",
935            storage.key
936        )));
937    }
938    // See `MAX_TENSOR_SIZE`: the file itself bounds nothing here.
939    if byte_len > MAX_TENSOR_SIZE {
940        return Err(PickleError::InvalidData(format!(
941            "Tensor with shape {shape:?} would materialize {byte_len} bytes, above the {MAX_TENSOR_SIZE} byte limit"
942        )));
943    }
944
945    let StorageRef { source, key, .. } = storage;
946    let provider_shape = shape.clone();
947    // Read only the window the view can touch: a storage that reaches further, before the
948    // tensor's offset or past its extent, is not paid for.
949    let window_start = storage_offset.saturating_mul(element_size);
950    let window_len = (extent - storage_offset).saturating_mul(element_size);
951    let provider = move || -> io::Result<Vec<u8>> {
952        let shape = &provider_shape;
953        let (mut data, skipped) = source.read(&key, window_start, window_len).map_err(|err| {
954            // A source reports a missing, corrupt or unreadable storage with whatever kind
955            // it or its container library chose, so they are folded into one. Only an error
956            // that came from the operating system keeps its kind: that is how a caller tells
957            // a file that disagrees with itself from one it cannot read.
958            let kind = match err.kind() {
959                kind if err.raw_os_error().is_some() => kind,
960                io::ErrorKind::UnexpectedEof => io::ErrorKind::UnexpectedEof,
961                _ => io::ErrorKind::InvalidData,
962            };
963            io::Error::new(
964                kind,
965                format!("Failed to read storage '{key}' for tensor with shape {shape:?}: {err}"),
966            )
967        })?;
968
969        // `data` starts at the tensor's own offset, so every index below is relative to it.
970        let available = (skipped + data.len()) / element_size;
971        if extent > available {
972            return Err(io::Error::new(
973                io::ErrorKind::UnexpectedEof,
974                format!(
975                    "Tensor with shape {shape:?} requires {extent} elements from storage '{key}', but only {available} are available"
976                ),
977            ));
978        }
979
980        let mut bytes = if is_contiguous_stride(shape, &stride) {
981            data.truncate(byte_len);
982            data
983        } else {
984            gather_strided(&data, shape, &stride, element_size, byte_len)
985        };
986
987        to_native_endian(&mut bytes, element_size);
988        if dtype == DType::Bool {
989            // A well-formed file holds only 0 or 1; any other byte would be an invalid
990            // `bool`, so normalize so a consumer can reinterpret them as such.
991            for byte in &mut bytes {
992                *byte = u8::from(*byte != 0);
993            }
994        }
995
996        Ok(bytes)
997    };
998
999    // The tensor's name is its path through the pickle's dicts, which `extract_tensors`
1000    // assigns once the whole pickle has been walked.
1001    Ok(Tensor::new(String::new(), dtype, shape, provider))
1002}
1003
1004// ---------------------------------------------------------------------------------------------
1005// The stack machine
1006// ---------------------------------------------------------------------------------------------
1007
1008/// Total nodes that may be copied through memo lookups and `DUP` before the pickle is
1009/// rejected as a memo bomb (each such opcode deep-copies an object).
1010const MAX_COPIED_NODES: usize = 1_000_000;
1011
1012/// Total string and bytes payload those copies may duplicate. A pickle can memoize one
1013/// large value and fetch it many times, so node counts alone do not bound memory.
1014const MAX_COPIED_BYTES: usize = 64 << 20;
1015
1016/// Deepest container nesting accepted.
1017///
1018/// Objects are walked recursively after parsing (extraction, conversion, drop), so a pickle
1019/// nested deeper than the thread's stack would overflow it instead of failing. Python's own
1020/// recursion limit is the same order of magnitude.
1021const MAX_NESTING_DEPTH: u32 = 1000;
1022
1023/// A stack or memo slot: the object plus how deeply it nests containers (0 for a scalar).
1024#[derive(Clone)]
1025struct Entry {
1026    object: Object,
1027    depth: u32,
1028    /// The memo slot holding a copy of this object, once memoized. CPython's memo aliases
1029    /// the object itself, so `BUILD` refreshes the copy to keep a later fetch in step.
1030    memo: Option<u32>,
1031}
1032
1033struct Unpickler<'a> {
1034    stack: Vec<Entry>,
1035    /// Stack heights at each open `MARK`, innermost last, as in CPython.
1036    marks: Vec<usize>,
1037    memo: HashMap<u32, Entry>,
1038    copied_nodes: usize,
1039    copied_bytes: usize,
1040    ids: &'a PersistentIds,
1041}
1042
1043impl<'a> Unpickler<'a> {
1044    fn new(ids: &'a PersistentIds) -> Self {
1045        Self {
1046            stack: Vec::new(),
1047            marks: Vec::new(),
1048            memo: HashMap::new(),
1049            copied_nodes: 0,
1050            copied_bytes: 0,
1051            ids,
1052        }
1053    }
1054
1055    fn push_scalar(&mut self, object: Object) {
1056        self.stack.push(Entry {
1057            object,
1058            depth: 0,
1059            memo: None,
1060        });
1061    }
1062
1063    fn push(&mut self, object: Object, depth: u32) -> Result<()> {
1064        if depth > MAX_NESTING_DEPTH {
1065            return Err(PickleError::InvalidData(format!(
1066                "pickle nesting exceeds {MAX_NESTING_DEPTH} levels"
1067            )));
1068        }
1069        self.stack.push(Entry {
1070            object,
1071            depth,
1072            memo: None,
1073        });
1074        Ok(())
1075    }
1076
1077    /// Whether the innermost `MARK` sits on top of the stack, with no value above it.
1078    fn mark_on_top(&self) -> bool {
1079        self.marks.last() == Some(&self.stack.len())
1080    }
1081
1082    fn pop(&mut self) -> Result<Entry> {
1083        if self.mark_on_top() {
1084            return Err(PickleError::InvalidData(
1085                "expected a value on the stack, found MARK".to_string(),
1086            ));
1087        }
1088        self.stack.pop().ok_or(PickleError::StackUnderflow)
1089    }
1090
1091    fn pop_object(&mut self) -> Result<Object> {
1092        self.pop().map(|entry| entry.object)
1093    }
1094
1095    fn top(&self) -> Result<&Entry> {
1096        if self.mark_on_top() {
1097            return Err(PickleError::InvalidData(
1098                "expected a value on the stack, found MARK".to_string(),
1099            ));
1100        }
1101        self.stack.last().ok_or(PickleError::StackUnderflow)
1102    }
1103
1104    fn top_mut(&mut self) -> Result<&mut Entry> {
1105        if self.mark_on_top() {
1106            return Err(PickleError::InvalidData(
1107                "expected a value on the stack, found MARK".to_string(),
1108            ));
1109        }
1110        self.stack.last_mut().ok_or(PickleError::StackUnderflow)
1111    }
1112
1113    /// Pop everything above the innermost `MARK`, and the mark itself.
1114    ///
1115    /// Returns the objects with the deepest nesting among them.
1116    fn pop_to_mark(&mut self) -> Result<(Vec<Object>, u32)> {
1117        let mark = self
1118            .marks
1119            .pop()
1120            .ok_or_else(|| PickleError::InvalidData("MARK not found on stack".to_string()))?;
1121        let entries = self.stack.split_off(mark);
1122        let depth = entries.iter().map(|entry| entry.depth).max().unwrap_or(0);
1123        Ok((
1124            entries.into_iter().map(|entry| entry.object).collect(),
1125            depth,
1126        ))
1127    }
1128
1129    /// Account for a deep copy of `object` against the copy budgets.
1130    fn charge_copy(&mut self, object: &Object) -> Result<()> {
1131        self.copied_nodes += object.node_count();
1132        self.copied_bytes += object.payload_bytes();
1133        if self.copied_nodes > MAX_COPIED_NODES || self.copied_bytes > MAX_COPIED_BYTES {
1134            return Err(PickleError::InvalidData(format!(
1135                "Pickle memo bomb detected: copies exceeded {MAX_COPIED_NODES} nodes or {MAX_COPIED_BYTES} bytes"
1136            )));
1137        }
1138        Ok(())
1139    }
1140
1141    fn memo_get(&mut self, idx: u32) -> Result<Entry> {
1142        let entry = self
1143            .memo
1144            .get(&idx)
1145            .ok_or(PickleError::MemoNotFound(idx))?
1146            .clone();
1147        self.charge_copy(&entry.object)?;
1148        Ok(entry)
1149    }
1150
1151    fn memo_put(&mut self, idx: u32) -> Result<()> {
1152        let top = self.top_mut()?;
1153        top.memo = Some(idx);
1154        let entry = top.clone();
1155        self.charge_copy(&entry.object)?;
1156        self.memo.insert(idx, entry);
1157        Ok(())
1158    }
1159
1160    /// Extend the list on top of the stack.
1161    fn append(&mut self, items: Vec<Object>, items_depth: u32, op: OpCode) -> Result<()> {
1162        let top = self.top_mut()?;
1163        let Object::List(list) = &mut top.object else {
1164            return Err(PickleError::UnexpectedOpCode(op));
1165        };
1166        list.extend(items);
1167        let depth = top.depth.max(items_depth + 1);
1168        top.depth = depth;
1169        if depth > MAX_NESTING_DEPTH {
1170            return Err(PickleError::InvalidData(format!(
1171                "pickle nesting exceeds {MAX_NESTING_DEPTH} levels"
1172            )));
1173        }
1174        Ok(())
1175    }
1176
1177    /// Insert key/value pairs into the dict on top of the stack.
1178    fn set_items(&mut self, items: Vec<Object>, items_depth: u32, op: OpCode) -> Result<()> {
1179        let top = self.top_mut()?;
1180        let Object::Dict(dict) = &mut top.object else {
1181            return Err(PickleError::UnexpectedOpCode(op));
1182        };
1183        insert_pairs(dict, items)?;
1184        let depth = top.depth.max(items_depth + 1);
1185        top.depth = depth;
1186        if depth > MAX_NESTING_DEPTH {
1187            return Err(PickleError::InvalidData(format!(
1188                "pickle nesting exceeds {MAX_NESTING_DEPTH} levels"
1189            )));
1190        }
1191        Ok(())
1192    }
1193
1194    fn run<R: BufRead>(mut self, r: &mut R) -> Result<Object> {
1195        loop {
1196            let byte = r.read_u8()?;
1197            let op = OpCode::try_from(byte).map_err(PickleError::InvalidOpCode)?;
1198            match op {
1199                OpCode::Proto => {
1200                    let version = r.read_u8()?;
1201                    if version > 5 {
1202                        return Err(PickleError::InvalidProtocol(version));
1203                    }
1204                }
1205                OpCode::Frame => {
1206                    // Framing only matters for streaming readers; the frame size is not needed.
1207                    r.read_u64::<LittleEndian>()?;
1208                }
1209                OpCode::Stop => break,
1210
1211                // Scalars
1212                OpCode::None => self.push_scalar(Object::None),
1213                OpCode::NewTrue => self.push_scalar(Object::Bool(true)),
1214                OpCode::NewFalse => self.push_scalar(Object::Bool(false)),
1215                OpCode::Int => {
1216                    let text = read_line(r)?;
1217                    // Protocol 0 spells booleans as INT 00 / INT 01.
1218                    let value = match text.as_str() {
1219                        "00" => Object::Bool(false),
1220                        "01" => Object::Bool(true),
1221                        _ => Object::Int(parse_int(&text)?),
1222                    };
1223                    self.push_scalar(value);
1224                }
1225                OpCode::Long => {
1226                    let text = read_line(r)?;
1227                    let text = text.strip_suffix('L').unwrap_or(&text);
1228                    self.push_scalar(parse_long(text)?);
1229                }
1230                OpCode::Float => {
1231                    let text = read_line(r)?;
1232                    let value = text.parse::<f64>().map_err(|e| {
1233                        PickleError::InvalidData(format!("Invalid FLOAT value '{text}': {e}"))
1234                    })?;
1235                    self.push_scalar(Object::Float(value));
1236                }
1237                OpCode::Unicode => {
1238                    let text = read_raw_unicode_escape_line(r)?;
1239                    self.push_scalar(Object::String(text));
1240                }
1241                OpCode::BinInt => {
1242                    let v = r.read_i32::<LittleEndian>()?;
1243                    self.push_scalar(Object::Int(v as i64));
1244                }
1245                OpCode::BinInt1 => {
1246                    let v = r.read_u8()?;
1247                    self.push_scalar(Object::Int(v as i64));
1248                }
1249                OpCode::BinInt2 => {
1250                    let v = r.read_u16::<LittleEndian>()?;
1251                    self.push_scalar(Object::Int(v as i64));
1252                }
1253                OpCode::BinFloat => {
1254                    let v = r.read_f64::<BigEndian>()?;
1255                    self.push_scalar(Object::Float(v));
1256                }
1257                OpCode::Long1 => {
1258                    let len = r.read_u8()? as u64;
1259                    let bytes = read_exact_len(r, len, STRING_PREALLOC_BOUND)?;
1260                    self.push_scalar(long_object(&bytes));
1261                }
1262                OpCode::Long4 => {
1263                    let len = r.read_u32::<LittleEndian>()? as u64;
1264                    let bytes = read_exact_len(r, len, STRING_PREALLOC_BOUND)?;
1265                    self.push_scalar(long_object(&bytes));
1266                }
1267
1268                // Strings and bytes
1269                OpCode::ShortBinUnicode => {
1270                    let len = r.read_u8()? as u64;
1271                    self.push_scalar(read_string(r, len)?);
1272                }
1273                OpCode::BinUnicode => {
1274                    let len = r.read_u32::<LittleEndian>()? as u64;
1275                    self.push_scalar(read_string(r, len)?);
1276                }
1277                OpCode::ShortBinString => {
1278                    let len = r.read_u8()? as u64;
1279                    self.push_scalar(read_py2_string(r, len)?);
1280                }
1281                OpCode::BinString => {
1282                    let len = r.read_u32::<LittleEndian>()? as u64;
1283                    self.push_scalar(read_py2_string(r, len)?);
1284                }
1285                OpCode::BinUnicode8 => {
1286                    let len = r.read_u64::<LittleEndian>()?;
1287                    self.push_scalar(read_string(r, len)?);
1288                }
1289                OpCode::ShortBinBytes => {
1290                    let len = r.read_u8()? as u64;
1291                    self.push_scalar(Object::Bytes(read_bytes(r, len)?));
1292                }
1293                OpCode::BinBytes => {
1294                    let len = r.read_u32::<LittleEndian>()? as u64;
1295                    self.push_scalar(Object::Bytes(read_bytes(r, len)?));
1296                }
1297                OpCode::BinBytes8 | OpCode::ByteArray8 => {
1298                    let len = r.read_u64::<LittleEndian>()?;
1299                    self.push_scalar(Object::Bytes(read_bytes(r, len)?));
1300                }
1301
1302                // Containers
1303                OpCode::Mark => self.marks.push(self.stack.len()),
1304                OpCode::EmptyTuple => self.push_scalar(Object::Tuple(Vec::new())),
1305                OpCode::EmptyList | OpCode::EmptySet => self.push_scalar(Object::List(Vec::new())),
1306                OpCode::EmptyDict => self.push_scalar(Object::Dict(HashMap::new())),
1307                OpCode::Tuple => {
1308                    let (items, depth) = self.pop_to_mark()?;
1309                    self.push(Object::Tuple(items), depth + 1)?;
1310                }
1311                OpCode::Tuple1 => {
1312                    let a = self.pop()?;
1313                    self.push(Object::Tuple(vec![a.object]), a.depth + 1)?;
1314                }
1315                OpCode::Tuple2 => {
1316                    let b = self.pop()?;
1317                    let a = self.pop()?;
1318                    let depth = a.depth.max(b.depth) + 1;
1319                    self.push(Object::Tuple(vec![a.object, b.object]), depth)?;
1320                }
1321                OpCode::Tuple3 => {
1322                    let c = self.pop()?;
1323                    let b = self.pop()?;
1324                    let a = self.pop()?;
1325                    let depth = a.depth.max(b.depth).max(c.depth) + 1;
1326                    self.push(Object::Tuple(vec![a.object, b.object, c.object]), depth)?;
1327                }
1328                OpCode::List | OpCode::FrozenSet => {
1329                    let (items, depth) = self.pop_to_mark()?;
1330                    self.push(Object::List(items), depth + 1)?;
1331                }
1332                OpCode::Append => {
1333                    let value = self.pop()?;
1334                    self.append(vec![value.object], value.depth, op)?;
1335                }
1336                OpCode::Appends | OpCode::AddItems => {
1337                    let (items, depth) = self.pop_to_mark()?;
1338                    self.append(items, depth, op)?;
1339                }
1340                OpCode::Dict => {
1341                    let (items, depth) = self.pop_to_mark()?;
1342                    let mut dict = HashMap::with_capacity(items.len() / 2);
1343                    insert_pairs(&mut dict, items)?;
1344                    self.push(Object::Dict(dict), depth + 1)?;
1345                }
1346                OpCode::SetItem => {
1347                    let value = self.pop()?;
1348                    let key = self.pop()?;
1349                    let depth = key.depth.max(value.depth);
1350                    self.set_items(vec![key.object, value.object], depth, op)?;
1351                }
1352                OpCode::SetItems => {
1353                    let (items, depth) = self.pop_to_mark()?;
1354                    self.set_items(items, depth, op)?;
1355                }
1356
1357                // Stack manipulation
1358                OpCode::Pop => {
1359                    // As in CPython, POP discards a bare MARK when that is what is on top.
1360                    if self.mark_on_top() {
1361                        self.marks.pop();
1362                    } else {
1363                        self.pop()?;
1364                    }
1365                }
1366                OpCode::PopMark => {
1367                    self.pop_to_mark()?;
1368                }
1369                OpCode::Dup => {
1370                    let top = self.top()?.clone();
1371                    self.charge_copy(&top.object)?;
1372                    self.stack.push(top);
1373                }
1374
1375                // Memo
1376                OpCode::Get => {
1377                    let idx = parse_index(&read_line(r)?)?;
1378                    let entry = self.memo_get(idx)?;
1379                    self.stack.push(entry);
1380                }
1381                OpCode::BinGet => {
1382                    let idx = r.read_u8()? as u32;
1383                    let entry = self.memo_get(idx)?;
1384                    self.stack.push(entry);
1385                }
1386                OpCode::LongBinGet => {
1387                    let idx = r.read_u32::<LittleEndian>()?;
1388                    let entry = self.memo_get(idx)?;
1389                    self.stack.push(entry);
1390                }
1391                OpCode::Put => {
1392                    let idx = parse_index(&read_line(r)?)?;
1393                    self.memo_put(idx)?;
1394                }
1395                OpCode::BinPut => {
1396                    let idx = r.read_u8()? as u32;
1397                    self.memo_put(idx)?;
1398                }
1399                OpCode::LongBinPut => {
1400                    let idx = r.read_u32::<LittleEndian>()?;
1401                    self.memo_put(idx)?;
1402                }
1403                OpCode::Memoize => {
1404                    let idx = self.memo.len() as u32;
1405                    self.memo_put(idx)?;
1406                }
1407
1408                // Objects
1409                OpCode::Global => {
1410                    let module_name = read_line(r)?;
1411                    let name = read_line(r)?;
1412                    self.push_scalar(Object::Class { module_name, name });
1413                }
1414                OpCode::StackGlobal => {
1415                    let name = self.pop_object()?;
1416                    let module_name = self.pop_object()?;
1417                    match (module_name, name) {
1418                        (Object::String(module_name), Object::String(name)) => {
1419                            self.push_scalar(Object::Class { module_name, name })
1420                        }
1421                        _ => {
1422                            return Err(PickleError::InvalidData(
1423                                "STACK_GLOBAL expects two strings".to_string(),
1424                            ));
1425                        }
1426                    }
1427                }
1428                OpCode::PersId => {
1429                    let pid = Object::String(read_line(r)?);
1430                    let obj = resolve_persistent_id(pid, self.ids)?;
1431                    self.push_scalar(obj);
1432                }
1433                OpCode::BinPersId => {
1434                    let pid = self.pop_object()?;
1435                    let obj = resolve_persistent_id(pid, self.ids)?;
1436                    self.push_scalar(obj);
1437                }
1438                OpCode::Reduce => {
1439                    let args = self.pop()?;
1440                    let callable = self.pop_object()?;
1441                    // A call's result nests no deeper than its arguments did.
1442                    let obj = reduce(callable, args.object)?;
1443                    self.push(obj, args.depth)?;
1444                }
1445                OpCode::NewObj => {
1446                    let args = self.pop_object()?;
1447                    let cls = self.pop_object()?;
1448                    let obj = opaque(&cls, &args)?;
1449                    self.push_scalar(obj);
1450                }
1451                OpCode::NewObjEx => {
1452                    let kwargs = self.pop_object()?;
1453                    let args = self.pop_object()?;
1454                    let cls = self.pop_object()?;
1455                    opaque(&cls, &kwargs)?;
1456                    let obj = opaque(&cls, &args)?;
1457                    self.push_scalar(obj);
1458                }
1459                OpCode::Build => {
1460                    let state = self.pop()?;
1461                    let object = self.pop()?;
1462                    let memo = object.memo;
1463                    let (built, depth) = match (object.object, state.object) {
1464                        (Object::Dict(mut dict), Object::Dict(update)) => {
1465                            dict.extend(update);
1466                            (Object::Dict(dict), object.depth.max(state.depth))
1467                        }
1468                        // A dict subclass with non-dict state: the items are what matter,
1469                        // but state holding tensor data must not vanish silently.
1470                        (dict @ Object::Dict(_), state) => {
1471                            if state.holds_tensor_data() {
1472                                return Err(PickleError::UnsupportedType(
1473                                    "dict with tensor-bearing state".to_string(),
1474                                ));
1475                            }
1476                            (dict, object.depth)
1477                        }
1478                        // A module instance (a full-model save) becomes its state_dict,
1479                        // which nests no deeper than the attributes it came from.
1480                        (Object::Opaque(_), Object::Dict(attrs)) if is_module_state(&attrs) => {
1481                            (module_state_dict(attrs)?, state.depth)
1482                        }
1483                        (object, state) => (opaque(&object, &state)?, 0),
1484                    };
1485                    // The pickler memoizes an object before writing its state, so the memo
1486                    // holds it as it was before `BUILD`. CPython's memo aliases the object
1487                    // and sees the state set on it; here the copy is refreshed, so a module
1488                    // assigned under two names is read built the second time as well.
1489                    if let Some(idx) = memo {
1490                        self.charge_copy(&built)?;
1491                        self.memo.insert(
1492                            idx,
1493                            Entry {
1494                                object: built.clone(),
1495                                depth,
1496                                memo: Some(idx),
1497                            },
1498                        );
1499                    }
1500                    self.push(built, depth)?;
1501                }
1502
1503                OpCode::Ext1
1504                | OpCode::Ext2
1505                | OpCode::Ext4
1506                | OpCode::NextBuffer
1507                | OpCode::ReadonlyBuffer => return Err(PickleError::UnsupportedOpCode(op)),
1508            }
1509        }
1510
1511        self.pop_object()
1512    }
1513}
1514
1515fn read_line<R: BufRead>(r: &mut R) -> Result<String> {
1516    let mut data = Vec::with_capacity(32);
1517    r.read_until(b'\n', &mut data)?;
1518    if data.pop() != Some(b'\n') {
1519        return Err(PickleError::Io(io::Error::new(
1520            io::ErrorKind::UnexpectedEof,
1521            "unterminated text field",
1522        )));
1523    }
1524    if data.last() == Some(&b'\r') {
1525        data.pop();
1526    }
1527    String::from_utf8(data).map_err(|e| PickleError::InvalidData(format!("Invalid UTF-8: {e}")))
1528}
1529
1530/// Read a protocol 0 `UNICODE` payload: Latin-1 bytes with `\uXXXX` and `\UXXXXXXXX`
1531/// escapes for everything else, Python's `raw-unicode-escape` codec.
1532fn read_raw_unicode_escape_line<R: BufRead>(r: &mut R) -> Result<String> {
1533    let mut data = Vec::with_capacity(32);
1534    r.read_until(b'\n', &mut data)?;
1535    if data.pop() != Some(b'\n') {
1536        return Err(PickleError::Io(io::Error::new(
1537            io::ErrorKind::UnexpectedEof,
1538            "unterminated text field",
1539        )));
1540    }
1541
1542    let mut text = String::with_capacity(data.len());
1543    let mut i = 0;
1544    while i < data.len() {
1545        let byte = data[i];
1546        let escape_len = match (byte, data.get(i + 1)) {
1547            (b'\\', Some(b'u')) => Some(4),
1548            (b'\\', Some(b'U')) => Some(8),
1549            _ => None,
1550        };
1551        match escape_len {
1552            Some(digits) => {
1553                let hex = data
1554                    .get(i + 2..i + 2 + digits)
1555                    .and_then(|hex| std::str::from_utf8(hex).ok())
1556                    .and_then(|hex| u32::from_str_radix(hex, 16).ok())
1557                    .and_then(char::from_u32)
1558                    .ok_or_else(|| {
1559                        PickleError::InvalidData("Invalid raw-unicode-escape sequence".to_string())
1560                    })?;
1561                text.push(hex);
1562                i += 2 + digits;
1563            }
1564            None => {
1565                text.push(char::from(byte));
1566                i += 1;
1567            }
1568        }
1569    }
1570    Ok(text)
1571}
1572
1573fn read_bytes<R: BufRead>(r: &mut R, len: u64) -> Result<Vec<u8>> {
1574    Ok(read_exact_len(r, len, STRING_PREALLOC_BOUND)?)
1575}
1576
1577fn read_string<R: BufRead>(r: &mut R, len: u64) -> Result<Object> {
1578    let s = String::from_utf8(read_bytes(r, len)?)
1579        .map_err(|e| PickleError::InvalidData(format!("Invalid UTF-8: {e}")))?;
1580    Ok(Object::String(s))
1581}
1582
1583/// A Python 2 `str` is bytes that usually hold text, but Python 2 pickles of numpy arrays
1584/// put raw array data in them. Text is kept as a string and anything else as bytes.
1585fn read_py2_string<R: BufRead>(r: &mut R, len: u64) -> Result<Object> {
1586    Ok(match String::from_utf8(read_bytes(r, len)?) {
1587        Ok(text) => Object::String(text),
1588        Err(err) => Object::Bytes(err.into_bytes()),
1589    })
1590}
1591
1592fn parse_int(text: &str) -> Result<i64> {
1593    text.parse::<i64>()
1594        .map_err(|e| PickleError::InvalidData(format!("Invalid integer '{text}': {e}")))
1595}
1596
1597fn parse_index(text: &str) -> Result<u32> {
1598    text.parse::<u32>()
1599        .map_err(|e| PickleError::InvalidData(format!("Invalid memo index '{text}': {e}")))
1600}
1601
1602/// A Python long, kept opaque when it does not fit in an `i64`.
1603///
1604/// Python integers are unbounded and a checkpoint may carry a large one beside its
1605/// weights, such as a seed of `2**64`. No int this reader acts on (a shape, an offset, an
1606/// element count) can be that large, so an out-of-range value is metadata it never looks
1607/// at, and refusing the file over it would refuse an otherwise loadable checkpoint.
1608fn long_object(bytes: &[u8]) -> Object {
1609    int_from_le_bytes(bytes).map_or_else(|| Object::Opaque("int".to_string()), Object::Int)
1610}
1611
1612/// Protocol 0 writes a Python long in decimal. Out-of-range values stay opaque, as in
1613/// [`long_object`]; anything that is not an integer at all is an error.
1614fn parse_long(text: &str) -> Result<Object> {
1615    use std::num::IntErrorKind;
1616
1617    match text.parse::<i64>() {
1618        Ok(value) => Ok(Object::Int(value)),
1619        Err(e)
1620            if matches!(
1621                e.kind(),
1622                IntErrorKind::PosOverflow | IntErrorKind::NegOverflow
1623            ) =>
1624        {
1625            Ok(Object::Opaque("int".to_string()))
1626        }
1627        Err(e) => Err(PickleError::InvalidData(format!(
1628            "Invalid integer '{text}': {e}"
1629        ))),
1630    }
1631}
1632
1633/// Decode a two's-complement little-endian integer, or `None` if it needs more than 64 bits.
1634fn int_from_le_bytes(bytes: &[u8]) -> Option<i64> {
1635    let negative = bytes.last().is_some_and(|b| b & 0x80 != 0);
1636    let fill = if negative { 0xff } else { 0x00 };
1637    // Beyond 8 bytes, every extra byte must be sign fill, and the sign bit of byte 7 must
1638    // agree with it: otherwise the value lies outside the i64 range.
1639    let fits = bytes.len() <= 8
1640        || (bytes[8..].iter().all(|&b| b == fill) && (bytes[7] & 0x80 != 0) == negative);
1641    if !fits {
1642        return None;
1643    }
1644    let mut buf = [fill; 8];
1645    let len = bytes.len().min(8);
1646    buf[..len].copy_from_slice(&bytes[..len]);
1647    Some(i64::from_le_bytes(buf))
1648}
1649
1650fn dict_key(key: Object) -> Result<String> {
1651    match key {
1652        Object::String(s) => Ok(s),
1653        other => key_string(&other, "dict key"),
1654    }
1655}
1656
1657fn insert_pairs(dict: &mut HashMap<String, Object>, items: Vec<Object>) -> Result<()> {
1658    if !items.len().is_multiple_of(2) {
1659        return Err(PickleError::InvalidData(
1660            "dict items must come in key/value pairs".to_string(),
1661        ));
1662    }
1663    let mut items = items.into_iter();
1664    while let (Some(key), Some(value)) = (items.next(), items.next()) {
1665        dict.insert(dict_key(key)?, value);
1666    }
1667    Ok(())
1668}
1669
1670// ---------------------------------------------------------------------------------------------
1671// Tensor extraction
1672// ---------------------------------------------------------------------------------------------
1673
1674/// Walk a parsed object tree, collecting each tensor found under a nested dict/list/tuple path.
1675///
1676/// Dicts are descended by key; lists and tuples are descended by index, so a tensor at
1677/// `{"weights": [w1, w2]}` is named `weights.0` and `weights.1`. This is burn's `Vec<Module>`
1678/// parameter naming and matches how `nn.ModuleList` entries appear in a PyTorch state_dict,
1679/// so such files load into the natural burn module without remapping. A tensor is built
1680/// without a name, its path being assembled only here, so this is also where each one gets
1681/// its final identity.
1682pub(crate) fn extract_tensors(dict: HashMap<String, Object>) -> HashMap<String, Tensor> {
1683    fn walk(obj: Object, path: &mut Vec<String>, tensors: &mut HashMap<String, Tensor>) {
1684        match obj {
1685            Object::Dict(dict) => {
1686                for (key, value) in dict {
1687                    path.push(key);
1688                    walk(value, path, tensors);
1689                    path.pop();
1690                }
1691            }
1692            Object::List(items) | Object::Tuple(items) => {
1693                for (index, item) in items.into_iter().enumerate() {
1694                    path.push(index.to_string());
1695                    walk(item, path, tensors);
1696                    path.pop();
1697                }
1698            }
1699            Object::Tensor(mut tensor) => {
1700                tensor.name = path.join(".");
1701                tensors.insert(tensor.name.clone(), tensor);
1702            }
1703            _ => {}
1704        }
1705    }
1706    let mut tensors = HashMap::new();
1707    walk(Object::Dict(dict), &mut Vec::new(), &mut tensors);
1708    tensors
1709}
1710
1711#[cfg(test)]
1712mod tests {
1713    use super::*;
1714    use crate::tests::{read_as, test_data_path};
1715    use std::io::Cursor;
1716
1717    fn fixture_source() -> Arc<StorageSource> {
1718        let path = test_data_path("non_contiguous.pt");
1719        Arc::new(StorageSource::Zip(
1720            crate::storage::ZipSource::open(&path).unwrap(),
1721        ))
1722    }
1723
1724    fn storage_pid(storage_type: &str, key: &str, numel: i64) -> Object {
1725        Object::Tuple(vec![
1726            Object::String("storage".to_string()),
1727            Object::Class {
1728                module_name: "torch".to_string(),
1729                name: storage_type.to_string(),
1730            },
1731            Object::String(key.to_string()),
1732            Object::String("cpu".to_string()),
1733            Object::Int(numel),
1734        ])
1735    }
1736
1737    fn rebuild_args(
1738        storage_type: &str,
1739        key: &str,
1740        numel: i64,
1741        offset: i64,
1742        shape: &[i64],
1743        stride: &[i64],
1744        source: &Arc<StorageSource>,
1745    ) -> Object {
1746        let storage = match &storage_pid(storage_type, key, numel) {
1747            Object::Tuple(fields) => resolve_storage_id(fields, source).unwrap(),
1748            _ => unreachable!(),
1749        };
1750        Object::Tuple(vec![
1751            Object::Storage(storage),
1752            Object::Int(offset),
1753            Object::Tuple(shape.iter().copied().map(Object::Int).collect()),
1754            Object::Tuple(stride.iter().copied().map(Object::Int).collect()),
1755        ])
1756    }
1757
1758    fn rebuild(args: Object) -> Result<Tensor> {
1759        match rebuild_tensor(args, TensorRebuild::Legacy)? {
1760            Object::Tensor(tensor) => Ok(tensor),
1761            other => panic!("expected tensor, got {other:?}"),
1762        }
1763    }
1764
1765    fn plain(bytes: &[u8]) -> Result<Object> {
1766        read_pickle(&mut Cursor::new(bytes), &PersistentIds::Unavailable)
1767    }
1768
1769    #[test]
1770    fn test_memo_bomb_mitigation() {
1771        // Generate the Billion Laughs equivalent for pickles
1772        let mut poc = vec![0x89, b'q', 0x00];
1773        let n = 20; // 144 bytes, originally took 2.6s and 10+ million nodes
1774        for _ in 0..n {
1775            poc.extend_from_slice(&[b'h', 0x00, b'h', 0x00, 0x86, b'q', 0x00]);
1776        }
1777        poc.push(b'.');
1778
1779        let err = plain(&poc).unwrap_err();
1780        assert!(matches!(err, PickleError::InvalidData(msg) if msg.contains("memo bomb")));
1781    }
1782
1783    #[test]
1784    fn memo_copies_of_large_payloads_are_bounded() {
1785        // A 2 MiB BINBYTES memoized once and fetched 40 times would copy 80 MiB.
1786        let payload = vec![0xabu8; 2 << 20];
1787        let mut bytes = vec![0x80, 0x02, b'B'];
1788        bytes.extend_from_slice(&(payload.len() as u32).to_le_bytes());
1789        bytes.extend_from_slice(&payload);
1790        bytes.extend_from_slice(b"q\x00(");
1791        for _ in 0..40 {
1792            bytes.extend_from_slice(b"h\x00");
1793        }
1794        bytes.extend_from_slice(b"t.");
1795        assert!(matches!(
1796            plain(&bytes).unwrap_err(),
1797            PickleError::InvalidData(msg) if msg.contains("memo bomb")
1798        ));
1799    }
1800
1801    #[test]
1802    fn dup_doubling_is_bounded() {
1803        // DUP then TUPLE2 doubles the object tree every two bytes.
1804        let mut bytes = vec![0x80, 0x02, b'K', 0x01];
1805        for _ in 0..40 {
1806            bytes.extend_from_slice(b"2\x86");
1807        }
1808        bytes.push(b'.');
1809        assert!(matches!(
1810            plain(&bytes).unwrap_err(),
1811            PickleError::InvalidData(msg) if msg.contains("memo bomb")
1812        ));
1813    }
1814
1815    #[test]
1816    fn unicode_opcode_decodes_raw_unicode_escape() {
1817        // pickle.dumps("\u00e9t\u00e9 \u6a21", protocol=0): Latin-1 bytes plus a \u escape.
1818        let bytes = b"V\xe9t\xe9 \\u6a21\np0\n.";
1819        assert!(matches!(
1820            plain(bytes).unwrap(),
1821            Object::String(s) if s == "\u{e9}t\u{e9} \u{6a21}"
1822        ));
1823        assert!(plain(b"V\\u12.\n.").is_err());
1824    }
1825
1826    #[test]
1827    fn rebuild_from_type_v2_rejects_tensor_bearing_state() {
1828        let source = fixture_source();
1829        let callable = Object::Class {
1830            module_name: "torch._tensor".to_string(),
1831            name: "_rebuild_from_type_v2".to_string(),
1832        };
1833        let mut state = HashMap::new();
1834        state.insert(
1835            "extra".to_string(),
1836            rebuild_args("FloatStorage", "0", 32, 0, &[2], &[1], &source),
1837        );
1838        let args = Object::Tuple(vec![
1839            Object::Class {
1840                module_name: "torch._utils".to_string(),
1841                name: "_rebuild_tensor".to_string(),
1842            },
1843            Object::Class {
1844                module_name: "torch".to_string(),
1845                name: "Tensor".to_string(),
1846            },
1847            rebuild_args("FloatStorage", "0", 32, 5, &[2, 3], &[3, 1], &source),
1848            Object::Dict(state),
1849        ]);
1850        assert!(matches!(
1851            reduce(callable, args).unwrap_err(),
1852            PickleError::UnsupportedType(_)
1853        ));
1854    }
1855
1856    #[test]
1857    fn protocol_4_pickle_parses() {
1858        // pickle.dumps({"a": 1, "b": [1.5, True, None], "c": b"xy", "d": {"e": "f"}}, protocol=4)
1859        let bytes: &[u8] = &[
1860            0x80, 0x04, 0x95, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7d, 0x94, 0x28,
1861            0x8c, 0x01, 0x61, 0x94, 0x4b, 0x01, 0x8c, 0x01, 0x62, 0x94, 0x5d, 0x94, 0x28, 0x47,
1862            0x3f, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x88, 0x4e, 0x65, 0x8c, 0x01, 0x63,
1863            0x94, 0x43, 0x02, 0x78, 0x79, 0x94, 0x8c, 0x01, 0x64, 0x94, 0x7d, 0x94, 0x8c, 0x01,
1864            0x65, 0x94, 0x8c, 0x01, 0x66, 0x94, 0x73, 0x75, 0x2e,
1865        ];
1866        let Object::Dict(dict) = plain(bytes).unwrap() else {
1867            panic!("expected dict");
1868        };
1869        assert!(matches!(dict["a"], Object::Int(1)));
1870        let Object::List(items) = &dict["b"] else {
1871            panic!("expected list");
1872        };
1873        assert!(
1874            matches!(items[..], [Object::Float(f), Object::Bool(true), Object::None] if f == 1.5)
1875        );
1876        assert!(matches!(&dict["c"], Object::Bytes(b) if b == b"xy"));
1877        let Object::Dict(inner) = &dict["d"] else {
1878            panic!("expected dict");
1879        };
1880        assert!(matches!(&inner["e"], Object::String(s) if s == "f"));
1881    }
1882
1883    #[test]
1884    fn stack_global_builds_class() {
1885        // pickle.dumps(collections.OrderedDict, protocol=4)
1886        let bytes: &[u8] = &[
1887            0x80, 0x04, 0x95, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8c, 0x0b, b'c',
1888            b'o', b'l', b'l', b'e', b'c', b't', b'i', b'o', b'n', b's', 0x94, 0x8c, 0x0b, b'O',
1889            b'r', b'd', b'e', b'r', b'e', b'd', b'D', b'i', b'c', b't', 0x94, 0x93, 0x94, 0x2e,
1890        ];
1891        assert!(matches!(
1892            plain(bytes).unwrap(),
1893            Object::Class { module_name, name } if module_name == "collections" && name == "OrderedDict"
1894        ));
1895    }
1896
1897    #[test]
1898    fn protocol_0_text_opcodes_parse() {
1899        // pickle.dumps({"n": -3, "f": 2.5, "t": True, "big": 2**40}, protocol=0)
1900        let bytes =
1901            b"(dp0\nVn\np1\nI-3\nsVf\np2\nF2.5\nsVt\np3\nI01\nsVbig\np4\nL1099511627776L\ns.";
1902        let Object::Dict(dict) = plain(bytes).unwrap_or_else(|e| panic!("{e}")) else {
1903            panic!("expected dict");
1904        };
1905        assert!(matches!(dict["n"], Object::Int(-3)));
1906        assert!(matches!(dict["f"], Object::Float(f) if f == 2.5));
1907        assert!(matches!(dict["t"], Object::Bool(true)));
1908        assert!(matches!(dict["big"], Object::Int(1099511627776)));
1909    }
1910
1911    #[test]
1912    fn long1_handles_sign_and_width() {
1913        assert_eq!(int_from_le_bytes(&[]).unwrap(), 0);
1914        assert_eq!(int_from_le_bytes(&[0xff]).unwrap(), -1);
1915        assert_eq!(int_from_le_bytes(&[0x00, 0x01]).unwrap(), 256);
1916        assert_eq!(
1917            int_from_le_bytes(&[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0x00]).unwrap(),
1918            i64::MAX
1919        );
1920        assert!(
1921            int_from_le_bytes(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01]).is_none()
1922        );
1923    }
1924
1925    #[test]
1926    fn oversized_int_beside_weights_stays_opaque() {
1927        // {'seed': 2**64, 'n': 7} in protocol 2 (LONG1) and protocol 0 (LONG).
1928        for bytes in [
1929            b"\x80\x02}q\x00(X\x04\x00\x00\x00seedq\x01\x8a\t\x00\x00\x00\x00\x00\x00\x00\x00\x01X\x01\x00\x00\x00nq\x02K\x07u.".as_slice(),
1930            b"(dp0\nVseed\np1\nL18446744073709551616L\nsVn\np2\nI7\ns.".as_slice(),
1931        ] {
1932            let Object::Dict(dict) = plain(bytes).unwrap_or_else(|e| panic!("{e}")) else {
1933                panic!("expected dict");
1934            };
1935            assert!(matches!(&dict["seed"], Object::Opaque(name) if name == "int"));
1936            assert!(matches!(dict["n"], Object::Int(7)));
1937        }
1938    }
1939
1940    #[test]
1941    fn a_long_that_is_not_a_number_is_an_error() {
1942        // LONG carrying junk rather than digits.
1943        assert!(plain(b"Lnope\n.").is_err());
1944    }
1945
1946    #[test]
1947    fn truncated_string_length_does_not_allocate() {
1948        // BINUNICODE claiming nearly 4 GiB followed by two bytes.
1949        let bytes: &[u8] = &[0x80, 0x02, b'X', 0xff, 0xff, 0xff, 0xff, b'h', b'i', b'.'];
1950        assert!(matches!(plain(bytes).unwrap_err(), PickleError::Io(_)));
1951    }
1952
1953    #[test]
1954    fn unknown_reduce_is_kept_opaque() {
1955        // The shape of a pickled numpy scalar: REDUCE of numpy.core.multiarray.scalar.
1956        let bytes = b"\x80\x02cnumpy.core.multiarray\nscalar\nU\x02f8U\x08\x00\x00\x00\x00\x00\x00\xe0?\x86R.";
1957        assert!(matches!(
1958            plain(bytes).unwrap(),
1959            Object::Opaque(name) if name == "numpy.core.multiarray.scalar"
1960        ));
1961    }
1962
1963    #[test]
1964    fn unknown_call_consuming_tensor_data_is_an_error() {
1965        let source = fixture_source();
1966        let callable = Object::Class {
1967            module_name: "torch._utils".to_string(),
1968            name: "_rebuild_qtensor".to_string(),
1969        };
1970        let args = rebuild_args("FloatStorage", "0", 32, 0, &[2], &[1], &source);
1971        assert!(matches!(
1972            reduce(callable, args).unwrap_err(),
1973            PickleError::UnsupportedType(name) if name == "torch._utils._rebuild_qtensor"
1974        ));
1975    }
1976
1977    #[test]
1978    fn persistent_id_without_source_is_an_error() {
1979        let bytes = b"\x80\x02U\x07storageQ.";
1980        assert!(matches!(
1981            plain(bytes).unwrap_err(),
1982            PickleError::NoDataSource
1983        ));
1984    }
1985
1986    #[test]
1987    fn int_dict_keys_are_accepted_everywhere() {
1988        // SETITEM with an int key, then DICT with an int key.
1989        assert!(
1990            matches!(plain(b"\x80\x02}K\x01K\x02s.").unwrap(), Object::Dict(d) if d.contains_key("1"))
1991        );
1992        assert!(
1993            matches!(plain(b"\x80\x02(K\x03K\x04d.").unwrap(), Object::Dict(d) if d.contains_key("3"))
1994        );
1995    }
1996
1997    #[test]
1998    fn stride_rejects_negative_values() {
1999        let negative_stride = Object::Tuple(vec![Object::Int(-1)]);
2000        assert!(matches!(
2001            parse_dims(&negative_stride, "stride"),
2002            Err(PickleError::InvalidData(msg)) if msg == "stride must be non-negative, got -1"
2003        ));
2004    }
2005
2006    #[test]
2007    fn storage_extent_rejects_rank_mismatch() {
2008        assert!(matches!(
2009            tensor_storage_extent(&[2, 3], &[3], 0),
2010            Err(PickleError::InvalidData(msg))
2011                if msg == "Tensor stride rank 1 does not match shape rank 2"
2012        ));
2013    }
2014
2015    #[test]
2016    fn storage_extent_rejects_overflow() {
2017        assert!(matches!(
2018            tensor_storage_extent(&[usize::MAX], &[2], 0),
2019            Err(PickleError::InvalidData(msg))
2020                if msg == "Tensor stride calculation overflows usize"
2021        ));
2022    }
2023
2024    #[test]
2025    fn strided_views_match_reference() {
2026        // Expected values computed independently as storage[offset + sum(i_k * stride_k)]
2027        // over the fixture storage, whose element k holds the value k. Covers the layouts
2028        // real checkpoints produce: channels_last conv weights, transposes, step slices,
2029        // overlapping as_strided windows, size-one dims with leftover strides, inner
2030        // expands, and empty views at the end of storage.
2031        type Case = (
2032            &'static str,
2033            &'static [usize],
2034            &'static [usize],
2035            usize,
2036            &'static [f32],
2037        );
2038        let cases: &[Case] = &[
2039            (
2040                "channels_last",
2041                &[2, 3, 2, 2][..],
2042                &[12, 1, 6, 3][..],
2043                0,
2044                &[
2045                    0.0, 3.0, 6.0, 9.0, 1.0, 4.0, 7.0, 10.0, 2.0, 5.0, 8.0, 11.0, 12.0, 15.0, 18.0,
2046                    21.0, 13.0, 16.0, 19.0, 22.0, 14.0, 17.0, 20.0, 23.0,
2047                ][..],
2048            ),
2049            (
2050                "transposed",
2051                &[3, 4][..],
2052                &[1, 3][..],
2053                2,
2054                &[
2055                    2.0, 5.0, 8.0, 11.0, 3.0, 6.0, 9.0, 12.0, 4.0, 7.0, 10.0, 13.0,
2056                ][..],
2057            ),
2058            (
2059                "step_slice",
2060                &[5][..],
2061                &[2][..],
2062                1,
2063                &[1.0, 3.0, 5.0, 7.0, 9.0][..],
2064            ),
2065            (
2066                "overlapping_windows",
2067                &[4, 3][..],
2068                &[1, 1][..],
2069                0,
2070                &[0.0, 1.0, 2.0, 1.0, 2.0, 3.0, 2.0, 3.0, 4.0, 3.0, 4.0, 5.0][..],
2071            ),
2072            (
2073                "size_one_dim_odd_stride",
2074                &[1, 3][..],
2075                &[12, 1][..],
2076                4,
2077                &[4.0, 5.0, 6.0][..],
2078            ),
2079            (
2080                "size_one_inner_dim",
2081                &[3, 1][..],
2082                &[1, 7][..],
2083                0,
2084                &[0.0, 1.0, 2.0][..],
2085            ),
2086            (
2087                "expand_inner",
2088                &[2, 3, 2][..],
2089                &[0, 1, 0][..],
2090                3,
2091                &[3.0, 3.0, 4.0, 4.0, 5.0, 5.0, 3.0, 3.0, 4.0, 4.0, 5.0, 5.0][..],
2092            ),
2093            ("empty_leading", &[0, 3][..], &[3, 1][..], 30, &[][..]),
2094            (
2095                "empty_trailing_at_end",
2096                &[2, 0][..],
2097                &[0, 1][..],
2098                32,
2099                &[][..],
2100            ),
2101        ];
2102        let source = fixture_source();
2103        for &(name, shape, stride, offset, expected) in cases {
2104            let to_i64 = |v: &[usize]| v.iter().map(|&x| x as i64).collect::<Vec<_>>();
2105            let tensor = rebuild(rebuild_args(
2106                "FloatStorage",
2107                "0",
2108                32,
2109                offset as i64,
2110                &to_i64(shape),
2111                &to_i64(stride),
2112                &source,
2113            ))
2114            .unwrap_or_else(|e| panic!("{name}: {e}"));
2115            let bytes = tensor.read().unwrap_or_else(|e| panic!("{name}: {e}"));
2116            let data: Vec<f32> = bytemuck::pod_collect_to_vec(&bytes);
2117            assert_eq!(data, expected, "{name}");
2118        }
2119    }
2120
2121    #[test]
2122    fn strided_views_for_other_element_sizes() {
2123        // The same 128 storage bytes viewed as i16, u8 and f64 through non-contiguous
2124        // strides; expected values are the reinterpreted bytes gathered independently.
2125        let source = fixture_source();
2126
2127        let tensor = rebuild(rebuild_args(
2128            "ShortStorage",
2129            "0",
2130            64,
2131            1,
2132            &[2, 3],
2133            &[1, 2],
2134            &source,
2135        ))
2136        .unwrap();
2137        assert_eq!(read_as::<i16>(&tensor), [0, 16256, 16384, 0, 0, 0]);
2138
2139        let tensor = rebuild(rebuild_args(
2140            "ByteStorage",
2141            "0",
2142            128,
2143            2,
2144            &[2, 4],
2145            &[1, 4],
2146            &source,
2147        ))
2148        .unwrap();
2149        assert_eq!(read_as::<u8>(&tensor), [0, 128, 0, 64, 0, 63, 64, 64]);
2150
2151        let tensor = rebuild(rebuild_args(
2152            "DoubleStorage",
2153            "0",
2154            16,
2155            0,
2156            &[2, 2],
2157            &[1, 2],
2158            &source,
2159        ))
2160        .unwrap();
2161        let bits: Vec<u64> = read_as::<f64>(&tensor)
2162            .iter()
2163            .map(|v| v.to_bits())
2164            .collect();
2165        assert_eq!(
2166            bits,
2167            vec![
2168                4575657221408423936,
2169                4656722015783223296,
2170                4629700418010611712,
2171                4674736414296899584
2172            ]
2173        );
2174    }
2175
2176    #[test]
2177    fn stride_rejects_non_integer_values() {
2178        let invalid_stride = Object::Tuple(vec![Object::String("one".to_string())]);
2179        assert!(matches!(
2180            parse_dims(&invalid_stride, "stride"),
2181            Err(PickleError::InvalidData(msg)) if msg == "stride must be an int, got str"
2182        ));
2183    }
2184
2185    #[test]
2186    fn view_beyond_declared_storage_fails_at_parse_time() {
2187        // The persistent id's element count is authoritative even when the file holds
2188        // more bytes: PyTorch's own `set_` raises for a view past the declared storage.
2189        let source = fixture_source();
2190        let err = rebuild(rebuild_args(
2191            "FloatStorage",
2192            "0",
2193            32,
2194            30,
2195            &[2, 3],
2196            &[3, 1],
2197            &source,
2198        ))
2199        .unwrap_err();
2200        assert!(matches!(
2201            err,
2202            PickleError::InvalidData(msg) if msg.contains("needs 36 elements, but storage '0' declares 32")
2203        ));
2204    }
2205
2206    #[test]
2207    fn declared_storage_larger_than_file_fails_at_read_time() {
2208        let source = fixture_source();
2209        let tensor = rebuild(rebuild_args(
2210            "FloatStorage",
2211            "0",
2212            64,
2213            40,
2214            &[2, 3],
2215            &[3, 1],
2216            &source,
2217        ))
2218        .unwrap();
2219        let err = tensor.read().unwrap_err();
2220        assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
2221        assert!(
2222            err.to_string()
2223                .contains("requires 46 elements from storage '0', but only 32 are available"),
2224            "{err}"
2225        );
2226    }
2227
2228    #[test]
2229    fn broadcast_view_above_limit_is_refused() {
2230        let source = fixture_source();
2231        let err = rebuild(rebuild_args(
2232            "FloatStorage",
2233            "0",
2234            32,
2235            0,
2236            &[1 << 21, 1 << 21],
2237            &[0, 0],
2238            &source,
2239        ))
2240        .unwrap_err();
2241        assert!(matches!(
2242            err,
2243            PickleError::InvalidData(msg) if msg.contains("byte limit")
2244        ));
2245    }
2246
2247    #[test]
2248    fn small_broadcast_view_loads() {
2249        let source = fixture_source();
2250        let tensor = rebuild(rebuild_args(
2251            "FloatStorage",
2252            "0",
2253            32,
2254            1,
2255            &[2, 3],
2256            &[0, 1],
2257            &source,
2258        ))
2259        .unwrap();
2260        assert_eq!(read_as::<f32>(&tensor), [1.0, 2.0, 3.0, 1.0, 2.0, 3.0]);
2261    }
2262
2263    #[test]
2264    fn legacy_rebuild_tensor_loads_contiguous_stride() {
2265        let source = fixture_source();
2266        let tensor = rebuild(rebuild_args(
2267            "FloatStorage",
2268            "0",
2269            32,
2270            5,
2271            &[2, 3],
2272            &[3, 1],
2273            &source,
2274        ))
2275        .unwrap();
2276        assert_eq!(read_as::<f32>(&tensor), [5.0, 6.0, 7.0, 8.0, 9.0, 10.0]);
2277    }
2278
2279    #[test]
2280    fn legacy_rebuild_tensor_loads_permuted_stride() {
2281        let source = fixture_source();
2282        let tensor = rebuild(rebuild_args(
2283            "FloatStorage",
2284            "0",
2285            32,
2286            5,
2287            &[2, 4, 3],
2288            &[12, 1, 4],
2289            &source,
2290        ))
2291        .unwrap();
2292        assert_eq!(
2293            read_as::<f32>(&tensor),
2294            [
2295                5.0, 9.0, 13.0, 6.0, 10.0, 14.0, 7.0, 11.0, 15.0, 8.0, 12.0, 16.0, 17.0, 21.0,
2296                25.0, 18.0, 22.0, 26.0, 19.0, 23.0, 27.0, 20.0, 24.0, 28.0,
2297            ]
2298        );
2299    }
2300
2301    #[test]
2302    fn legacy_rebuild_tensor_loads_scalar_at_offset() {
2303        let source = fixture_source();
2304        let tensor = rebuild(rebuild_args("FloatStorage", "0", 32, 5, &[], &[], &source)).unwrap();
2305        assert_eq!(read_as::<f32>(&tensor), [5.0]);
2306    }
2307
2308    #[test]
2309    fn storage_view_offset_is_applied() {
2310        let source = fixture_source();
2311        let mut pid = match storage_pid("FloatStorage", "0", 32) {
2312            Object::Tuple(fields) => fields,
2313            _ => unreachable!(),
2314        };
2315        pid.push(Object::Tuple(vec![
2316            Object::String("7".to_string()),
2317            Object::Int(4),
2318            Object::Int(10),
2319        ]));
2320        let storage = resolve_storage_id(&pid, &source).unwrap();
2321        assert_eq!(storage.view_offset, 4);
2322        let tensor = build_tensor(storage, DType::F32, 1, vec![3], vec![1]).unwrap();
2323        assert_eq!(read_as::<f32>(&tensor), [5.0, 6.0, 7.0]);
2324    }
2325
2326    #[test]
2327    fn rebuild_v3_reads_untyped_storage_with_explicit_dtype() {
2328        let source = fixture_source();
2329        let pid = [
2330            Object::String("storage".to_string()),
2331            Object::Class {
2332                module_name: "torch".to_string(),
2333                name: "UntypedStorage".to_string(),
2334            },
2335            Object::String("0".to_string()),
2336            Object::String("cpu".to_string()),
2337            Object::Int(128),
2338        ];
2339        let storage = resolve_storage_id(&pid, &source).unwrap();
2340        assert_eq!(storage.dtype, None);
2341        let args = Object::Tuple(vec![
2342            Object::Storage(storage),
2343            Object::Int(0),
2344            Object::Tuple(vec![Object::Int(2)]),
2345            Object::Tuple(vec![Object::Int(1)]),
2346            Object::Bool(false),
2347            Object::None,
2348            Object::Class {
2349                module_name: "torch".to_string(),
2350                name: "uint32".to_string(),
2351            },
2352        ]);
2353        let Object::Tensor(tensor) = rebuild_tensor(args, TensorRebuild::V3).unwrap() else {
2354            panic!("expected tensor");
2355        };
2356        assert_eq!(tensor.dtype(), DType::U32);
2357        // The storage holds f32 0.0 and 1.0; read as u32 bit patterns.
2358        assert_eq!(read_as::<u32>(&tensor), [0, 1.0f32.to_bits()]);
2359    }
2360
2361    #[test]
2362    fn rebuild_from_type_v2_dispatches_inner_call() {
2363        // _rebuild_from_type_v2(func, new_type, args, state) for a tensor subclass.
2364        let source = fixture_source();
2365        let callable = Object::Class {
2366            module_name: "torch._tensor".to_string(),
2367            name: "_rebuild_from_type_v2".to_string(),
2368        };
2369        let args = Object::Tuple(vec![
2370            Object::Class {
2371                module_name: "torch._utils".to_string(),
2372                name: "_rebuild_tensor".to_string(),
2373            },
2374            Object::Class {
2375                module_name: "torch".to_string(),
2376                name: "Tensor".to_string(),
2377            },
2378            rebuild_args("FloatStorage", "0", 32, 5, &[2, 3], &[3, 1], &source),
2379            Object::Dict(HashMap::new()),
2380        ]);
2381        let Object::Tensor(tensor) = reduce(callable, args).unwrap() else {
2382            panic!("expected tensor");
2383        };
2384        assert_eq!(read_as::<f32>(&tensor), [5.0, 6.0, 7.0, 8.0, 9.0, 10.0]);
2385    }
2386
2387    #[test]
2388    fn bool_bytes_are_normalized() {
2389        // Read the f32 storage as bools: 0.0 is eight zero bytes, 1.0 is 00 00 80 3f.
2390        let source = fixture_source();
2391        let pid = storage_pid("BoolStorage", "0", 128);
2392        let Object::Tuple(pid) = pid else {
2393            unreachable!()
2394        };
2395        let storage = resolve_storage_id(&pid, &source).unwrap();
2396        let tensor = build_tensor(storage, DType::Bool, 0, vec![8], vec![1]).unwrap();
2397        assert_eq!(tensor.read().unwrap(), [0, 0, 0, 0, 0, 0, 1, 1]);
2398    }
2399
2400    #[test]
2401    fn untyped_storage_with_view_metadata_is_rejected() {
2402        let source = fixture_source();
2403        let pid = [
2404            Object::String("storage".to_string()),
2405            Object::Class {
2406                module_name: "torch".to_string(),
2407                name: "UntypedStorage".to_string(),
2408            },
2409            Object::String("0".to_string()),
2410            Object::String("cpu".to_string()),
2411            Object::Int(128),
2412            Object::Tuple(vec![
2413                Object::String("7".to_string()),
2414                Object::Int(4),
2415                Object::Int(10),
2416            ]),
2417        ];
2418        assert!(matches!(
2419            resolve_storage_id(&pid, &source),
2420            Err(PickleError::InvalidData(msg)) if msg.contains("untyped but carries view metadata")
2421        ));
2422    }
2423
2424    #[test]
2425    fn long_at_i64_boundary_does_not_fit() {
2426        // 2^63 and -2^63-1 need 9 bytes whose sign fill disagrees with byte 7.
2427        assert!(int_from_le_bytes(&[0, 0, 0, 0, 0, 0, 0, 0x80, 0]).is_none());
2428        assert!(
2429            int_from_le_bytes(&[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f, 0xff]).is_none()
2430        );
2431        assert_eq!(
2432            int_from_le_bytes(&[0, 0, 0, 0, 0, 0, 0, 0x80, 0xff]).unwrap(),
2433            i64::MIN
2434        );
2435    }
2436
2437    #[test]
2438    fn deep_nesting_is_rejected() {
2439        // EMPTY_LIST then TUPLE1 repeated wraps the list ever deeper.
2440        let mut bytes = vec![0x80, 0x02, b']'];
2441        bytes.extend(std::iter::repeat_n(0x85, 1500));
2442        bytes.push(b'.');
2443        assert!(matches!(
2444            plain(&bytes).unwrap_err(),
2445            PickleError::InvalidData(msg) if msg.contains("nesting exceeds")
2446        ));
2447
2448        // Just under the limit still parses.
2449        let mut bytes = vec![0x80, 0x02, b']'];
2450        bytes.extend(std::iter::repeat_n(0x85, 900));
2451        bytes.push(b'.');
2452        assert!(plain(&bytes).is_ok());
2453    }
2454
2455    #[test]
2456    fn mark_never_becomes_a_value() {
2457        // MARK TUPLE1: nothing above the mark to wrap.
2458        assert!(matches!(
2459            plain(b"\x80\x02(\x85.").unwrap_err(),
2460            PickleError::InvalidData(msg) if msg.contains("found MARK")
2461        ));
2462        // MARK BINPUT 0: memoizing a mark.
2463        assert!(plain(b"\x80\x02(q\x00.").is_err());
2464        // A bare MARK on top at STOP.
2465        assert!(plain(b"\x80\x02K\x01(.").is_err());
2466        // POP discards a bare mark, as in CPython.
2467        assert!(matches!(
2468            plain(b"\x80\x02K\x01(0.").unwrap(),
2469            Object::Int(1)
2470        ));
2471    }
2472
2473    #[test]
2474    fn build_on_a_tensor_is_an_error() {
2475        let source = fixture_source();
2476        let tensor = rebuild(rebuild_args(
2477            "FloatStorage",
2478            "0",
2479            32,
2480            0,
2481            &[2],
2482            &[1],
2483            &source,
2484        ))
2485        .unwrap();
2486        let err = opaque(&Object::Tensor(tensor), &Object::Dict(HashMap::new())).unwrap_err();
2487        assert!(matches!(err, PickleError::UnsupportedType(_)));
2488    }
2489
2490    /// A module pickled without torch: a class `M` whose `__dict__` holds the module
2491    /// tables, with ints in place of tensors. The root has a parameter slot holding None,
2492    /// a persistent and a non-persistent buffer, a child module, a child slot holding None
2493    /// and two plain attributes. Protocol 2 writes the set as `REDUCE __builtin__.set`,
2494    /// protocol 4 as `EMPTY_SET` and `ADDITEMS`.
2495    const MODULE_PICKLE_PROTO2: &[u8] = b"\x80\x02c__main__\nM\nq\x00)\x81q\x01}q\x02(X\x0b\x00\x00\x00_parametersq\x03}q\x04X\x04\x00\x00\x00biasq\x05NsX\x08\x00\x00\x00_buffersq\x06}q\x07(X\x07\x00\x00\x00runningq\x08K\x02X\x04\x00\x00\x00maskq\x09K\x03uX\x1b\x00\x00\x00_non_persistent_buffers_setq\x0ac__builtin__\nset\nq\x0b]q\x0ch\x09a\x85q\x0dRq\x0eX\x08\x00\x00\x00_modulesq\x0f}q\x10(X\x02\x00\x00\x00fcq\x11h\x00)\x81q\x12}q\x13(h\x03}q\x14X\x06\x00\x00\x00weightq\x15K\x01sh\x06}q\x16h\x0f}q\x17ubX\x04\x00\x00\x00goneq\x18NuX\x08\x00\x00\x00trainingq\x19\x88X\x05\x00\x00\x00scaleq\x1aK\x04ub.";
2496    const MODULE_PICKLE_PROTO4: &[u8] = b"\x80\x04\x95\xcf\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x01M\x94\x93\x94)\x81\x94}\x94(\x8c\x0b_parameters\x94}\x94\x8c\x04bias\x94Ns\x8c\x08_buffers\x94}\x94(\x8c\x07running\x94K\x02\x8c\x04mask\x94K\x03u\x8c\x1b_non_persistent_buffers_set\x94\x8f\x94(h\x0b\x90\x8c\x08_modules\x94}\x94(\x8c\x02fc\x94h\x02)\x81\x94}\x94(h\x05}\x94\x8c\x06weight\x94K\x01sh\x08}\x94h\x0e}\x94ub\x8c\x04gone\x94Nu\x8c\x08training\x94\x88\x8c\x05scale\x94K\x04ub.";
2497
2498    #[test]
2499    fn module_build_becomes_its_state_dict() {
2500        for bytes in [MODULE_PICKLE_PROTO2, MODULE_PICKLE_PROTO4] {
2501            let Object::Dict(dict) = plain(bytes).unwrap_or_else(|e| panic!("{e}")) else {
2502                panic!("expected dict");
2503            };
2504            // The None slots, the non-persistent buffer and the plain attributes are gone.
2505            let mut keys: Vec<_> = dict.keys().cloned().collect();
2506            keys.sort();
2507            assert_eq!(keys, ["fc", "running"]);
2508            assert!(matches!(dict["running"], Object::Int(2)));
2509            let Object::Dict(child) = &dict["fc"] else {
2510                panic!("expected the child module as a dict");
2511            };
2512            assert_eq!(child.len(), 1);
2513            assert!(matches!(child["weight"], Object::Int(1)));
2514        }
2515    }
2516
2517    #[test]
2518    fn module_shared_under_two_names_is_built_under_both() {
2519        // {"model": root, "ema": root} where root has _modules {"a": child, "b": child}:
2520        // the second reference to each instance is a memo fetch (BINGET) of an object
2521        // memoized before its BUILD, which state_dict() nonetheless lists under both names.
2522        let bytes = b"\x80\x02}q\x00(X\x05\x00\x00\x00modelq\x01c__main__\nM\nq\x02)\x81q\x03}q\x04(X\x0b\x00\x00\x00_parametersq\x05}q\x06X\x08\x00\x00\x00_buffersq\x07}q\x08X\x08\x00\x00\x00_modulesq\x09}q\n(X\x01\x00\x00\x00aq\x0bh\x02)\x81q\x0c}q\x0d(h\x05}q\x0eX\x06\x00\x00\x00weightq\x0fK\x01sh\x07}q\x10h\x09}q\x11ubX\x01\x00\x00\x00bq\x12h\x0cuubX\x03\x00\x00\x00emaq\x13h\x03u.";
2523        let Object::Dict(checkpoint) = plain(bytes).unwrap_or_else(|e| panic!("{e}")) else {
2524            panic!("expected dict");
2525        };
2526        for key in ["model", "ema"] {
2527            let Object::Dict(root) = &checkpoint[key] else {
2528                panic!("expected {key} as a dict, got {:?}", checkpoint[key]);
2529            };
2530            for child in ["a", "b"] {
2531                assert!(
2532                    matches!(&root[child], Object::Dict(d) if matches!(d["weight"], Object::Int(1))),
2533                    "{key}.{child}: {:?}",
2534                    root[child]
2535                );
2536            }
2537        }
2538    }
2539
2540    #[test]
2541    fn object_with_a_tensor_attribute_is_refused() {
2542        // An instance of `C` whose `__dict__` is {"w": tensor}: not a module, so the
2543        // tensor must not vanish. The storage is key "0" of the fixture.
2544        let bytes = b"\x80\x02c__main__\nC\nq\x00)\x81q\x01}q\x02X\x01\x00\x00\x00wq\x03ctorch._utils\n_rebuild_tensor_v2\nq\x04((X\x07\x00\x00\x00storageq\x05ctorch\nFloatStorage\nq\x06X\x01\x00\x00\x000q\x07X\x03\x00\x00\x00cpuq\x08K tq\x09QK\x00K\x02\x85q\nK\x01\x85q\x0b\x89ccollections\nOrderedDict\nq\x0c)Rq\x0dtq\x0eRq\x0fsb.";
2545        let ids = PersistentIds::Storages(fixture_source());
2546        assert!(matches!(
2547            read_pickle(&mut Cursor::new(&bytes[..]), &ids),
2548            Err(PickleError::UnsupportedType(name)) if name == "__main__.C"
2549        ));
2550    }
2551
2552    #[test]
2553    fn object_without_module_tables_stays_opaque() {
2554        // An instance of `M` whose `__dict__` is {"x": 1}.
2555        let bytes = b"\x80\x02c__main__\nM\nq\x00)\x81q\x01}q\x02X\x01\x00\x00\x00xq\x03K\x01sb.";
2556        assert!(matches!(
2557            plain(bytes).unwrap(),
2558            Object::Opaque(name) if name == "__main__.M"
2559        ));
2560    }
2561
2562    #[test]
2563    fn module_registering_a_name_twice_is_an_error() {
2564        let table = |value| Object::Dict(HashMap::from([("w".to_string(), value)]));
2565        let attrs = HashMap::from([
2566            ("_parameters".to_string(), table(Object::Int(1))),
2567            ("_buffers".to_string(), table(Object::Int(2))),
2568            ("_modules".to_string(), Object::Dict(HashMap::new())),
2569        ]);
2570        assert!(matches!(
2571            module_state_dict(attrs),
2572            Err(PickleError::InvalidData(msg)) if msg.contains("'w' more than once")
2573        ));
2574    }
2575
2576    #[test]
2577    fn module_state_dict_rejects_a_malformed_buffer_set() {
2578        let mut attrs =
2579            HashMap::from([("_non_persistent_buffers_set".to_string(), Object::Int(1))]);
2580        for table in MODULE_TABLES {
2581            attrs.insert(table.to_string(), Object::Dict(HashMap::new()));
2582        }
2583        assert!(matches!(
2584            module_state_dict(attrs),
2585            Err(PickleError::InvalidData(msg)) if msg.contains("must be a set, got int")
2586        ));
2587    }
2588
2589    #[test]
2590    fn set_reduce_is_read_as_a_list() {
2591        // pickle.dumps({"mask"}, protocol=2) and pickle.dumps(frozenset(), protocol=2).
2592        let items = match plain(
2593            b"\x80\x02c__builtin__\nset\nq\x00]q\x01X\x04\x00\x00\x00maskq\x02a\x85q\x03Rq\x04.",
2594        )
2595        .unwrap()
2596        {
2597            Object::List(items) => items,
2598            other => panic!("expected list, got {other:?}"),
2599        };
2600        assert!(matches!(&items[..], [Object::String(s)] if s == "mask"));
2601        assert!(matches!(
2602            plain(b"\x80\x02c__builtin__\nfrozenset\nq\x00]q\x01\x85q\x02Rq\x03.").unwrap(),
2603            Object::List(items) if items.is_empty()
2604        ));
2605    }
2606
2607    #[test]
2608    fn ordered_dict_rejects_non_list_argument() {
2609        let args = Object::Tuple(vec![Object::Int(3)]);
2610        assert!(matches!(
2611            ordered_dict(args),
2612            Err(PickleError::InvalidData(msg)) if msg.contains("must be a list")
2613        ));
2614    }
2615
2616    #[test]
2617    fn call_with_extra_arguments_is_an_error() {
2618        // set(items, extra) is a TypeError in Python, and the extra could hold anything.
2619        let args = Object::Tuple(vec![Object::List(Vec::new()), Object::Int(1)]);
2620        assert!(matches!(
2621            sequence_arg(args, "set"),
2622            Err(PickleError::InvalidData(msg)) if msg == "set takes one argument, got 2"
2623        ));
2624    }
2625
2626    #[test]
2627    fn missing_storage_returns_contextual_error() {
2628        let source = fixture_source();
2629        let tensor = rebuild(rebuild_args(
2630            "FloatStorage",
2631            "missing",
2632            6,
2633            0,
2634            &[2, 3],
2635            &[3, 1],
2636            &source,
2637        ))
2638        .unwrap();
2639        let err = tensor.read().unwrap_err();
2640        assert_eq!(err.kind(), io::ErrorKind::InvalidData);
2641        assert!(
2642            err.to_string()
2643                .contains("Failed to read storage 'missing' for tensor with shape [2, 3]"),
2644            "{err}"
2645        );
2646    }
2647
2648    #[test]
2649    fn os_errors_keep_their_kind() {
2650        // A read the operating system refuses reaches the caller with the kind the
2651        // operating system gave it, not folded into `InvalidData` with the errors a source
2652        // raises about the file's contents. A handle open for writing only is refused on
2653        // every read, on every platform, once the reader holds it.
2654        use std::io::Read;
2655        let dir = tempfile::tempdir().unwrap();
2656        let file = std::fs::File::create(dir.path().join("write_only.pt")).unwrap();
2657        let expected = (&file).read(&mut [0u8; 8]).unwrap_err();
2658        assert!(expected.raw_os_error().is_some(), "{expected}");
2659
2660        let source = Arc::new(StorageSource::Legacy(crate::storage::LegacySource::new(
2661            file,
2662        )));
2663        let args = rebuild_args("FloatStorage", "0", 3, 0, &[3], &[1], &source);
2664        let StorageSource::Legacy(legacy) = &*source else {
2665            unreachable!()
2666        };
2667        legacy.finish(&["0".to_string()], 0, 8 + 12).unwrap();
2668        let tensor = rebuild(args).unwrap();
2669
2670        let err = tensor.read().unwrap_err();
2671        assert_eq!(err.kind(), expected.kind(), "{err}");
2672        assert!(
2673            err.to_string()
2674                .contains("Failed to read storage '0' for tensor with shape [3]"),
2675            "{err}"
2676        );
2677    }
2678
2679    #[test]
2680    fn tensors_inside_lists_and_tuples_get_indexed_names() {
2681        // Regression test for https://github.com/tracel-ai/burn/issues/5595:
2682        // torch.save({"weights": [w1, w2]}) must expose both tensors instead of dropping them.
2683        let source = fixture_source();
2684        let w1 = rebuild(rebuild_args(
2685            "FloatStorage",
2686            "0",
2687            4,
2688            0,
2689            &[2, 2],
2690            &[2, 1],
2691            &source,
2692        ))
2693        .unwrap();
2694        let w2 = rebuild(rebuild_args(
2695            "FloatStorage",
2696            "0",
2697            9,
2698            0,
2699            &[3, 3],
2700            &[3, 1],
2701            &source,
2702        ))
2703        .unwrap();
2704        let b = rebuild(rebuild_args("FloatStorage", "0", 4, 0, &[4], &[1], &source)).unwrap();
2705
2706        let mut nested = HashMap::new();
2707        nested.insert("w".to_string(), Object::Tensor(w2));
2708
2709        let mut dict = HashMap::new();
2710        // Non-tensor list entries are still ignored, exactly as before.
2711        dict.insert(
2712            "weights".to_string(),
2713            Object::List(vec![
2714                Object::Tensor(w1),
2715                Object::Int(7),
2716                Object::Dict(nested),
2717            ]),
2718        );
2719        dict.insert("bias".to_string(), Object::Tuple(vec![Object::Tensor(b)]));
2720
2721        let mut tensors = extract_tensors(dict);
2722        assert_eq!(tensors.len(), 3);
2723        assert!(tensors.contains_key("weights.0"));
2724        assert!(tensors.contains_key("weights.2.w"));
2725        assert!(tensors.contains_key("bias.0"));
2726
2727        // The indexed names match burn's Vec<Module> naming; the tensor data is intact.
2728        let tensor = tensors.remove("weights.0").unwrap();
2729        assert_eq!(read_as::<f32>(&tensor), [0.0, 1.0, 2.0, 3.0]);
2730    }
2731}