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