use std::io::{self, Cursor, Read};
use anyhow::{Context, Result};
use memmap2::Mmap;
use rayon::prelude::*;
use zip::ZipArchive;
use crate::config::RuntimeConfig;
use crate::parsers::{ParseResult, structured::tensor3d::tensor3d_plane_stats_for_npy_bytes};
use crate::results::{ArrayLayoutSummary, ColumnarCommonFields, NpzMetadata, NpzNpyEntrySummary};
use super::npy::parse_npy_prefix;
use super::sample::{column_common_from_npy_bytes, zip_member_target_read_len};
const MAX_NPZ_NPY_ENTRIES: usize = 128;
const MAX_NPZ_ENTRY_READ: usize = 64 * 1024 * 1024 + 256 * 1024;
const INITIAL_NPZ_READ: usize = 512 * 1024;
struct NpzEntryRead {
name: String,
uncompressed_size: u64,
bytes: Result<Vec<u8>, String>,
}
fn npy_entry_from_read(entry: &NpzEntryRead, config: &RuntimeConfig) -> NpzNpyEntrySummary {
let name = entry.name.clone();
let uncompressed_u64 = entry.uncompressed_size;
let uncompressed_size = entry.uncompressed_size as usize;
let buf = match &entry.bytes {
Err(e) => {
return NpzNpyEntrySummary {
name,
uncompressed_size: Some(uncompressed_u64),
layout: ArrayLayoutSummary::default(),
common: ColumnarCommonFields::default(),
tensor3d: None,
entry_parse_error: Some(e.clone()),
};
}
Ok(b) => b,
};
match parse_npy_prefix(buf, uncompressed_size) {
Ok(layout) => {
let common = column_common_from_npy_bytes(buf, &layout, config);
let tensor3d = tensor3d_plane_stats_for_npy_bytes(buf, &layout);
NpzNpyEntrySummary {
name,
uncompressed_size: Some(uncompressed_u64),
layout,
common,
tensor3d,
entry_parse_error: None,
}
}
Err(e) => NpzNpyEntrySummary {
name,
uncompressed_size: Some(uncompressed_u64),
layout: ArrayLayoutSummary::default(),
common: ColumnarCommonFields::default(),
tensor3d: None,
entry_parse_error: Some(format!("{e:#}")),
},
}
}
fn read_npy_zip_member<R: Read + ?Sized>(
zf: &mut zip::read::ZipFile<'_, R>,
uncompressed_size: usize,
config: &RuntimeConfig,
) -> Result<Vec<u8>> {
let max_read = uncompressed_size.min(MAX_NPZ_ENTRY_READ);
let mut buf = Vec::new();
io::copy(
&mut zf.take(INITIAL_NPZ_READ.min(max_read) as u64),
&mut buf,
)
.context("read npz .npy initial chunk")?;
let logical_len = uncompressed_size;
let mut layout = parse_npy_prefix(&buf, logical_len);
if layout.is_err() && buf.len() < max_read {
io::copy(&mut zf.take((max_read - buf.len()) as u64), &mut buf)
.context("read npz .npy remainder for header")?;
layout = parse_npy_prefix(&buf, logical_len);
}
let layout = layout.context("parse .npy header inside .npz")?;
let need = zip_member_target_read_len(&layout, uncompressed_size, config)
.min(max_read)
.max(buf.len());
if buf.len() < need {
io::copy(&mut zf.take((need - buf.len()) as u64), &mut buf)
.context("read npz .npy sample")?;
}
Ok(buf)
}
pub fn extract_npz_metadata(
mmap: &Mmap,
stats: &ParseResult,
config: &RuntimeConfig,
) -> Result<NpzMetadata> {
let cursor = Cursor::new(mmap.as_ref());
let mut archive = ZipArchive::new(cursor).context("open NPZ as ZIP")?;
let zip_entry_count = archive.len();
let mut raw_entries: Vec<NpzEntryRead> = Vec::new();
let mut npy_entries_scanned = 0usize;
for i in 0..archive.len() {
if raw_entries.len() >= MAX_NPZ_NPY_ENTRIES {
break;
}
let Ok(mut zf) = archive.by_index(i) else {
continue;
};
let name = zf.name().to_string();
#[allow(clippy::case_sensitive_file_extension_comparisons)]
if !name.to_ascii_lowercase().ends_with(".npy") {
continue;
}
npy_entries_scanned = npy_entries_scanned.saturating_add(1);
let uncompressed_size = zf.size() as usize;
let uncompressed_u64 = zf.size();
let bytes =
read_npy_zip_member(&mut zf, uncompressed_size, config).map_err(|e| format!("{e:#}"));
raw_entries.push(NpzEntryRead {
name,
uncompressed_size: uncompressed_u64,
bytes,
});
}
let npy_entries: Vec<NpzNpyEntrySummary> = raw_entries
.par_iter()
.map(|e| npy_entry_from_read(e, config))
.collect();
Ok(NpzMetadata {
byte_count: stats.byte_count,
zip_entry_count: Some(zip_entry_count),
npy_entries_scanned: Some(npy_entries_scanned),
npy_entries: Some(npy_entries),
})
}
crate::no_template_mining!(
extract_npz_templates,
"`NumPy` `.npz`: ZIP listing, per-`.npy` layout, and bounded column stats (same pipeline as `.npy`); no template mining."
);