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