Skip to main content

structured_asic/
layout.rs

1//! Complete original member roles, without claiming cryptography from a name.
2
3use crate::{
4    error::{Error, resource},
5    zip::{Archive, Member, MemberId},
6};
7use structured_asn1::{
8    Span,
9    memory::{OwnedVec, Work},
10    range::RangeSource,
11};
12
13mod assess;
14pub use assess::{Form, Inspection, Issue, Principal, Requirement, Rules, Strength};
15
16/// Original filename role. A matching filename never proves readable content,
17/// schema validity, signed coverage, a signature result or a baseline level.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum Role {
20    /// An explicit ZIP directory occurrence, not an implied additional data file.
21    Directory,
22    /// The exact root-level `mimetype` member.
23    Mimetype,
24    /// A file outside the original root META-INF directory.
25    Data,
26    /// A potential `CAdES` file matching the normative naming pattern.
27    Cades,
28    /// A potential XML signature file, with its original parent context intact.
29    Xades,
30    /// A potential RFC3161 token file.
31    Timestamp,
32    /// A potential ASN.1 RFC4998 record file.
33    Evidence,
34    /// A named XMLERS record, retained even when its mechanism is unavailable.
35    XmlEvidence,
36    /// A potential ordinary `ASiCManifest`.
37    Manifest,
38    /// A potential `ASiCArchiveManifest`.
39    ArchiveManifest,
40    /// A potential `ASiCEvidenceRecordManifest`.
41    EvidenceManifest,
42    /// The original ODF package manifest.
43    OdfManifest,
44    /// The original OCF rootfile manifest.
45    OcfContainer,
46    /// Other application-specific metadata without reserved keywords.
47    Application,
48    /// Metadata with reserved naming that matches no supported format role.
49    Reserved,
50}
51
52/// Independent original mimetype-header facts. Content and media-type binding
53/// are checked against the actual acquired member, not these filenames.
54#[derive(Debug, Clone, Copy)]
55pub struct MimetypeHeader<D, G> {
56    /// Exact original occurrence; duplicate names produce separate records.
57    pub member: MemberId<D, G>,
58    /// Actual physical local-record enclosure, if independently bounded.
59    pub local: Option<Span>,
60    flags: u8,
61}
62impl<D, G> MimetypeHeader<D, G> {
63    /// It is physically first at original offset zero, regardless of CD order.
64    #[must_use]
65    pub fn first(&self) -> bool {
66        self.flags & 1 != 0
67    }
68    /// Original compression method is STORED.
69    #[must_use]
70    pub fn stored(&self) -> bool {
71        self.flags & 2 != 0
72    }
73    /// Original local header contains exactly zero extra bytes.
74    #[must_use]
75    pub fn no_extra(&self) -> bool {
76        self.flags & 4 != 0
77    }
78    /// Original flags do not request ZIP encryption.
79    #[must_use]
80    pub fn unencrypted(&self) -> bool {
81        self.flags & 8 != 0
82    }
83}
84
85/// One paid original occurrence and its unchanged processing cause.
86pub type OccurrenceResult<'v, 's, S> =
87    crate::error::SourceResult<Option<(Member<'v, 's, S>, Role)>, S>;
88type HeaderResult<S> = crate::error::SourceResult<
89    MimetypeHeader<<S as RangeSource>::Document, <S as RangeSource>::Generation>,
90    S,
91>;
92
93/// Retains every original member role in CD order, including duplicates,
94/// reserved names and unreadable members. One pooled name buffer remains in
95/// the archive; this catalogue stores only fixed-size classifications.
96pub struct Census<'v, 's, S: RangeSource> {
97    archive: &'v Archive<'s, S>,
98    roles: OwnedVec<Role>,
99    mimetypes: OwnedVec<MimetypeHeader<S::Document, S::Generation>>,
100}
101
102impl<'v, 's, S: RangeSource> Census<'v, 's, S> {
103    /// Performs a paid metadata pass under both the source and current query.
104    ///
105    /// # Panics
106    /// An internal catalogue invariant fails if a bounded original ordinal is absent.
107    pub fn read<W: Work>(archive: &'v Archive<'s, S>, work: &W) -> Result<Self, Error<S::Error>> {
108        archive.admit(work, archive.directory_span())?;
109        let execution = archive.session().query(work);
110        let mut roles = execution.vector();
111        roles
112            .try_reserve_exact(archive.len())
113            .map_err(|cause| resource(archive.directory_span(), cause))?;
114        let mut mimetypes = execution.vector();
115        for ordinal in 0..archive.len() {
116            let member = archive
117                .member(ordinal, work)?
118                .expect("bounded original ordinal");
119            let execution = member.session().query(work);
120            execution
121                .charge_work(member.name().len() as u64 * 64 + 1)
122                .map_err(|cause| resource(member.name_span(), cause))?;
123            let role = classify(member.name());
124            if role == Role::Mimetype {
125                // EN319162-1 V1.1.1 A.1: local, not central, extra fields
126                // determine the fixed physical MIME magic position.
127                // https://www.etsi.org/deliver/etsi_en/319100_319199/31916201/01.01.01_60/en_31916201v010101p.pdf
128                mimetypes
129                    .try_push(MimetypeHeader {
130                        member: member.id(),
131                        local: member.local_span(),
132                        flags: u8::from(member.local_span().is_some_and(|span| span.start == 0))
133                            | (u8::from(member.method() == 0) << 1)
134                            | (u8::from(
135                                member
136                                    .local_extra_span()
137                                    .is_some_and(|span| span.length == 0),
138                            ) << 2)
139                            | (u8::from(member.flags() & 0x2041 == 0) << 3),
140                    })
141                    .map_err(|cause| resource(member.central_span(), cause))?;
142            }
143            roles
144                .try_push(role)
145                .map_err(|cause| resource(member.central_span(), cause))?;
146        }
147        archive.admit(work, archive.directory_span())?;
148        Ok(Self {
149            archive,
150            roles: roles.into_owned(),
151            mimetypes: mimetypes.into_owned(),
152        })
153    }
154    /// The complete immutable ZIP occurrence census.
155    #[must_use]
156    pub fn archive(&self) -> &'v Archive<'s, S> {
157        self.archive
158    }
159    /// Number of original members, never the number of unique names.
160    #[must_use]
161    pub fn len(&self) -> usize {
162        self.roles.len()
163    }
164    /// Whether the original census is empty.
165    #[must_use]
166    pub fn is_empty(&self) -> bool {
167        self.roles.is_empty()
168    }
169    /// Selects one original occurrence and its paid role under this query.
170    pub fn occurrence<W: Work>(&self, ordinal: usize, work: &W) -> OccurrenceResult<'v, 's, S> {
171        let Some(member) = self.archive.member(ordinal, work)? else {
172            return Ok(None);
173        };
174        Ok(Some((member, self.roles[ordinal])))
175    }
176    /// Complete mimetype occurrence traversal. Failure fuses this query, and
177    /// an absent optional member never becomes a fabricated default media type.
178    pub fn mimetypes<'q, W: Work>(
179        &'q self,
180        work: &'q W,
181    ) -> impl Iterator<Item = HeaderResult<S>> + use<'q, 'v, 's, S, W> {
182        let mut at = 0;
183        let mut stopped = false;
184        core::iter::from_fn(move || {
185            if stopped || at == self.mimetypes.len() {
186                return None;
187            }
188            if let Err(cause) = self.archive.admit(work, self.archive.directory_span()) {
189                stopped = true;
190                return Some(Err(cause));
191            }
192            let value = self.mimetypes[at];
193            at += 1;
194            Some(Ok(value))
195        })
196    }
197}
198
199fn classify(name: &[u8]) -> Role {
200    if name.ends_with(b"/") {
201        return Role::Directory;
202    }
203    if name == b"mimetype" {
204        return Role::Mimetype;
205    }
206    let Some(relative) = name.strip_prefix(b"META-INF/") else {
207        return Role::Data;
208    };
209    if relative == b"manifest.xml" {
210        return Role::OdfManifest;
211    }
212    if relative == b"container.xml" {
213        return Role::OcfContainer;
214    }
215    if relative.ends_with(b".xml") {
216        // Annex A.7(2a) permits an arbitrary prefix when the previous latest
217        // manifest is renamed: META-INF/*ASiCArchiveManifest*.xml.
218        if contains(relative, b"ASiCArchiveManifest") {
219            return Role::ArchiveManifest;
220        }
221        if relative.starts_with(b"ASiCEvidenceRecordManifest") {
222            return Role::EvidenceManifest;
223        }
224        if relative.starts_with(b"ASiCManifest") {
225            return Role::Manifest;
226        }
227        if contains(relative, b"evidencerecord") {
228            return Role::XmlEvidence;
229        }
230        if contains(relative, b"signatures") {
231            return Role::Xades;
232        }
233    }
234    if relative.ends_with(b".p7s") && contains(relative, b"signature") {
235        return Role::Cades;
236    }
237    if relative.ends_with(b".tst") && contains(relative, b"timestamp") {
238        return Role::Timestamp;
239    }
240    if relative.ends_with(b".ers") && contains(relative, b"evidencerecord") {
241        return Role::Evidence;
242    }
243    // EN319162-1 ยง4.4.3.2(5d) alone uses case-insensitive reserved words.
244    // This rule must not case-fold actual member or signed reference identity.
245    for word in [
246        b"signature".as_slice(),
247        b"timestamp",
248        b"manifest",
249        b"container.xml",
250    ] {
251        if relative
252            .windows(word.len())
253            .any(|part| part.eq_ignore_ascii_case(word))
254        {
255            return Role::Reserved;
256        }
257    }
258    Role::Application
259}
260
261fn contains(name: &[u8], word: &[u8]) -> bool {
262    name.windows(word.len()).any(|part| part == word)
263}