Skip to main content

structured_asic/layout/
assess.rs

1//! File-layout requirements, separate from content, coverage and attained level.
2
3use super::{Census, Role};
4use crate::{
5    error::{Error, resource},
6    zip::{FaultKind, MemberId},
7};
8use structured_asn1::{
9    Span,
10    memory::{OwnedVec, Work},
11    range::RangeSource,
12};
13
14/// One principal file format for a simple container.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum Principal {
17    /// The original `META-INF/signature.p7s` file.
18    Cades,
19    /// The original `META-INF/signatures.xml` file.
20    Xades,
21    /// The original `META-INF/timestamp.tst` file.
22    Timestamp,
23    /// The original `META-INF/evidencerecord.ers` file.
24    Evidence,
25    /// XMLERS is retained as a distinct format, without an implemented engine.
26    XmlEvidence,
27}
28impl Principal {
29    fn name(self) -> &'static [u8] {
30        match self {
31            Self::Cades => b"META-INF/signature.p7s",
32            Self::Xades => b"META-INF/signatures.xml",
33            Self::Timestamp => b"META-INF/timestamp.tst",
34            Self::Evidence => b"META-INF/evidencerecord.ers",
35            Self::XmlEvidence => b"META-INF/evidencerecord.xml",
36        }
37    }
38}
39
40/// Explicit format being assessed, never inferred as an acceptance policy.
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum Form {
43    /// One original root data file and one principal file.
44    Simple(Principal),
45    /// Extended container using XML signature files and optional evidence records.
46    ExtendedXades,
47    /// Extended container using `CAdES` signatures and/or time assertions.
48    ExtendedCadesAndAssertions,
49}
50
51/// Normative rule set. Signature levels remain separate common stage reports.
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum Rules {
54    /// EN319162-1 clause4 syntax and layout.
55    General,
56    /// EN319162-1 clause5, only its defined S-signature/E-XAdES routes.
57    BaselinePart1,
58    /// EN319162-2 additional-container requirements, composed with clause4.
59    AdditionalPart2,
60}
61
62/// A recommendation is preserved without turning SHOULD into SHALL.
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum Strength {
65    /// A required layout condition is not satisfied.
66    Required,
67    /// A normative recommended encoding condition is not satisfied.
68    Recommended,
69}
70
71/// Located independent layout requirement, not a mathematical/trust outcome.
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum Requirement {
74    /// The archive's original ZIP/UTF-8 fact violates the applicable encoding.
75    Zip(FaultKind),
76    /// Simple data cardinality is not one, or extended data cardinality is zero.
77    DataCount,
78    /// A simple data file is not an original root-level file.
79    SimpleDataPath,
80    /// The original principal-file cardinality does not satisfy the form.
81    PrincipalCount,
82    /// Another principal signature/time-assertion family occurs in the layout.
83    PrincipalFamily,
84    /// An optional mimetype has several original occurrences.
85    MimetypeCount,
86    /// Actual physical mimetype framing violates Annex A.1.
87    MimetypeHeader,
88    /// Baseline S contains an additional original file/directory.
89    BaselineSimpleExtra,
90    /// Baseline E-XAdES lacks exactly one original META-INF/manifest.xml.
91    BaselineOdfManifest,
92    /// Application metadata uses a reserved name outside a defined role.
93    ReservedName,
94}
95
96/// One original constraint failure. None denotes a container cardinality.
97#[derive(Debug, Clone, Copy)]
98pub struct Issue<D, G> {
99    /// Exact original member, where the requirement concerns one occurrence.
100    pub member: Option<MemberId<D, G>>,
101    /// Actual physical original enclosure.
102    pub range: Span,
103    /// Independent layout requirement.
104    pub requirement: Requirement,
105    /// Normative requirement versus recommendation.
106    pub strength: Strength,
107}
108
109/// Complete paid layout pass. This does not attest XML schema, actual signed
110/// coverage, MIME association, archive continuity or any signature's level.
111pub struct Inspection<'c, 'v, 's, S: RangeSource> {
112    census: &'c Census<'v, 's, S>,
113    form: Form,
114    rules: Rules,
115    defined: bool,
116    required: bool,
117    issues: OwnedVec<Issue<S::Document, S::Generation>>,
118}
119
120struct Issues<D, G, W: Work> {
121    required: bool,
122    values: OwnedVec<Issue<D, G>, W>,
123}
124impl<D, G, W: Work> Issues<D, G, W> {
125    fn push<E>(
126        &mut self,
127        member: Option<MemberId<D, G>>,
128        range: Span,
129        requirement: Requirement,
130        strength: Strength,
131    ) -> Result<(), Error<E>> {
132        self.values
133            .try_push(Issue {
134                member,
135                range,
136                requirement,
137                strength,
138            })
139            .map_err(|cause| resource(range, cause))?;
140        if strength == Strength::Required {
141            self.required = false;
142        }
143        Ok(())
144    }
145}
146
147impl<'c, 'v, 's, S: RangeSource> Inspection<'c, 'v, 's, S> {
148    /// Checks every original occurrence. Resource/source refusal is terminal;
149    /// a readable negative member never becomes a successful partial census.
150    ///
151    /// # Panics
152    /// An internal immutable census invariant fails if its bounded ordinal or
153    /// retained fault ordinal is absent from the same original archive.
154    #[expect(
155        clippy::too_many_lines,
156        reason = "the complete layout rule table is evaluated in one paid pass, preserving independent per-occurrence and aggregate findings"
157    )]
158    pub fn read<W: Work>(
159        census: &'c Census<'v, 's, S>,
160        form: Form,
161        rules: Rules,
162        work: &W,
163    ) -> Result<Self, Error<S::Error>> {
164        let archive = census.archive;
165        let execution = archive.session().query(work);
166        archive.admit(&execution, archive.directory_span())?;
167        let defined = match rules {
168            Rules::General => true,
169            Rules::BaselinePart1 => matches!(
170                form,
171                Form::Simple(Principal::Cades | Principal::Xades) | Form::ExtendedXades
172            ),
173            Rules::AdditionalPart2 => matches!(
174                form,
175                Form::Simple(Principal::Timestamp | Principal::Evidence | Principal::XmlEvidence)
176                    | Form::ExtendedCadesAndAssertions
177            ),
178        };
179        // EN319162-2 §4 incorporates Part1 clause4, not the additional ISO
180        // restriction in Part1 §5.3.1. Additional is not a baseline alias.
181        let baseline = rules == Rules::BaselinePart1 && defined;
182        let mut result = Issues {
183            required: true,
184            values: execution.vector(),
185        };
186        let mut data = 0;
187        let mut principal = 0;
188        let mut mimetypes = 0;
189        let mut odf_manifests = 0;
190        for ordinal in 0..census.len() {
191            let (member, role) = census
192                .occurrence(ordinal, &execution)?
193                .expect("bounded original ordinal");
194            let name = member.name();
195            execution
196                .charge_work(name.len() as u64 + 1)
197                .map_err(|cause| resource(member.name_span(), cause))?;
198            match role {
199                Role::Data => {
200                    data += 1;
201                    if matches!(form, Form::Simple(_)) && name.contains(&b'/') {
202                        result.push(
203                            Some(member.id()),
204                            member.name_span(),
205                            Requirement::SimpleDataPath,
206                            Strength::Required,
207                        )?;
208                    }
209                }
210                Role::Mimetype => mimetypes += 1,
211                Role::OdfManifest => odf_manifests += 1,
212                _ => {}
213            }
214            match form {
215                Form::Simple(selected) => {
216                    if name == selected.name() {
217                        principal += 1;
218                    } else if matches!(
219                        name,
220                        b"META-INF/signature.p7s"
221                            | b"META-INF/signatures.xml"
222                            | b"META-INF/timestamp.tst"
223                            | b"META-INF/evidencerecord.ers"
224                            | b"META-INF/evidencerecord.xml"
225                    ) {
226                        result.push(
227                            Some(member.id()),
228                            member.name_span(),
229                            Requirement::PrincipalFamily,
230                            Strength::Required,
231                        )?;
232                    }
233                    // EN319162-1 §§5.3.2.2/5.3.2.3: the baseline is stricter
234                    // than §4.3.3.2(5), which permits general auxiliary metadata.
235                    if baseline
236                        && name != selected.name()
237                        && role != Role::Data
238                        && role != Role::Mimetype
239                        && name != b"META-INF/"
240                    {
241                        result.push(
242                            Some(member.id()),
243                            member.name_span(),
244                            Requirement::BaselineSimpleExtra,
245                            Strength::Required,
246                        )?;
247                    }
248                }
249                Form::ExtendedXades => {
250                    if role == Role::Xades {
251                        principal += 1;
252                    } else if matches!(role, Role::Cades | Role::Timestamp) {
253                        result.push(
254                            Some(member.id()),
255                            member.name_span(),
256                            Requirement::PrincipalFamily,
257                            Strength::Required,
258                        )?;
259                    }
260                }
261                Form::ExtendedCadesAndAssertions => {
262                    if matches!(
263                        role,
264                        Role::Cades | Role::Timestamp | Role::Evidence | Role::XmlEvidence
265                    ) {
266                        principal += 1;
267                    } else if role == Role::Xades {
268                        result.push(
269                            Some(member.id()),
270                            member.name_span(),
271                            Requirement::PrincipalFamily,
272                            Strength::Required,
273                        )?;
274                    }
275                }
276            }
277            if !matches!(form, Form::Simple(_)) && role == Role::Reserved {
278                result.push(
279                    Some(member.id()),
280                    member.name_span(),
281                    Requirement::ReservedName,
282                    Strength::Required,
283                )?;
284            }
285        }
286        if (matches!(form, Form::Simple(_)) && data != 1) || data == 0 {
287            result.push(
288                None,
289                archive.directory_span(),
290                Requirement::DataCount,
291                Strength::Required,
292            )?;
293        }
294        if (matches!(form, Form::Simple(_)) && principal != 1) || principal == 0 {
295            result.push(
296                None,
297                archive.directory_span(),
298                Requirement::PrincipalCount,
299                Strength::Required,
300            )?;
301        }
302        if mimetypes > 1 {
303            result.push(
304                None,
305                archive.directory_span(),
306                Requirement::MimetypeCount,
307                Strength::Required,
308            )?;
309        }
310        if baseline && form == Form::ExtendedXades && odf_manifests != 1 {
311            result.push(
312                None,
313                archive.directory_span(),
314                Requirement::BaselineOdfManifest,
315                Strength::Required,
316            )?;
317        }
318        for header in census.mimetypes(&execution) {
319            let header = header?;
320            if !header.first() || !header.stored() || !header.no_extra() || !header.unencrypted() {
321                let member = archive.original(header.member, &execution)?;
322                result.push(
323                    Some(header.member),
324                    member.central_span(),
325                    Requirement::MimetypeHeader,
326                    Strength::Required,
327                )?;
328            }
329        }
330        for fault in archive.faults(&execution)? {
331            let fault = fault?;
332            let strength = match fault.kind {
333                FaultKind::Utf8
334                | FaultKind::Name
335                | FaultKind::LocalMismatch
336                | FaultKind::Descriptor
337                | FaultKind::Overlap
338                | FaultKind::LocalBounds
339                | FaultKind::StoredLength
340                | FaultKind::Zip64 => Some(Strength::Required),
341                FaultKind::Compression => Some(if baseline {
342                    Strength::Required
343                } else {
344                    Strength::Recommended
345                }),
346                FaultKind::IsoProfile | FaultKind::DirectorySignature | FaultKind::Encrypted
347                    if baseline =>
348                {
349                    Some(Strength::Required)
350                }
351                _ => None,
352            };
353            if let Some(strength) = strength {
354                let member = if let Some(ordinal) = fault.member {
355                    Some(
356                        archive
357                            .member(ordinal, &execution)?
358                            .expect("original fault ordinal")
359                            .id(),
360                    )
361                } else {
362                    None
363                };
364                result.push(member, fault.range, Requirement::Zip(fault.kind), strength)?;
365            }
366        }
367        archive.admit(&execution, archive.directory_span())?;
368        Ok(Self {
369            census,
370            form,
371            rules,
372            defined,
373            required: result.required,
374            issues: result.values.into_owned(),
375        })
376    }
377
378    /// Exact complete original role catalogue used by this pass.
379    #[must_use]
380    pub fn census(&self) -> &'c Census<'v, 's, S> {
381        self.census
382    }
383    /// Explicit requested form.
384    #[must_use]
385    pub fn form(&self) -> Form {
386        self.form
387    }
388    /// Explicit requested normative rule set.
389    #[must_use]
390    pub fn rules(&self) -> Rules {
391        self.rules
392    }
393    /// Whether this rule set defines the requested format route.
394    #[must_use]
395    pub fn defined(&self) -> bool {
396        self.defined
397    }
398    /// Only file layout, with recommendations independent. None means the
399    /// selected standard does not define this route, not a successful baseline.
400    #[must_use]
401    pub fn required_layout(&self) -> Option<bool> {
402        if self.defined {
403            Some(self.required)
404        } else {
405            None
406        }
407    }
408    /// Full paid issue traversal, with first resource refusal followed by None.
409    pub fn issues<'q, W: Work>(
410        &'q self,
411        work: &'q W,
412    ) -> impl Iterator<Item = IssueResult<S>> + use<'q, 'c, 'v, 's, S, W> {
413        let mut at = 0;
414        let mut stopped = false;
415        core::iter::from_fn(move || {
416            if stopped || at == self.issues.len() {
417                return None;
418            }
419            if let Err(cause) = self
420                .census
421                .archive
422                .admit(work, self.census.archive.directory_span())
423            {
424                stopped = true;
425                return Some(Err(cause));
426            }
427            let issue = self.issues[at];
428            at += 1;
429            Some(Ok(issue))
430        })
431    }
432}
433
434type IssueResult<S> = crate::error::SourceResult<
435    Issue<<S as RangeSource>::Document, <S as RangeSource>::Generation>,
436    S,
437>;