Skip to main content

structured_asic/
zip.rs

1//! Immutable ZIP occurrences and bounded original-member acquisition.
2
3use crate::{
4    Session,
5    error::{Error, resource},
6};
7use structured_asn1::{
8    Span,
9    memory::{OwnedVec, Work},
10    range::{RangeSource, Stamp, Window},
11};
12
13mod directory;
14mod framing;
15mod stream;
16pub use stream::{Acquisition, Integrity, ReadFailure, Source};
17
18/// One original central-directory ordinal, bound to its immutable source view.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub struct MemberId<D, G> {
21    stamp: Stamp<D, G>,
22    ordinal: usize,
23}
24impl<D: Copy, G: Copy> MemberId<D, G> {
25    /// Original central-directory position, never sorted-name position.
26    #[must_use]
27    pub fn ordinal(&self) -> usize {
28        self.ordinal
29    }
30    /// Original source identity, generation and exact length.
31    #[must_use]
32    pub fn stamp(&self) -> Stamp<D, G> {
33        self.stamp
34    }
35}
36
37/// A readable negative original fact. No invalid occurrence is filtered out.
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum FaultKind {
40    /// Original name/comment is not UTF-8, as required by EN319162-1 ยง4.2.
41    Utf8,
42    /// A name is empty, absolute, or has a drive/device/backslash component.
43    Name,
44    /// Identical original names identify several member occurrences.
45    DuplicateName,
46    /// Local and central metadata do not describe the same member.
47    LocalMismatch,
48    /// A descriptor is missing or disagrees with the central entry.
49    Descriptor,
50    /// Original member enclosures overlap, including duplicate local offsets.
51    Overlap,
52    /// A local enclosure cannot be bounded before the central directory.
53    LocalBounds,
54    /// Stored compression has unequal compressed/uncompressed lengths.
55    StoredLength,
56    /// Entry uses encryption, independently of any claimed `ASiC` profile.
57    Encrypted,
58    /// This compression method is unavailable to the decoder.
59    Compression,
60    /// ISO21320 excludes the original flags/version needed for extraction.
61    IsoProfile,
62    /// A ZIP64 optional record is duplicate, missing or structurally invalid.
63    Zip64,
64    /// A Unicode path extra field disagrees with the original name.
65    UnicodeName,
66    /// A central-directory signature is outside the ISO21320 profile.
67    DirectorySignature,
68}
69
70/// One original negative fact, retaining its exact physical location.
71#[derive(Debug, Clone, Copy)]
72pub struct Fault {
73    /// Original member ordinal; None denotes archive-level metadata.
74    pub member: Option<usize>,
75    /// Original physical enclosure.
76    pub range: Span,
77    /// Independent negative fact.
78    pub kind: FaultKind,
79}
80
81#[derive(Clone, Copy)]
82struct Bytes {
83    start: usize,
84    length: usize,
85}
86impl Bytes {
87    fn get(self, values: &[u8]) -> &[u8] {
88        &values[self.start..self.start + self.length]
89    }
90}
91
92struct Entry {
93    central: Span,
94    name_span: Span,
95    extra: Span,
96    comment: Span,
97    name: Bytes,
98    needed: u16,
99    flags: u16,
100    method: u16,
101    crc: u32,
102    compressed: u64,
103    decoded: u64,
104    local: u64,
105    local_extra: Option<Span>,
106    data: Option<Span>,
107    enclosure: Option<Span>,
108    zip64: bool,
109    readable: bool,
110    disk: u32,
111    unicode: Option<Span>,
112    overlap: bool,
113}
114
115/// Complete original central-directory census. Names occupy one pooled buffer;
116/// entry order is never replaced by a map's unique-name interpretation.
117pub struct Archive<'s, S: RangeSource> {
118    source: &'s S,
119    session: &'s Session,
120    stamp: Stamp<S::Document, S::Generation>,
121    directory: Span,
122    comment: Span,
123    entries: OwnedVec<Entry>,
124    names: OwnedVec<u8>,
125    order: OwnedVec<usize>,
126    faults: OwnedVec<Fault>,
127}
128
129/// A selected original occurrence; construction checks its immutable archive.
130pub struct Member<'view, 'source, S: RangeSource> {
131    archive: &'view Archive<'source, S>,
132    ordinal: usize,
133}
134
135impl<'s, S: RangeSource> Archive<'s, S> {
136    pub(crate) fn session(&self) -> &'s Session {
137        self.session
138    }
139    /// Reads original ZIP/ZIP64 metadata under source AND current execution.
140    /// No payload is inflated and no filesystem path is acquired.
141    pub async fn read<W: Work>(
142        source: &'s S,
143        session: &'s Session,
144        work: &W,
145    ) -> Result<Self, Error<S::Error>> {
146        directory::read(source, session, work).await
147    }
148    /// Immutable source metadata captured before directory acquisition.
149    #[must_use]
150    pub fn stamp(&self) -> Stamp<S::Document, S::Generation> {
151        self.stamp
152    }
153    pub(crate) fn unchanged(&self) -> bool {
154        self.source.stamp() == self.stamp
155    }
156    /// Number of original occurrences, including duplicates and invalid entries.
157    #[must_use]
158    pub fn len(&self) -> usize {
159        self.entries.len()
160    }
161    /// Whether the original directory contains no members.
162    #[must_use]
163    pub fn is_empty(&self) -> bool {
164        self.entries.is_empty()
165    }
166    /// Original archive comment bytes, distinct from a filename/MIME hint.
167    #[must_use]
168    pub fn comment_span(&self) -> Span {
169        self.comment
170    }
171    /// Original directory enclosure.
172    #[must_use]
173    pub fn directory_span(&self) -> Span {
174        self.directory
175    }
176
177    pub(crate) fn admit<W: Work>(&self, work: &W, range: Span) -> Result<(), Error<S::Error>> {
178        self.session
179            .query(work)
180            .charge_work(1)
181            .map_err(|cause| resource(range, cause))?;
182        if self.source.stamp() != self.stamp {
183            return Err(structured_asn1::range::InputError::Changed { range }.into());
184        }
185        Ok(())
186    }
187
188    /// Selects by original ordinal after current/source admission. No lookup
189    /// chooses one copy of a duplicated name or drops malformed occurrences.
190    pub fn member<W: Work>(
191        &self,
192        ordinal: usize,
193        work: &W,
194    ) -> Result<Option<Member<'_, 's, S>>, Error<S::Error>> {
195        self.admit(work, self.directory)?;
196        if ordinal >= self.entries.len() {
197            return Ok(None);
198        }
199        Ok(Some(Member {
200            archive: self,
201            ordinal,
202        }))
203    }
204    /// Selects an existing ID only within its exact original source view.
205    pub fn original<W: Work>(
206        &self,
207        id: MemberId<S::Document, S::Generation>,
208        work: &W,
209    ) -> Result<Member<'_, 's, S>, Error<S::Error>> {
210        self.admit(work, self.directory)?;
211        if id.stamp != self.stamp || id.ordinal >= self.entries.len() {
212            return Err(Error::Foreign {
213                range: self.directory,
214            });
215        }
216        Ok(Member {
217            archive: self,
218            ordinal: id.ordinal,
219        })
220    }
221    /// Visits every negative fact in original discovery order under this query.
222    /// Refusal is returned once and permanently fuses traversal.
223    pub fn faults<'v, 'q, W: Work>(
224        &'v self,
225        work: &'q W,
226    ) -> crate::error::SourceResult<
227        impl Iterator<Item = Result<Fault, Error<S::Error>>> + use<'v, 'q, 's, S, W>,
228        S,
229    > {
230        self.admit(work, self.directory)?;
231        let mut at = 0;
232        let mut refused = false;
233        Ok(core::iter::from_fn(move || {
234            if refused || at == self.faults.len() {
235                return None;
236            }
237            if let Err(cause) = self.admit(work, self.directory) {
238                refused = true;
239                return Some(Err(cause));
240            }
241            let value = self.faults[at];
242            at += 1;
243            Some(Ok(value))
244        }))
245    }
246
247    /// Exact original-name lookup. The result keeps the complete duplicate
248    /// range; no Unicode/case normalization or filesystem interpretation occurs.
249    pub fn find<W: Work>(
250        &self,
251        name: &[u8],
252        work: &W,
253    ) -> Result<Names<'_, 's, S>, Error<S::Error>> {
254        self.admit(work, self.directory)?;
255        let execution = self.session.query(work);
256        let mut lo = 0;
257        let mut hi = self.order.len();
258        while lo < hi {
259            let mid = lo + (hi - lo) / 2;
260            let other = self.entries[self.order[mid]].name.get(&self.names);
261            execution
262                .charge_work(other.len().min(name.len()) as u64 + 1)
263                .map_err(|cause| resource(self.directory, cause))?;
264            if other < name {
265                lo = mid + 1;
266            } else {
267                hi = mid;
268            }
269        }
270        let first = lo;
271        hi = self.order.len();
272        while lo < hi {
273            let mid = lo + (hi - lo) / 2;
274            let other = self.entries[self.order[mid]].name.get(&self.names);
275            execution
276                .charge_work(other.len().min(name.len()) as u64 + 1)
277                .map_err(|cause| resource(self.directory, cause))?;
278            if other <= name {
279                lo = mid + 1;
280            } else {
281                hi = mid;
282            }
283        }
284        Ok(Names {
285            archive: self,
286            first,
287            end: lo,
288        })
289    }
290
291    fn window<'q, W: Work>(
292        &'q self,
293        work: &'q W,
294        bounds: Span,
295    ) -> Result<Window<'q, S>, Error<S::Error>> {
296        Ok(Window::new(
297            self.source,
298            self.stamp,
299            bounds,
300            self.session.query(work),
301        )?)
302    }
303}
304
305/// Complete exact-name candidate range, independent of whether a member is usable.
306pub struct Names<'view, 'source, S: RangeSource> {
307    archive: &'view Archive<'source, S>,
308    first: usize,
309    end: usize,
310}
311impl<'v, 's, S: RangeSource> Names<'v, 's, S> {
312    /// Number of distinct original occurrences with this exact name.
313    #[must_use]
314    pub fn len(&self) -> usize {
315        self.end - self.first
316    }
317    /// Whether no exact original name was present.
318    #[must_use]
319    pub fn is_empty(&self) -> bool {
320        self.first == self.end
321    }
322    /// Retrieves one original candidate, never a normalized replacement.
323    pub fn member<W: Work>(
324        &self,
325        ordinal: usize,
326        work: &W,
327    ) -> Result<Option<Member<'v, 's, S>>, Error<S::Error>> {
328        self.archive.admit(work, self.archive.directory)?;
329        if ordinal >= self.len() {
330            return Ok(None);
331        }
332        Ok(Some(Member {
333            archive: self.archive,
334            ordinal: self.archive.order[self.first + ordinal],
335        }))
336    }
337}
338
339impl<S: RangeSource> Member<'_, '_, S> {
340    fn entry(&self) -> &Entry {
341        &self.archive.entries[self.ordinal]
342    }
343    /// Immutable identity of this original occurrence.
344    #[must_use]
345    pub fn id(&self) -> MemberId<S::Document, S::Generation> {
346        MemberId {
347            stamp: self.archive.stamp,
348            ordinal: self.ordinal,
349        }
350    }
351    /// Untouched original filename bytes, including invalid UTF-8.
352    #[must_use]
353    pub fn name(&self) -> &[u8] {
354        self.entry().name.get(&self.archive.names)
355    }
356    /// Original central-directory enclosure.
357    #[must_use]
358    pub fn central_span(&self) -> Span {
359        self.entry().central
360    }
361    /// Original physical filename enclosure.
362    #[must_use]
363    pub fn name_span(&self) -> Span {
364        self.entry().name_span
365    }
366    /// Original declared compression method.
367    #[must_use]
368    pub fn method(&self) -> u16 {
369        self.entry().method
370    }
371    /// Original ZIP general-purpose flags.
372    #[must_use]
373    pub fn flags(&self) -> u16 {
374        self.entry().flags
375    }
376    /// Original required extraction version.
377    #[must_use]
378    pub fn version_needed(&self) -> u16 {
379        self.entry().needed
380    }
381    /// Original compressed payload extent, if it can be bounded unambiguously.
382    #[must_use]
383    pub fn data_span(&self) -> Option<Span> {
384        self.entry().data
385    }
386    /// Original complete local record, including its actual descriptor.
387    #[must_use]
388    pub fn local_span(&self) -> Option<Span> {
389        self.entry().enclosure
390    }
391    /// Original local-header extra fields, independently of central extras.
392    #[must_use]
393    pub fn local_extra_span(&self) -> Option<Span> {
394        self.entry().local_extra
395    }
396    /// Declared decoded length. Successful acquisition independently checks it.
397    #[must_use]
398    pub fn decoded_length(&self) -> u64 {
399        self.entry().decoded
400    }
401}
402
403impl<'v, 's, S: RangeSource> Member<'v, 's, S> {
404    pub(crate) fn archive(&self) -> &'v Archive<'s, S> {
405        self.archive
406    }
407    pub(crate) fn session(&self) -> &'s Session {
408        self.archive.session
409    }
410}