use std::fs::File;
use std::io::Read;
use ztensor::catalog::{Entry, Location, PartEntry, Payload};
use ztensor::{Catalog, DType, Error, Opaque, Result, Store, StoreId, Vocabulary};
use crate::project::Projection;
fn bad(detail: impl Into<String>) -> Error {
Error::InvalidInput(format!("npz: {}", detail.into()))
}
fn map_descr(descr: &str) -> Result<(DType, Option<&'static str>)> {
Ok(match descr {
"<f8" | "=f8" => (DType::F64, None),
"<f4" | "=f4" => (DType::F32, None),
"<f2" | "=f2" => (DType::F16, None),
"<i8" | "=i8" => (DType::I64, None),
"<i4" | "=i4" => (DType::I32, None),
"<i2" | "=i2" => (DType::I16, None),
"|i1" => (DType::I8, None),
"<u8" | "=u8" => (DType::U64, None),
"<u4" | "=u4" => (DType::U32, None),
"<u2" | "=u2" => (DType::U16, None),
"|u1" => (DType::U8, None),
"|b1" => (DType::U8, Some("bool")),
other => {
return Err(Error::Unsupported(format!(
"numpy descr {other:?} has no projection (big-endian and object dtypes are refused)"
)))
}
})
}
pub(crate) struct NpyHeader {
pub dtype: DType,
pub ltype: Option<&'static str>,
pub shape: Vec<u64>,
pub data_offset: usize,
}
pub(crate) fn parse_npy_header(data: &[u8]) -> Result<NpyHeader> {
if data.len() < 10 || &data[..6] != b"\x93NUMPY" {
return Err(bad("not a .npy entry"));
}
let major = data[6];
let (header_len, header_start): (usize, usize) = if major >= 2 {
if data.len() < 12 {
return Err(bad("truncated header"));
}
(
u32::from_le_bytes(data[8..12].try_into().unwrap()) as usize,
12,
)
} else {
(
u16::from_le_bytes(data[8..10].try_into().unwrap()) as usize,
10,
)
};
let header_end = header_start
.checked_add(header_len)
.filter(|&e| e <= data.len())
.ok_or_else(|| bad("header extends past entry"))?;
let header = std::str::from_utf8(&data[header_start..header_end])
.map_err(|_| bad("header is not UTF-8"))?;
if find_flag(header, "'fortran_order'")? {
return Err(Error::Unsupported(
"npz: fortran_order arrays are refused (reversing the shape would silently \
transpose the data)"
.into(),
));
}
let descr = find_str(header, "'descr'").ok_or_else(|| bad("header missing 'descr'"))?;
let (dtype, ltype) = map_descr(&descr)?;
let shape = find_shape(header)?;
Ok(NpyHeader {
dtype,
ltype,
shape,
data_offset: header_end,
})
}
fn find_str(header: &str, key: &str) -> Option<String> {
let after = header.split(key).nth(1)?.trim_start().strip_prefix(':')?;
let t = after.trim_start();
let quote = t.chars().next().filter(|&q| q == '\'' || q == '"')?;
let inner = &t[1..];
Some(inner[..inner.find(quote)?].to_string())
}
fn find_flag(header: &str, key: &str) -> Result<bool> {
let Some(after) = header.split(key).nth(1) else {
return Ok(false);
};
let after = after.trim_start();
let after = after.strip_prefix(':').unwrap_or(after).trim_start();
Ok(after.starts_with("True"))
}
fn find_shape(header: &str) -> Result<Vec<u64>> {
let after = header
.split("'shape'")
.nth(1)
.and_then(|s| s.trim_start().strip_prefix(':'))
.ok_or_else(|| bad("header missing 'shape'"))?;
let open = after.find('(').ok_or_else(|| bad("shape missing '('"))?;
let close = after.find(')').ok_or_else(|| bad("shape missing ')'"))?;
if close < open {
return Err(bad("malformed shape tuple"));
}
let mut dims = Vec::new();
for tok in after[open + 1..close].split(',') {
let tok = tok.trim();
if tok.is_empty() {
continue; }
dims.push(
tok.parse::<u64>()
.map_err(|_| bad(format!("bad shape dim {tok:?}")))?,
);
}
Ok(dims)
}
enum Where {
Stored { offset: u64, length: u64 },
Deflated {
zip_index: usize,
data_offset: usize,
},
}
struct Deflated {
archive: std::sync::Mutex<zip::ZipArchive<File>>,
entries: Vec<(usize, usize)>,
}
impl Opaque for Deflated {
fn read(&self, key: u64, decoded_len: u64) -> Result<Vec<u8>> {
let (zip_index, data_offset) = *self
.entries
.get(key as usize)
.ok_or_else(|| bad(format!("no deflated entry {key}")))?;
let expected = crate::safe::add("npz entry", data_offset as u64, decoded_len)?;
let cap = crate::safe::alloc_size("npz entry", expected)?;
let mut archive = self.archive.lock().expect("npz archive lock");
let mut entry = archive
.by_index(zip_index)
.map_err(|e| bad(format!("ZIP entry: {e}")))?;
let mut bytes = Vec::with_capacity(cap);
std::io::Read::take(&mut entry, expected + 1)
.read_to_end(&mut bytes)
.map_err(|e| bad(format!("decompress: {e}")))?;
if bytes.len() as u64 != expected {
return Err(bad("decompressed size mismatch"));
}
Ok(bytes.split_off(data_offset))
}
}
pub(crate) fn project(store: &Store) -> Result<Projection> {
let vocab = Vocabulary::standard();
let mut archive = zip::ZipArchive::new(File::open(store.path())?)
.map_err(|e| bad(format!("not a ZIP archive: {e}")))?;
let mut catalog = Catalog::new();
let mut deflated: Vec<(usize, usize)> = Vec::new();
for i in 0..archive.len() {
let mut entry = archive
.by_index(i)
.map_err(|e| bad(format!("ZIP entry {i}: {e}")))?;
let Some(name) = entry.name().strip_suffix(".npy").map(str::to_string) else {
continue;
};
let stored = entry.compression() == zip::CompressionMethod::Stored;
let (header, placed) = if stored {
let start = entry.data_start();
let size = entry.size();
if crate::safe::add("npz entry", start, size)? > store.len() {
return Err(bad(format!("entry {name:?} extends past file")));
}
let probe = store.read(start, size.min(4096))?;
let header = parse_npy_header(&probe)?;
let data_off = crate::safe::add("npz entry", start, header.data_offset as u64)?;
let length = size - header.data_offset as u64;
(
header,
Where::Stored {
offset: data_off,
length,
},
)
} else {
let mut head = vec![0u8; 4096.min(entry.size() as usize)];
entry
.read_exact(&mut head)
.map_err(|e| bad(format!("entry {name:?}: {e}")))?;
let header = parse_npy_header(&head)?;
let data_offset = header.data_offset;
(
header,
Where::Deflated {
zip_index: i,
data_offset,
},
)
};
let entry_size = entry.size();
drop(entry);
let elems = crate::safe::product("npz shape", &header.shape)?;
let expected = vocab
.size_of(header.ltype, header.dtype, elems)
.ok_or_else(|| bad("size not computable"))?;
let actual = match &placed {
Where::Stored { length, .. } => *length,
Where::Deflated { data_offset, .. } => entry_size - *data_offset as u64,
};
if actual != expected {
return Err(bad(format!(
"entry {name:?} holds {actual} bytes but shape implies {expected}"
)));
}
let payload = match placed {
Where::Stored { offset, length } => Payload::At(Location {
store: StoreId(0),
offset,
len: length,
}),
Where::Deflated {
zip_index,
data_offset,
} => {
deflated.push((zip_index, data_offset));
Payload::Opaque {
store: StoreId(0),
key: deflated.len() as u64 - 1,
decoded_len: expected,
}
}
};
let mut parts = std::collections::BTreeMap::new();
parts.insert(
"data".to_string(),
PartEntry {
dtype: header.dtype,
logical: header.ltype.map(str::to_string),
payload,
digest: None,
},
);
if catalog
.insert(
name.clone(),
Entry {
shape: header.shape,
layout: "dense".to_string(),
attributes: None,
parts,
},
)
.is_some()
{
return Err(bad(format!("duplicate entry name {name:?}")));
}
}
let projection = Projection::new(catalog);
Ok(if deflated.is_empty() {
projection
} else {
projection.with_opaque(Box::new(Deflated {
archive: std::sync::Mutex::new(archive),
entries: deflated,
}))
})
}