use std::collections::HashMap;
use std::fmt;
use std::fs::File;
use std::io::{self, BufReader, Read};
use std::path::Path;
use std::sync::Mutex;
use zip::read::ZipFile;
use zip::{CompressionMethod, ZipArchive, ZipReadOptions};
use crate::{MAX_PICKLE_SIZE, PytorchError};
const MAX_TEXT_ENTRY_SIZE: u64 = 4096;
pub(crate) enum StorageSource {
Zip(ZipSource),
Tar(TarSource),
Legacy(LegacySource),
}
impl fmt::Debug for StorageSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Zip(_) => "StorageSource::Zip",
Self::Tar(_) => "StorageSource::Tar",
Self::Legacy(_) => "StorageSource::Legacy",
})
}
}
impl StorageSource {
pub fn read(&self, key: &str, start: usize, max_len: usize) -> io::Result<(Vec<u8>, usize)> {
match self {
Self::Zip(source) => source.read_storage(key, start, max_len),
Self::Tar(source) => source.read_storage(key, start, max_len),
Self::Legacy(source) => source.read_storage(key, start, max_len),
}
}
pub fn declare(&self, key: &str, byte_len: usize, element_size: usize) -> io::Result<()> {
match self {
Self::Legacy(source) => source.declare(key, byte_len, element_size),
Self::Zip(_) | Self::Tar(_) => Ok(()),
}
}
}
fn invalid_data(message: String) -> io::Error {
io::Error::new(io::ErrorKind::InvalidData, message)
}
fn lock_ignoring_poison<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
mutex
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
pub(crate) fn read_exact_len<R: Read>(
reader: &mut R,
len: u64,
capacity_bound: usize,
) -> io::Result<Vec<u8>> {
let capacity = usize::try_from(len).map_or(capacity_bound, |len| len.min(capacity_bound));
let mut buffer = Vec::with_capacity(capacity);
reader.by_ref().take(len).read_to_end(&mut buffer)?;
if buffer.len() as u64 != len {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
format!("expected {len} bytes, found {}", buffer.len()),
));
}
Ok(buffer)
}
pub(crate) struct ZipSource {
archive: Mutex<ZipArchive<BufReader<File>>>,
file: File,
root: String,
file_len: u64,
}
impl ZipSource {
pub fn open(path: &Path) -> Result<Self, PytorchError> {
let file = File::open(path)?;
let file_len = file.metadata()?.len();
let archive = ZipArchive::new(BufReader::new(file.try_clone()?))?;
let root = archive
.file_names()
.collect::<zip::result::ZipResult<Vec<_>>>()?
.iter()
.filter_map(|name| name.strip_suffix("data.pkl"))
.filter(|root| root.is_empty() || root.ends_with('/'))
.min_by_key(|root| root.len())
.map(str::to_string)
.ok_or_else(|| {
PytorchError::InvalidFormat(
"No data.pkl entry found in ZIP archive. Expected a PyTorch 1.6+ checkpoint"
.to_string(),
)
})?;
Ok(Self {
archive: Mutex::new(archive),
file,
root,
file_len,
})
}
pub fn data_size(&self) -> io::Result<u64> {
let data_prefix = format!("{}data/", self.root);
let mut archive = lock_ignoring_poison(&self.archive);
let indices: Vec<usize> = archive
.file_names()
.collect::<zip::result::ZipResult<Vec<_>>>()?
.iter()
.filter(|name| name.starts_with(&data_prefix) && !name.ends_with('/'))
.filter_map(|name| archive.index_for_name(name))
.collect();
let mut data_size = 0u64;
for index in indices {
data_size = data_size.saturating_add(archive.by_index_raw(index)?.size());
}
Ok(data_size)
}
pub fn pickle(&self) -> io::Result<Vec<u8>> {
self.read_entry(&format!("{}data.pkl", self.root), MAX_PICKLE_SIZE)
}
pub fn read_text(&self, name: &str) -> io::Result<Option<String>> {
let full_name = format!("{}{name}", self.root);
if !self.has_entry(name) {
return Ok(None);
}
let bytes = self.read_entry(&full_name, MAX_TEXT_ENTRY_SIZE)?;
let text = String::from_utf8(bytes)
.map_err(|err| invalid_data(format!("ZIP entry '{full_name}': {err}")))?;
Ok(Some(text.trim().to_string()))
}
pub fn has_entry(&self, name: &str) -> bool {
let full_name = format!("{}{name}", self.root);
lock_ignoring_poison(&self.archive)
.index_for_name(&full_name)
.is_some()
}
fn read_storage(
&self,
key: &str,
start: usize,
max_len: usize,
) -> io::Result<(Vec<u8>, usize)> {
let name = format!("{}data/{key}", self.root);
let mut archive = lock_ignoring_poison(&self.archive);
let mut entry = open_entry(&mut archive, &name)?;
let size = entry.size();
let skipped = (start as u64).min(size);
let rest = size - skipped;
let len = rest.min(max_len as u64);
if cfg!(unix) && entry.compression() == CompressionMethod::Stored {
if entry.compressed_size() != size {
return Err(invalid_data(format!(
"ZIP entry '{name}' declares {size} bytes but stores {}",
entry.compressed_size()
)));
}
let data_start = entry
.data_start()
.ok_or_else(|| invalid_data(format!("ZIP entry '{name}' has no data offset")))?;
let crc32 = (len == size)
.then_some(entry.crc32())
.filter(|&crc32| crc32 != 0);
drop(entry);
drop(archive);
let bytes = self.read_stored(&name, data_start, skipped, len, crc32)?;
return Ok((bytes, skipped as usize));
}
io::copy(&mut (&mut entry).take(skipped), &mut io::sink())?;
let bytes = read_zip_entry(&mut entry, &name, len, rest, self.file_len)?;
Ok((bytes, skipped as usize))
}
fn read_stored(
&self,
name: &str,
data_start: u64,
skipped: u64,
len: u64,
crc32: Option<u32>,
) -> io::Result<Vec<u8>> {
let offset = data_start
.checked_add(skipped)
.filter(|offset| {
offset
.checked_add(len)
.is_some_and(|end| end <= self.file_len)
})
.ok_or_else(|| {
invalid_data(format!(
"ZIP entry '{name}' extends beyond the end of the file"
))
})?;
let mut bytes = vec![0u8; len as usize];
read_exact_at(&self.file, &mut bytes, offset).map_err(|err| match err.kind() {
io::ErrorKind::UnexpectedEof => io::Error::new(
io::ErrorKind::UnexpectedEof,
format!(
"ZIP entry '{name}' ends before its {len} bytes: the file shrank after it was opened"
),
),
_ => err,
})?;
if crc32.is_some_and(|crc32| crc32fast::hash(&bytes) != crc32) {
return Err(invalid_data(format!(
"ZIP entry '{name}': Invalid checksum"
)));
}
Ok(bytes)
}
fn read_entry(&self, name: &str, max_size: u64) -> io::Result<Vec<u8>> {
let mut archive = lock_ignoring_poison(&self.archive);
let mut entry = open_entry(&mut archive, name)?;
let size = entry.size();
if size > max_size {
return Err(invalid_data(format!(
"ZIP entry '{name}' is {size} bytes, above the {max_size} byte limit"
)));
}
read_zip_entry(&mut entry, name, size, size, self.file_len)
}
}
fn open_entry<'a>(
archive: &'a mut ZipArchive<BufReader<File>>,
name: &str,
) -> io::Result<ZipFile<'a, BufReader<File>>> {
let index = archive
.index_for_name(name)
.ok_or_else(|| invalid_data(format!("ZIP entry '{name}': not found in archive")))?;
let crc32 = archive
.by_index_raw(index)
.map_err(|err| invalid_data(format!("ZIP entry '{name}': {err}")))?
.crc32();
archive
.by_index_with_options(index, ZipReadOptions::new().ignore_crc32(crc32 == 0))
.map_err(|err| invalid_data(format!("ZIP entry '{name}': {err}")))
}
fn read_zip_entry<R: Read>(
entry: &mut R,
name: &str,
len: u64,
size: u64,
capacity_bound: u64,
) -> io::Result<Vec<u8>> {
let capacity_bound = usize::try_from(capacity_bound).unwrap_or(usize::MAX);
let bytes = read_exact_len(entry, len, capacity_bound)?;
if len == size {
let mut probe = [0u8; 1];
let trailing = entry
.read(&mut probe)
.map_err(|err| invalid_data(format!("ZIP entry '{name}': {err}")))?;
if trailing != 0 {
return Err(invalid_data(format!(
"ZIP entry '{name}' holds more than the {size} bytes it declares"
)));
}
}
Ok(bytes)
}
#[cfg(unix)]
fn read_exact_at(file: &File, buf: &mut [u8], offset: u64) -> io::Result<()> {
std::os::unix::fs::FileExt::read_exact_at(file, buf, offset)
}
#[cfg(windows)]
fn read_exact_at(file: &File, mut buf: &mut [u8], mut offset: u64) -> io::Result<()> {
use std::os::windows::fs::FileExt;
while !buf.is_empty() {
match file.seek_read(buf, offset) {
Ok(0) => return Err(io::ErrorKind::UnexpectedEof.into()),
Ok(read) => {
buf = &mut buf[read..];
offset += read as u64;
}
Err(err) if err.kind() == io::ErrorKind::Interrupted => {}
Err(err) => return Err(err),
}
}
Ok(())
}
#[cfg(not(any(unix, windows)))]
fn read_exact_at(_file: &File, _buf: &mut [u8], _offset: u64) -> io::Result<()> {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"no positional file read on this target",
))
}
pub(crate) struct TarSource {
blob: Vec<u8>,
layout: HashMap<String, (usize, usize)>,
}
impl TarSource {
pub fn new(blob: Vec<u8>, layout: HashMap<String, (usize, usize)>) -> Self {
Self { blob, layout }
}
fn read_storage(
&self,
key: &str,
start: usize,
max_len: usize,
) -> io::Result<(Vec<u8>, usize)> {
let &(offset, len) = self
.layout
.get(key)
.ok_or_else(|| invalid_data(format!("storage '{key}' not found in TAR archive")))?;
let start = start.min(len);
let window = (len - start).min(max_len);
offset
.checked_add(start)
.and_then(|from| Some(from..from.checked_add(window)?))
.and_then(|range| self.blob.get(range))
.map(|bytes| (bytes.to_vec(), start))
.ok_or_else(|| invalid_data(format!("storage '{key}' lies outside the TAR data")))
}
}
pub(crate) struct LegacySource {
file: File,
state: Mutex<LegacyState>,
}
enum LegacyState {
Declaring(HashMap<String, (usize, usize)>),
Finished(HashMap<String, (u64, usize, usize)>),
}
impl LegacySource {
pub fn new(file: File) -> Self {
Self {
file,
state: Mutex::new(LegacyState::Declaring(HashMap::new())),
}
}
fn declare(&self, key: &str, byte_len: usize, element_size: usize) -> io::Result<()> {
let mut state = lock_ignoring_poison(&self.state);
let LegacyState::Declaring(declared) = &mut *state else {
return Err(invalid_data(format!(
"storage '{key}' declared after the legacy layout was finalized"
)));
};
match declared.get(key) {
Some(&(prior_len, prior_size))
if (prior_len, prior_size) != (byte_len, element_size) =>
{
Err(invalid_data(format!(
"storage '{key}' is declared twice with different sizes ({prior_len} and {byte_len} bytes)"
)))
}
Some(_) => Ok(()),
None => {
declared.insert(key.to_string(), (byte_len, element_size));
Ok(())
}
}
}
pub fn finish(&self, keys: &[String], data_start: u64, file_len: u64) -> io::Result<()> {
let mut state = lock_ignoring_poison(&self.state);
let LegacyState::Declaring(declared) = &*state else {
return Err(invalid_data(
"legacy storage layout finalized twice".to_string(),
));
};
let mut layout = HashMap::with_capacity(keys.len());
let mut offset = data_start;
for key in keys {
let &(byte_len, element_size) = declared.get(key).ok_or_else(|| {
invalid_data(format!(
"storage '{key}' is listed in the legacy key list but never referenced by the pickle"
))
})?;
let end = offset
.checked_add(8)
.and_then(|start| start.checked_add(byte_len as u64))
.filter(|&end| end <= file_len)
.ok_or_else(|| {
invalid_data(format!(
"storage '{key}' ({byte_len} bytes) extends beyond the end of the file"
))
})?;
layout.insert(key.clone(), (offset, byte_len, element_size));
offset = end;
}
*state = LegacyState::Finished(layout);
Ok(())
}
fn read_storage(
&self,
key: &str,
start: usize,
max_len: usize,
) -> io::Result<(Vec<u8>, usize)> {
let (offset, byte_len, element_size) = {
let state = lock_ignoring_poison(&self.state);
let LegacyState::Finished(layout) = &*state else {
return Err(invalid_data(
"legacy storage layout was never finalized".to_string(),
));
};
*layout
.get(key)
.ok_or_else(|| invalid_data(format!("storage '{key}' not found in legacy file")))?
};
let mut count = [0u8; 8];
read_exact_at(&self.file, &mut count, offset)?;
let stored_numel = i64::from_le_bytes(count);
let expected_numel = (byte_len / element_size) as i64;
if stored_numel != expected_numel {
return Err(invalid_data(format!(
"storage '{key}' holds {stored_numel} elements but the pickle declares {expected_numel}"
)));
}
let start = start.min(byte_len);
let len = (byte_len - start).min(max_len);
let mut bytes = vec![0u8; len];
read_exact_at(&self.file, &mut bytes, offset + 8 + start as u64)?;
Ok((bytes, start))
}
}