Skip to main content

structured_asic/
archive.rs

1//! Annex A.7 source-linked archive-manifest coverage and renewal graph.
2
3use crate::{
4    error::{Error as ArchiveError, resource},
5    layout::{Census, Role},
6    manifest::Boolean,
7    xml::producer::Facts,
8    zip::{Archive, MemberId},
9};
10use structured_asn1::{
11    memory::{OwnedMap, OwnedVec, Work},
12    range::RangeSource,
13};
14
15mod check;
16mod inputs;
17mod preservation;
18pub use check::{Mathematics, ObjectsState, TimestampCheck, TimestampState};
19pub use inputs::{AssertionInput, AssertionScope, InputAssociation, TimestampInputs};
20pub use preservation::{ActorCheck, ActorResult, CoverageResult, RequirementsResult};
21
22/// Readable source/coverage problems retain every original occurrence.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum FindingKind {
25    /// The fixed latest filename is absent or ambiguous; no older fallback.
26    Latest,
27    /// This original archive manifest has no completed producing inspection.
28    Uninspected,
29    /// Original schema inspection has negative or unassessed findings.
30    Syntax,
31    /// The one signature reference does not select an actual timestamp member.
32    Timestamp,
33    /// An object URI is missing, malformed, fragmented or ambiguous.
34    Target,
35    /// Rootfile does not uniquely identify the immediately preceding manifest.
36    Rootfile,
37    /// The original predecessor sequence cycles.
38    Cycle,
39    /// An original archive-manifest occurrence was omitted from the sequence.
40    Unreachable,
41    /// A later manifest omits a prior object, manifest or timestamp.
42    Dropped,
43}
44
45/// Original archive occurrence and optional exact instruction/related object.
46#[derive(Debug, Clone, Copy)]
47pub struct Finding<D, G> {
48    /// Owning original archive manifest, absent only for latest-name selection.
49    pub manifest: Option<MemberId<D, G>>,
50    /// Original `DataObjectReference` ordinal, where applicable.
51    pub reference: Option<usize>,
52    /// Original missing/dropped/cyclic source occurrence.
53    pub related: Option<MemberId<D, G>>,
54    /// Completed structural result, never application trust.
55    pub kind: FindingKind,
56}
57
58/// Caller/processing errors cannot become readable archive conformance faults.
59#[derive(Debug)]
60pub enum Error<E, D, G> {
61    /// Actual source/current processing refused.
62    Archive(ArchiveError<E>),
63    /// More than one producing reading was supplied for one original occurrence.
64    DuplicateFacts(MemberId<D, G>),
65    /// A supplied reading belongs to another archive or a non-manifest member.
66    ForeignFacts(MemberId<D, G>),
67    /// Two actual timestamp receipts were supplied for the same original input.
68    DuplicateTimestamp(MemberId<D, G>),
69}
70type ResultOf<T, S> = Result<
71    T,
72    Error<<S as RangeSource>::Error, <S as RangeSource>::Document, <S as RangeSource>::Generation>,
73>;
74
75/// One original occurrence, including uninspected/unreadable source positions.
76pub struct Node<D, G> {
77    member: MemberId<D, G>,
78    facts: Option<usize>,
79    previous: Option<usize>,
80    timestamp: Option<MemberId<D, G>>,
81    objects: Option<OwnedVec<Option<MemberId<D, G>>>>,
82}
83impl<D: Copy, G: Copy> Node<D, G> {
84    /// Original manifest member identity, not its current filename spelling.
85    #[must_use]
86    pub fn member(&self) -> MemberId<D, G> {
87        self.member
88    }
89    /// Original producing-facts index; None preserves a missing inspection.
90    #[must_use]
91    pub fn facts(&self) -> Option<usize> {
92        self.facts
93    }
94    /// Exact predecessor node index, never CD/physical/success order.
95    #[must_use]
96    pub fn previous(&self) -> Option<usize> {
97        self.previous
98    }
99    /// Exact original timestamp occurrence if the singleton URI resolved.
100    #[must_use]
101    pub fn timestamp(&self) -> Option<MemberId<D, G>> {
102        self.timestamp
103    }
104    /// Every original object instruction, including unresolved positions.
105    #[must_use]
106    pub fn objects(&self) -> &[Option<MemberId<D, G>>] {
107        match &self.objects {
108            Some(objects) => objects,
109            None => &[],
110        }
111    }
112}
113
114/// Full original A.7 graph. Positive geometry is independent of timestamp
115/// mathematics, validation material, historical trust and algorithm longevity.
116/// There is no fallback from an unreadable/bad latest to an earlier success.
117pub struct Graph<'v, 's, N, D, G, S: RangeSource> {
118    archive: &'v Archive<'s, S>,
119    facts: OwnedVec<Facts<'v, 's, N, D, G, S>>,
120    nodes: OwnedVec<Node<S::Document, S::Generation>>,
121    order: OwnedVec<usize>,
122    findings: OwnedVec<Finding<S::Document, S::Generation>>,
123    latest: Option<usize>,
124    covered: OwnedMap<usize, ()>,
125}
126impl<'v, 's, N: Copy, D: Copy + Eq, G: Copy + Eq, S: RangeSource> Graph<'v, 's, N, D, G, S> {
127    /// Derives the complete sequence from original Rootfile links. A.7(1–2)
128    /// requires each renewal to retain prior objects AND prior manifests/TSTs;
129    /// renamed manifests may have arbitrary prefixes. Input order is irrelevant.
130    /// <https://www.etsi.org/deliver/etsi_en/319100_319199/31916201/01.01.01_60/en_31916201v010101p.pdf>
131    ///
132    /// # Panics
133    /// An internal immutable catalogue invariant fails if a bounded census or
134    /// retained original-member ordinal cannot be selected from its catalogue.
135    #[expect(
136        clippy::too_many_lines,
137        reason = "one transactional graph pass keeps source admission, predecessor cycle detection and cumulative prior coverage in their required order"
138    )]
139    pub fn inspect<W: Work>(
140        census: &Census<'v, 's, S>,
141        facts: OwnedVec<Facts<'v, 's, N, D, G, S>>,
142        work: &W,
143    ) -> ResultOf<Self, S> {
144        const LATEST: &[u8] = b"META-INF/ASiCArchiveManifest.xml";
145        let archive = census.archive();
146        archive
147            .admit(work, archive.directory_span())
148            .map_err(Error::Archive)?;
149        let execution = archive.session().query(work);
150        let refusal = |cause| Error::Archive(resource(archive.directory_span(), cause));
151        // Reject foreign sources before allocating the graph or invoking a
152        // deferred target lookup. Independently retained storage is moved.
153        for fact in &facts {
154            execution.charge_work(1).map_err(refusal)?;
155            if !core::ptr::eq(fact.catalogue().base().archive(), archive) {
156                return Err(Error::ForeignFacts(fact.catalogue().base().id()));
157            }
158        }
159        let mut by_source = execution.map();
160        for (index, fact) in facts.iter().enumerate() {
161            let id = fact.catalogue().base().id();
162            if !matches!(
163                census
164                    .occurrence(id.ordinal(), &execution)
165                    .map_err(Error::Archive)?
166                    .map(|(_, role)| role),
167                Some(Role::ArchiveManifest | Role::Manifest)
168            ) {
169                return Err(Error::ForeignFacts(id));
170            }
171            if by_source
172                .try_insert(id.ordinal(), index)
173                .map_err(refusal)?
174                .is_some()
175            {
176                return Err(Error::DuplicateFacts(id));
177            }
178        }
179        let mut nodes = execution.vector();
180        let mut by_member = execution.map();
181        let mut findings = execution.vector();
182        let mut latest = None;
183        let mut latest_count = 0usize;
184        for ordinal in 0..census.len() {
185            let (member, role) = census
186                .occurrence(ordinal, &execution)
187                .map_err(Error::Archive)?
188                .expect("census length bounds its original ordinal");
189            if role != Role::ArchiveManifest {
190                continue;
191            }
192            let index = nodes.len();
193            let id = member.id();
194            execution.charge_work(1).map_err(refusal)?;
195            if member.name().len() == LATEST.len() {
196                execution
197                    .charge_work(LATEST.len() as u64)
198                    .map_err(refusal)?;
199                if member.name() == LATEST {
200                    latest_count += 1;
201                    latest = Some(index);
202                }
203            }
204            let fact = by_source
205                .get(&ordinal, &execution)
206                .map_err(refusal)?
207                .copied();
208            if fact.is_none() {
209                findings
210                    .try_push(Finding {
211                        manifest: Some(id),
212                        reference: None,
213                        related: None,
214                        kind: FindingKind::Uninspected,
215                    })
216                    .map_err(refusal)?;
217            }
218            nodes
219                .try_push(Node {
220                    member: id,
221                    facts: fact,
222                    previous: None,
223                    timestamp: None,
224                    objects: None,
225                })
226                .map_err(refusal)?;
227            by_member.try_insert(ordinal, index).map_err(refusal)?;
228        }
229        if latest_count != 1 {
230            latest = None;
231            findings
232                .try_push(Finding {
233                    manifest: None,
234                    reference: None,
235                    related: None,
236                    kind: FindingKind::Latest,
237                })
238                .map_err(refusal)?;
239        }
240        for node in &mut nodes {
241            let Some(index) = node.facts else {
242                continue;
243            };
244            let catalogue = facts[index].catalogue();
245            let id = node.member;
246            if !catalogue.reading().faults().is_empty() {
247                findings
248                    .try_push(Finding {
249                        manifest: Some(id),
250                        reference: None,
251                        related: None,
252                        kind: FindingKind::Syntax,
253                    })
254                    .map_err(refusal)?;
255            }
256            if let Some(token) = catalogue
257                .signature_member(&execution)
258                .map_err(Error::Archive)?
259                && census
260                    .occurrence(token.id().ordinal(), &execution)
261                    .map_err(Error::Archive)?
262                    .map(|(_, role)| role)
263                    == Some(Role::Timestamp)
264            {
265                node.timestamp = Some(token.id());
266            }
267            if node.timestamp.is_none() {
268                findings
269                    .try_push(Finding {
270                        manifest: Some(id),
271                        reference: None,
272                        related: None,
273                        kind: FindingKind::Timestamp,
274                    })
275                    .map_err(refusal)?;
276            }
277            let mut objects = execution.vector();
278            objects
279                .try_reserve_exact(catalogue.objects().len())
280                .map_err(refusal)?;
281            let mut roots = 0usize;
282            for (ordinal, object) in catalogue.reading().objects().iter().enumerate() {
283                let target = catalogue
284                    .object_member(ordinal, &execution)
285                    .map_err(Error::Archive)?
286                    .map(|member| member.id());
287                objects.try_push(target).map_err(refusal)?;
288                if target.is_none() {
289                    findings
290                        .try_push(Finding {
291                            manifest: Some(id),
292                            reference: Some(ordinal),
293                            related: None,
294                            kind: FindingKind::Target,
295                        })
296                        .map_err(refusal)?;
297                }
298                match object.rootfile {
299                    Boolean::Value(true) => {
300                        roots += 1;
301                        let predecessor = match target {
302                            Some(target) => by_member
303                                .get(&target.ordinal(), &execution)
304                                .map_err(refusal)?
305                                .copied(),
306                            None => None,
307                        };
308                        if predecessor.is_none() {
309                            findings
310                                .try_push(Finding {
311                                    manifest: Some(id),
312                                    reference: Some(ordinal),
313                                    related: target,
314                                    kind: FindingKind::Rootfile,
315                                })
316                                .map_err(refusal)?;
317                        }
318                        node.previous = predecessor;
319                    }
320                    Boolean::Malformed => {
321                        findings
322                            .try_push(Finding {
323                                manifest: Some(id),
324                                reference: Some(ordinal),
325                                related: target,
326                                kind: FindingKind::Rootfile,
327                            })
328                            .map_err(refusal)?;
329                    }
330                    Boolean::Absent | Boolean::Value(false) => {}
331                }
332            }
333            if roots > 1 {
334                node.previous = None;
335                findings
336                    .try_push(Finding {
337                        manifest: Some(id),
338                        reference: None,
339                        related: None,
340                        kind: FindingKind::Rootfile,
341                    })
342                    .map_err(refusal)?;
343            }
344            node.objects = Some(objects.into_owned());
345        }
346        let mut order = execution.vector();
347        let mut visited = execution.map();
348        let mut current = latest;
349        while let Some(index) = current {
350            execution.charge_work(1).map_err(refusal)?;
351            if visited.try_insert(index, ()).map_err(refusal)?.is_some() {
352                findings
353                    .try_push(Finding {
354                        manifest: Some(nodes[index].member),
355                        reference: None,
356                        related: Some(nodes[index].member),
357                        kind: FindingKind::Cycle,
358                    })
359                    .map_err(refusal)?;
360                break;
361            }
362            order.try_push(index).map_err(refusal)?;
363            current = nodes[index].previous;
364        }
365        for (index, node) in nodes.iter().enumerate() {
366            if visited.get(&index, &execution).map_err(refusal)?.is_none() {
367                findings
368                    .try_push(Finding {
369                        manifest: Some(node.member),
370                        reference: None,
371                        related: None,
372                        kind: FindingKind::Unreachable,
373                    })
374                    .map_err(refusal)?;
375            }
376        }
377        execution.charge_work(order.len() as u64).map_err(refusal)?;
378        order.reverse();
379        let mut required = execution.map();
380        let mut covered = execution.map();
381        for index in &order {
382            let node = &nodes[*index];
383            let mut actual = execution.map();
384            for target in node.objects().iter().flatten() {
385                actual.try_insert(target.ordinal(), ()).map_err(refusal)?;
386            }
387            for (ordinal, ()) in required.try_iter(&execution).map_err(refusal)? {
388                if actual.get(ordinal, &execution).map_err(refusal)?.is_none() {
389                    let related = archive
390                        .member(*ordinal, &execution)
391                        .map_err(Error::Archive)?
392                        .expect("required set only contains admitted original members")
393                        .id();
394                    findings
395                        .try_push(Finding {
396                            manifest: Some(node.member),
397                            reference: None,
398                            related: Some(related),
399                            kind: FindingKind::Dropped,
400                        })
401                        .map_err(refusal)?;
402                }
403            }
404            for target in node.objects().iter().flatten() {
405                required.try_insert(target.ordinal(), ()).map_err(refusal)?;
406            }
407            required
408                .try_insert(node.member.ordinal(), ())
409                .map_err(refusal)?;
410            if let Some(token) = node.timestamp {
411                required.try_insert(token.ordinal(), ()).map_err(refusal)?;
412            }
413            covered = actual;
414        }
415        archive
416            .admit(&execution, archive.directory_span())
417            .map_err(Error::Archive)?;
418        Ok(Self {
419            archive,
420            facts,
421            nodes: nodes.into_owned(),
422            order: order.into_owned(),
423            findings: findings.into_owned(),
424            latest,
425            covered: covered.into_owned(),
426        })
427    }
428    /// All original source-derived readings, moved without copying catalogues.
429    #[must_use]
430    pub fn facts(&self) -> &[Facts<'v, 's, N, D, G, S>] {
431        &self.facts
432    }
433    /// Every original archive-manifest occurrence, including uninspected slots.
434    #[must_use]
435    pub fn nodes(&self) -> &[Node<S::Document, S::Generation>] {
436        &self.nodes
437    }
438    /// Oldest-to-latest linked node indices. Findings expose any partial graph.
439    #[must_use]
440    pub fn order(&self) -> &[usize] {
441        &self.order
442    }
443    /// Every original negative geometry/coverage outcome.
444    #[must_use]
445    pub fn findings(&self) -> &[Finding<S::Document, S::Generation>] {
446        &self.findings
447    }
448    /// Exact fixed latest occurrence, never an earlier-success fallback.
449    #[must_use]
450    pub fn latest(&self) -> Option<usize> {
451        self.latest
452    }
453    /// Complete source/layout geometry only, NOT mathematics/material/trust.
454    #[must_use]
455    pub fn geometry_complete(&self) -> bool {
456        self.latest.is_some() && self.findings.is_empty()
457    }
458    /// Whether the latest original manifest declares every requested exact
459    /// member. Requested long-term objects are caller input, not inferred from
460    /// files introduced after an earlier timestamp. This does not check hashes.
461    pub fn declares_all<W: Work>(
462        &self,
463        requested: &[MemberId<S::Document, S::Generation>],
464        work: &W,
465    ) -> Result<bool, ArchiveError<S::Error>> {
466        let execution = self.archive.session().query(work);
467        self.archive
468            .admit(&execution, self.archive.directory_span())?;
469        for id in requested {
470            self.archive.original(*id, &execution)?;
471            if self
472                .covered
473                .get(&id.ordinal(), &execution)
474                .map_err(|cause| resource(self.archive.directory_span(), cause))?
475                .is_none()
476            {
477                return Ok(false);
478            }
479        }
480        Ok(self.latest.is_some())
481    }
482}