Skip to main content

pith_zip/
reference.rs

1//! The canonical member-stream serialization the `reference.json`
2//! vectors and the C ABI surface (`ffi`) are defined over.
3//!
4//! # Wire format
5//!
6//! Every integer is big-endian. The stream a member-emitting FFI call
7//! hands out is exactly:
8//!
9//! 1. `member_count` as `u32`;
10//! 2. per member, in central-directory order:
11//!    - `name_len` as `u32`, followed by the member name as UTF-8
12//!      bytes (the reader refuses non-UTF-8 names at
13//!      [`ZipArchive::new`], so the name is always valid);
14//!    - `method` as `u16` (`0` stored, `8` DEFLATE);
15//!    - `data_descriptor` as one byte, `0` or `1`;
16//!    - `compressed_size` as `u64`;
17//!    - `uncompressed_size` as `u64`;
18//!    - `crc32` as `u32`;
19//!    - `content_len` as `u64`, followed by exactly that many bytes of
20//!      the extracted member content (CRC-verified by extraction).
21//!
22//! The per-member `sha256` digests in `reference.json` cover the
23//! `content` bytes alone; the member-emitting FFI operation returns the
24//! whole stream, so the language SDKs re-derive every recorded field
25//! from one buffer.
26//!
27//! This module is the single definition of that format: the vector
28//! generator (`tools/gen-reference`) serializes its own records and
29//! shares no code with the runtime, so nothing was moved here — the
30//! generator stays untouched and the SDK-facing contract lives only in
31//! this file.
32
33use crate::reader::{ZipArchive, ZipEntry};
34use pith_digest::{Error, Result};
35
36/// Serializes one member — its central-directory facts plus the
37/// extracted `content` — into the canonical stream fragment (see the
38/// module docs for the exact layout).
39pub fn member_bytes(entry: &ZipEntry<'_>, content: &[u8]) -> Result<Vec<u8>> {
40    let name = entry.name().as_bytes();
41    let name_len = u32::try_from(name.len())
42        .map_err(|_| Error::BadValue("zip member name exceeds u32 length"))?;
43    let content_len = u64::try_from(content.len())
44        .map_err(|_| Error::BadValue("zip member content exceeds u64 length"))?;
45    let mut out = Vec::with_capacity(4 + name.len() + 2 + 1 + 8 + 8 + 4 + 8 + content.len());
46    out.extend_from_slice(&name_len.to_be_bytes());
47    out.extend_from_slice(name);
48    out.extend_from_slice(&entry.method().to_be_bytes());
49    out.push(u8::from(entry.uses_data_descriptor()));
50    out.extend_from_slice(&entry.compressed_size().to_be_bytes());
51    out.extend_from_slice(&entry.uncompressed_size().to_be_bytes());
52    out.extend_from_slice(&entry.crc32().to_be_bytes());
53    out.extend_from_slice(&content_len.to_be_bytes());
54    out.extend_from_slice(content);
55    Ok(out)
56}
57
58/// Serializes a whole parsed archive into the canonical byte stream the
59/// language SDKs receive: the `u32` member count followed by every
60/// member in central-directory order, each extracted and CRC-verified.
61///
62/// Extraction failures (unsupported method, size mismatch, CRC
63/// mismatch, truncated payload) propagate as the named
64/// [`Error`](pith_digest::Error) — the caller, not this module, decides
65/// the status code.
66pub fn members_stream(archive: &ZipArchive<'_>) -> Result<Vec<u8>> {
67    let count = u32::try_from(archive.len())
68        .map_err(|_| Error::BadValue("zip member count exceeds u32"))?;
69    let mut out = Vec::new();
70    out.extend_from_slice(&count.to_be_bytes());
71    for entry in archive.entries() {
72        let content = archive.extract(entry)?;
73        out.extend_from_slice(&member_bytes(entry, &content)?);
74    }
75    Ok(out)
76}