Skip to main content

structured_asic/
package.rs

1//! Source-produced ODF manifest and OCF rootfile associations.
2//!
3//! Format inventory is independent of `XMLDSig`, member digests and trust. ODF
4//! full-path is a ZIP filename, not an URI to decode or normalize.
5
6use crate::{
7    error::Error as ArchiveError,
8    zip::{Member, MemberId, Names},
9};
10use structured_asn1::{
11    memory::{MemoryError, OwnedVec, Work},
12    range::RangeSource,
13};
14use structured_cms::stream::{AsyncSource, Receiver, Refused};
15use structured_xades::xml::{Access, AccessError, Name, Node, Snapshot, Xml};
16
17/// ODF package manifest namespace, distinct from digital signatures.
18pub const ODF: &str = "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0";
19
20/// The selected original package inventory vocabulary.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum Format {
23    /// ODF 1.2 manifest:file-entry and its exact ZIP filename.
24    Odf,
25    /// OCF container/rootfiles/rootfile and its full-path.
26    Ocf,
27}
28
29/// Every readable negative or explicitly unassessed original instruction.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum FaultKind {
32    /// Wrong original root, child or non-whitespace character content.
33    Structure,
34    /// Required original attribute or rootfiles/entry is absent.
35    Missing,
36    /// Selected schema's version is absent or has a different value.
37    Version,
38    /// An entry selects no original member or several exact-name occurrences.
39    Target,
40    /// The same original member or package root is declared repeatedly.
41    Duplicate,
42    /// ODF forbids declaring mimetype or the manifest itself.
43    Forbidden,
44    /// An original non-META-INF file has no unique ODF declaration.
45    Undeclared,
46    /// An original media-type value is not complete MIME syntax.
47    Media,
48    /// Encryption or extension content is retained but not assessed here.
49    Unassessed,
50}
51
52/// Located original package instruction or missing member obligation.
53#[derive(Debug, Clone, Copy)]
54pub struct Fault<N, D, G> {
55    /// Actual original XML location, or root for an undeclared member.
56    pub node: N,
57    /// Actual ZIP occurrence when the obligation identifies one.
58    pub member: Option<MemberId<D, G>>,
59    /// Independent completed negative/unassessed fact.
60    pub kind: FaultKind,
61}
62
63/// Original path projection without changing signed filename identity.
64pub enum Target<'v, 's, S: RangeSource> {
65    /// Required full-path was absent.
66    Absent,
67    /// ODF's exact "/" package declaration.
68    Package,
69    /// ODF directories need not have an explicit ZIP directory occurrence.
70    Directory(OwnedVec<u8>),
71    /// Every exact original candidate, including zero or multiple occurrences.
72    Members(Names<'v, 's, S>),
73}
74
75/// Every original declaration, not just successful unique members.
76pub struct Entry<'v, 's, N, S: RangeSource> {
77    node: N,
78    target: Target<'v, 's, S>,
79    media: Option<OwnedVec<u8>>,
80}
81impl<'v, 's, N: Copy, S: RangeSource> Entry<'v, 's, N, S> {
82    /// Actual original declaration location.
83    #[must_use]
84    pub fn node(&self) -> N {
85        self.node
86    }
87    /// Original path projection, without URI or Unicode normalization.
88    #[must_use]
89    pub fn target(&self) -> &Target<'v, 's, S> {
90        &self.target
91    }
92    /// XML-normalized original media-type, preserving absent versus empty.
93    #[must_use]
94    pub fn media_type(&self) -> Option<&[u8]> {
95        self.media.as_deref()
96    }
97}
98
99/// Terminal source, owner or resource cause, never a readable format fault.
100#[derive(Debug)]
101pub enum Error<E, N, SE> {
102    /// Original XML source/current operation refused.
103    Xml(AccessError<E, N>),
104    /// Original archive source/current operation refused.
105    Archive(ArchiveError<SE>),
106    /// Retained storage/lexical comparison refused before doing the work.
107    Resource {
108        /// Actual attempted original XML node.
109        node: N,
110        /// Original admission or allocation refusal.
111        cause: MemoryError,
112    },
113}
114
115/// Inventory derived only inside the actual member-producing XML scope.
116/// This does not attest complete ODF encryption/extension semantics or signatures.
117pub struct Reading<'v, 's, N, D, G, S: RangeSource> {
118    source: Member<'v, 's, S>,
119    snapshot: Snapshot<D, G>,
120    root: N,
121    format: Format,
122    entries: OwnedVec<Entry<'v, 's, N, S>>,
123    faults: OwnedVec<Fault<N, S::Document, S::Generation>>,
124}
125impl<'v, 's, N: Copy, D: Copy, G: Copy, S: RangeSource> Reading<'v, 's, N, D, G, S> {
126    /// Actual complete member from which this XML inventory was produced.
127    #[must_use]
128    pub fn source(&self) -> &Member<'v, 's, S> {
129        &self.source
130    }
131    /// Actual original XML source and generation.
132    #[must_use]
133    pub fn snapshot(&self) -> Snapshot<D, G> {
134        self.snapshot
135    }
136    /// Actual whole-document root, preserving its parent context.
137    #[must_use]
138    pub fn root(&self) -> N {
139        self.root
140    }
141    /// Explicit selected inventory vocabulary.
142    #[must_use]
143    pub fn format(&self) -> Format {
144        self.format
145    }
146    /// All original declarations in document order.
147    #[must_use]
148    pub fn entries(&self) -> &[Entry<'v, 's, N, S>] {
149        &self.entries
150    }
151    /// All original negative/unassessed declarations and missing obligations.
152    #[must_use]
153    pub fn faults(&self) -> &[Fault<N, S::Document, S::Generation>] {
154        &self.faults
155    }
156
157    /// ODF1.2 part3 §3.3 requires exact equality of the package root's media
158    /// attribute with the complete mimetype member. MIME essence equality is
159    /// insufficient. Duplicate roots/names remain unavailable, never selected.
160    ///
161    /// # Panics
162    /// An internal immutable name-catalogue invariant fails if its unique
163    /// candidate cannot be selected from that same catalogue.
164    pub async fn check_root_media<W: Work>(
165        &self,
166        work: &W,
167    ) -> Result<RootMedia, crate::zip::ReadFailure<S::Error, S::Document, S::Generation>> {
168        let archive = self.source.archive();
169        let execution = archive.session().query(work);
170        let failure = |cause| crate::zip::ReadFailure::Processing {
171            member: self.source.id(),
172            cause,
173        };
174        archive
175            .admit(&execution, self.source.central_span())
176            .map_err(failure)?;
177        if self.format != Format::Odf {
178            return Ok(RootMedia::NotApplicable);
179        }
180        let names = archive.find(b"mimetype", &execution).map_err(failure)?;
181        let mut root = None;
182        for entry in &self.entries {
183            execution.charge_work(1).map_err(|cause| {
184                failure(crate::error::resource(self.source.central_span(), cause))
185            })?;
186            if matches!(entry.target, Target::Package) {
187                if root.is_some() {
188                    return Ok(RootMedia::Unavailable);
189                }
190                root = Some(entry);
191            }
192        }
193        if names.is_empty() && root.is_none() {
194            return Ok(RootMedia::Absent);
195        }
196        if names.len() != 1 {
197            return Ok(RootMedia::Unavailable);
198        }
199        let Some(expected) = root.and_then(Entry::media_type) else {
200            return Ok(RootMedia::Unavailable);
201        };
202        let member = names
203            .member(0, &execution)
204            .map_err(failure)?
205            .expect("one exact original candidate");
206        let mut source = member.source(&execution)?;
207        let mut receiver = Compare {
208            expected,
209            at: 0,
210            differs: false,
211            work: &execution,
212            refused: None,
213        };
214        let result = source.stream(&mut receiver, &execution).await;
215        if let Some(cause) = receiver.refused {
216            return Err(failure(crate::error::resource(
217                member.central_span(),
218                cause,
219            )));
220        }
221        result?;
222        archive
223            .admit(&execution, self.source.central_span())
224            .map_err(failure)?;
225        Ok(if receiver.differs || receiver.at != expected.len() {
226            RootMedia::Differs
227        } else {
228            RootMedia::Matches
229        })
230    }
231}
232
233/// Exact package media association, independently of container signatures.
234#[derive(Debug, Clone, Copy, PartialEq, Eq)]
235pub enum RootMedia {
236    /// This selected vocabulary has no ODF root/mimetype equality rule.
237    NotApplicable,
238    /// Neither optional package declaration nor mimetype is present.
239    Absent,
240    /// Missing/ambiguous input or media attribute prevents a unique comparison.
241    Unavailable,
242    /// Complete original CRC-checked member equals the original media attribute.
243    Matches,
244    /// Completed exact bytes or lengths differ.
245    Differs,
246}
247
248struct Compare<'a, W: Work> {
249    expected: &'a [u8],
250    at: usize,
251    differs: bool,
252    work: &'a W,
253    refused: Option<MemoryError>,
254}
255impl<W: Work> Receiver for Compare<'_, W> {
256    fn write(&mut self, bytes: &[u8]) -> Result<(), Refused> {
257        if self.refused.is_some() {
258            return Err(Refused);
259        }
260        if let Err(cause) = self.work.charge_work(bytes.len() as u64) {
261            self.refused = Some(cause);
262            return Err(Refused);
263        }
264        if self.differs {
265            return Ok(());
266        }
267        if bytes.len() > self.expected.len() - self.at {
268            self.differs = true;
269            return Ok(());
270        }
271        let end = self.at + bytes.len();
272        if self.expected[self.at..end] != *bytes {
273            self.differs = true;
274        }
275        self.at = end;
276        Ok(())
277    }
278}
279
280type ResultOf<T, X, S> =
281    Result<T, Error<<X as Xml>::Error, <X as Xml>::Id, <S as RangeSource>::Error>>;
282
283/// Complete source-produced package reading or original processing refusal.
284pub type InspectionResult<'v, 's, X, S> = ResultOf<
285    Reading<'v, 's, <X as Xml>::Id, <X as Xml>::Document, <X as Xml>::Generation, S>,
286    X,
287    S,
288>;
289
290#[expect(
291    clippy::too_many_lines,
292    reason = "ODF and OCF share one ordered inventory walk and duplicate map; splitting the closure would obscure which exact source declarations discharge the aggregate census"
293)]
294pub(crate) fn inspect<'v, 's, X: Xml, S: RangeSource, W: Work>(
295    base: Member<'v, 's, S>,
296    access: &Access<'_, X, W>,
297    root: X::Id,
298    format: Format,
299) -> InspectionResult<'v, 's, X, S> {
300    let work = access.execution();
301    let archive = base.archive();
302    archive
303        .admit(work, base.central_span())
304        .map_err(Error::Archive)?;
305    let namespace = match format {
306        Format::Odf => ODF,
307        Format::Ocf => crate::xml::OCF,
308    };
309    let has_name = |node, local| {
310        access
311            .has_name(node, Name { namespace, local })
312            .map_err(Error::Xml)
313    };
314    let mut faults = work.vector();
315    let mut entries = work.vector();
316    let refusal = |cause| Error::Resource { node: root, cause };
317    let fault = |faults: &mut OwnedVec<_, _>, node, member, kind| {
318        faults
319            .try_push(Fault { node, member, kind })
320            .map_err(refusal)
321    };
322    let root_name = match format {
323        Format::Odf => "manifest",
324        Format::Ocf => "container",
325    };
326    if !has_name(root, root_name)? {
327        fault(&mut faults, root, None, FaultKind::Structure)?;
328    }
329    let version_namespace = if format == Format::Odf { ODF } else { "" };
330    let valid = access
331        .attribute(
332            root,
333            Name {
334                namespace: version_namespace,
335                local: "version",
336            },
337            |value| {
338                let Some(value) = value else {
339                    return Ok(false);
340                };
341                work.charge_work(value.len() as u64)?;
342                Ok(value.trim_matches([' ', '\t', '\r', '\n'])
343                    == if format == Format::Odf { "1.2" } else { "1.0" })
344            },
345        )
346        .map_err(Error::Xml)?
347        .map_err(refusal)?;
348    if !valid {
349        fault(&mut faults, root, None, FaultKind::Version)?;
350    }
351    let mut parents = work.vector();
352    if format == Format::Ocf {
353        let mut groups = 0usize;
354        walk::<X, S, W>(access, root, |node, element| {
355            if element && has_name(node, "rootfiles")? {
356                groups += 1;
357                parents.try_push(node).map_err(refusal)
358            } else {
359                fault(&mut faults, node, None, FaultKind::Structure)
360            }
361        })?;
362        if groups != 1 {
363            fault(
364                &mut faults,
365                root,
366                None,
367                if groups == 0 {
368                    FaultKind::Missing
369                } else {
370                    FaultKind::Duplicate
371                },
372            )?;
373        }
374    } else {
375        parents.try_push(root).map_err(refusal)?;
376    }
377    let mut declared = work.map();
378    let mut package = false;
379    for parent in parents {
380        walk::<X, S, W>(access, parent, |node, element| {
381            let entry_name = if format == Format::Odf {
382                "file-entry"
383            } else {
384                "rootfile"
385            };
386            if !element || !has_name(node, entry_name)? {
387                return fault(&mut faults, node, None, FaultKind::Structure);
388            }
389            let attr_namespace = if format == Format::Odf { ODF } else { "" };
390            // ODF1.2 part3 §4.8.4 defines a ZIP filename, not an IRI. In
391            // particular a percent sign is literal and must not be decoded.
392            // https://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part3.html
393            let target = access
394                .attribute(
395                    node,
396                    Name {
397                        namespace: attr_namespace,
398                        local: "full-path",
399                    },
400                    |value| {
401                        let Some(value) = value else {
402                            return Ok(Target::Absent);
403                        };
404                        work.charge_work(value.len() as u64)
405                            .map_err(|cause| Error::Resource { node, cause })?;
406                        if value == "/" && format == Format::Odf {
407                            return Ok(Target::Package);
408                        }
409                        if value.ends_with('/') && format == Format::Odf {
410                            let mut path = work.vector();
411                            path.try_extend_from_slice(value.as_bytes())
412                                .map_err(|cause| Error::Resource { node, cause })?;
413                            return Ok(Target::Directory(path.into_owned()));
414                        }
415                        archive
416                            .find(value.as_bytes(), work)
417                            .map(Target::Members)
418                            .map_err(Error::Archive)
419                    },
420                )
421                .map_err(Error::Xml)??;
422            match &target {
423                Target::Absent => fault(&mut faults, node, None, FaultKind::Missing)?,
424                Target::Package => {
425                    if package {
426                        fault(&mut faults, node, None, FaultKind::Duplicate)?;
427                    }
428                    package = true;
429                }
430                Target::Members(names) => {
431                    if names.len() != 1 {
432                        fault(&mut faults, node, None, FaultKind::Target)?;
433                    }
434                    for ordinal in 0..names.len() {
435                        let member = names
436                            .member(ordinal, work)
437                            .map_err(Error::Archive)?
438                            .expect("exact-name candidate count bounds its ordinal");
439                        if declared
440                            .try_insert(member.id().ordinal(), ())
441                            .map_err(refusal)?
442                            .is_some()
443                        {
444                            fault(&mut faults, node, Some(member.id()), FaultKind::Duplicate)?;
445                        }
446                        if format == Format::Odf
447                            && (member.name() == b"mimetype"
448                                || member.name() == b"META-INF/manifest.xml")
449                        {
450                            fault(&mut faults, node, Some(member.id()), FaultKind::Forbidden)?;
451                        }
452                    }
453                }
454                Target::Directory(_) => {}
455            }
456            let media = access
457                .attribute(
458                    node,
459                    Name {
460                        namespace: attr_namespace,
461                        local: "media-type",
462                    },
463                    |value| {
464                        let Some(value) = value else {
465                            return Ok(None);
466                        };
467                        let mut bytes = work.vector();
468                        bytes.try_extend_from_slice(value.as_bytes())?;
469                        Ok(Some(bytes.into_owned()))
470                    },
471                )
472                .map_err(Error::Xml)?
473                .map_err(refusal)?;
474            match &media {
475                None => fault(&mut faults, node, None, FaultKind::Missing)?,
476                Some(bytes) if !bytes.is_empty() => {
477                    let mut syntax = structured_cms::mime::Syntax::<true>::new();
478                    let text = core::str::from_utf8(bytes).expect("XML attribute bytes are UTF-8");
479                    syntax.feed(text, work).map_err(refusal)?;
480                    if !syntax.complete() {
481                        fault(&mut faults, node, None, FaultKind::Media)?;
482                    }
483                }
484                Some(_) => {}
485            }
486            walk::<X, S, W>(access, node, |child, element| {
487                fault(
488                    &mut faults,
489                    child,
490                    None,
491                    if element {
492                        FaultKind::Unassessed
493                    } else {
494                        FaultKind::Structure
495                    },
496                )
497            })?;
498            entries
499                .try_push(Entry {
500                    node,
501                    target,
502                    media,
503                })
504                .map_err(refusal)
505        })?;
506    }
507    if entries.is_empty() {
508        fault(&mut faults, root, None, FaultKind::Missing)?;
509    }
510    if format == Format::Odf {
511        // ODF1.2 part3 §3.2 excludes mimetype and META-INF, but every other
512        // original file requires exactly one declaration. ZIP directory entries
513        // represent folders rather than files and do not acquire that obligation.
514        for ordinal in 0..archive.len() {
515            let member = archive
516                .member(ordinal, work)
517                .map_err(Error::Archive)?
518                .expect("archive length bounds member ordinal");
519            let name = member.name();
520            work.charge_work(name.len() as u64).map_err(refusal)?;
521            if name == b"mimetype" {
522                if !package {
523                    fault(&mut faults, root, Some(member.id()), FaultKind::Missing)?;
524                }
525            } else if !name.starts_with(b"META-INF/")
526                && !name.ends_with(b"/")
527                && declared.get(&ordinal, work).map_err(refusal)?.is_none()
528            {
529                fault(&mut faults, root, Some(member.id()), FaultKind::Undeclared)?;
530            }
531        }
532    }
533    access.inspect(root, |_| ()).map_err(Error::Xml)?;
534    archive
535        .admit(work, base.central_span())
536        .map_err(Error::Archive)?;
537    Ok(Reading {
538        source: base,
539        snapshot: access.snapshot(),
540        root,
541        format,
542        entries: entries.into_owned(),
543        faults: faults.into_owned(),
544    })
545}
546
547fn walk<X: Xml, S: RangeSource, W: Work>(
548    access: &Access<'_, X, W>,
549    parent: X::Id,
550    mut visit: impl FnMut(X::Id, bool) -> ResultOf<(), X, S>,
551) -> ResultOf<(), X, S> {
552    let mut next = access.first_child(parent).map_err(Error::Xml)?;
553    while let Some(node) = next {
554        next = access.next_sibling(parent, node).map_err(Error::Xml)?;
555        let kind = access
556            .inspect(node, |value| match value {
557                Node::Element(_) => Ok(Some(true)),
558                Node::Other => Ok(None),
559                Node::Text(text) => {
560                    access.execution().charge_work(text.len() as u64)?;
561                    Ok(
562                        if text
563                            .bytes()
564                            .all(|byte| matches!(byte, b' ' | b'\t' | b'\r' | b'\n'))
565                        {
566                            None
567                        } else {
568                            Some(false)
569                        },
570                    )
571                }
572            })
573            .map_err(Error::Xml)?
574            .map_err(|cause| Error::Resource { node, cause })?;
575        if let Some(element) = kind {
576            visit(node, element)?;
577        }
578    }
579    Ok(())
580}