Skip to main content

structured_cades/timestamp/
archive.rs

1//! Original `ATSv3` input/index composition over the existing CMS archive core.
2
3use super::{
4    CheckError, CheckTime, Checked, Context, InputError, ProfileFailure, ProfileSource, tsp,
5};
6use crate::{
7    Cades, Error, ISSUER,
8    prepare::Preparation,
9    source::{DocumentId, Node},
10};
11use core::cell::Cell;
12use structured_asn1::digest::Resolution;
13use structured_asn1::memory::Memory;
14use structured_asn1::{
15    Reader,
16    memory::{Execution, OwnedVec, Work},
17};
18use structured_cms::{
19    Octets,
20    archive::{self, HashIndex, IndexCoverage},
21    digest::{DigestBatch, DigestError, PassError},
22};
23use structured_cms::{
24    provider::SignatureProvider,
25    stream::{Receiver, Refused, Source},
26};
27use structured_ltv::{
28    material::Coverage,
29    source::{Query, Scope, Source as _},
30};
31use structured_ltv::{pki::CheckedPath, timestamp::Purpose};
32
33/// No detached source exists for a carried-content operation. This
34/// uninhabited type cannot supply a fabricated empty-input fallback.
35#[derive(Debug)]
36pub enum NoDetached {}
37impl Source for NoDetached {
38    type Error = core::convert::Infallible;
39    fn stream<W: Work>(&mut self, _: &mut impl Receiver, _: &W) -> Result<(), Self::Error> {
40        match *self {}
41    }
42}
43
44/// Actual `ATSv3` preflight/content refusal, distinct from absent proof.
45#[derive(Debug)]
46pub enum ArchiveError<SE, PE> {
47    /// Original source/schema/encoding/resource refusal.
48    Processing(Error),
49    /// Original archive-index digest/refusal/output-contract cause.
50    Digest(DigestError<PE>),
51    /// An index or imprint mechanism could not be resolved.
52    Unsupported,
53    /// An index or imprint identifier violates its algorithm's schema.
54    MalformedAlgorithm,
55    /// Actual MD5 use is forbidden by the baseline format before index hashing.
56    ForbiddenDigest {
57        /// Original index algorithm enclosure.
58        at: u64,
59    },
60    /// An index hash has no original root value to which it refers.
61    UnmatchedIndex,
62    /// The index and timestamp imprint do not use the same digest mechanism.
63    AlgorithmMismatch,
64    /// Original content pass failed without a complete digest.
65    Content(PassError<SE, PE>),
66    /// Private prepared mapping did not produce its sole completed request.
67    MissingDigest,
68}
69
70/// Sticky original range delivery, with no document buffering or second pass.
71#[derive(Debug)]
72pub enum ArchiveStreamError {
73    /// Metadata admission failed before the next original range.
74    Resource(structured_asn1::memory::MemoryError),
75    /// The common driver retains the original receiver refusal.
76    Receiver(Refused),
77    /// No complete original content digest was prepared; never hash empty bytes.
78    MissingDigest,
79    /// The original archive range helper refused source membership.
80    Source(structured_cms::Error),
81}
82
83enum Content<'s, 'd, S> {
84    Carried(&'s Octets<'d>),
85    Detached(&'s mut S),
86}
87impl<S: Source> Source for Content<'_, '_, S> {
88    type Error = InputError<S::Error>;
89    fn stream<W: Work>(
90        &mut self,
91        receiver: &mut impl Receiver,
92        work: &W,
93    ) -> Result<(), Self::Error> {
94        match self {
95            Self::Carried(source) => source.stream(receiver, work).map_err(InputError::Carried),
96            Self::Detached(source) => source.stream(receiver, work).map_err(InputError::Detached),
97        }
98    }
99}
100
101/// Independently checked archive token, exact original index coverage and its
102/// derived whole-item selection. Application chronology/status/trust remain
103/// separate; coverage is usable only with this exact successful token actor.
104pub struct ArchiveChecked<'model, 'document, 'path> {
105    timestamp: Checked<'model, 'document, 'path>,
106    index: IndexCoverage,
107    coverage: Coverage<Node, DocumentId<'model, 'document>, (), Memory, Cades>,
108}
109
110/// Borrowed common facts of an actual modern or historical archive receipt.
111/// Private fields prevent callers from pairing one token with another input's
112/// coverage. Negative checks remain negative; material requires its own actor.
113#[derive(Clone, Copy)]
114pub struct ArchiveView<'view, 'model, 'document, 'path> {
115    timestamp: &'view Checked<'model, 'document, 'path>,
116    coverage: &'view Coverage<Node, DocumentId<'model, 'document>, (), Memory, Cades>,
117}
118impl<'v, 'm, 'd, 'p> From<&'v ArchiveChecked<'m, 'd, 'p>> for ArchiveView<'v, 'm, 'd, 'p> {
119    fn from(value: &'v ArchiveChecked<'m, 'd, 'p>) -> Self {
120        Self {
121            timestamp: value.timestamp(),
122            coverage: value.coverage(),
123        }
124    }
125}
126impl<'v, 'm, 'd, 'p> From<&'v super::historical::HistoricalChecked<'m, 'd, 'p>>
127    for ArchiveView<'v, 'm, 'd, 'p>
128{
129    fn from(value: &'v super::historical::HistoricalChecked<'m, 'd, 'p>) -> Self {
130        Self {
131            timestamp: value.timestamp(),
132            coverage: value.coverage(),
133        }
134    }
135}
136impl<'v, 'm, 'd, 'p> From<&'v crate::evidence::RecordArchive<'_, '_, 'm, 'd, 'p>>
137    for ArchiveView<'v, 'm, 'd, 'p>
138{
139    fn from(value: &'v crate::evidence::RecordArchive<'_, '_, 'm, 'd, 'p>) -> Self {
140        Self {
141            timestamp: value.timestamp(),
142            coverage: value.coverage(),
143        }
144    }
145}
146impl<'m, 'd, 'p> ArchiveView<'_, 'm, 'd, 'p> {
147    pub(crate) fn timestamp(&self) -> &Checked<'m, 'd, 'p> {
148        self.timestamp
149    }
150    pub(crate) fn coverage(&self) -> &Coverage<Node, DocumentId<'m, 'd>, (), Memory, Cades> {
151        self.coverage
152    }
153}
154impl<'m, 'd, 'p> ArchiveChecked<'m, 'd, 'p> {
155    /// Complete original common timestamp facts, including negative outcomes.
156    #[must_use]
157    pub fn timestamp(&self) -> &Checked<'m, 'd, 'p> {
158        &self.timestamp
159    }
160    /// Actual original certificate/revocation/unsigned-value index matches.
161    #[must_use]
162    pub fn index(&self) -> &IndexCoverage {
163        &self.index
164    }
165    /// Mathematical input selection; bind it to this timestamp's actual
166    /// Verified receipt before claiming archived material or earlier actors.
167    #[must_use]
168    pub fn coverage(&self) -> &Coverage<Node, DocumentId<'m, 'd>, (), Memory, Cades> {
169        &self.coverage
170    }
171
172    /// Derives the complete earlier-actor census from THIS archive's actual
173    /// mathematical index coverage. Source order and genTime are not used as
174    /// substitutes for coverage; the archive's own TSA is excluded. The full
175    /// original counter census is retained with its explicit inclusion mask.
176    pub fn requirements(
177        &self,
178        prepared: &Preparation<'m, 'd>,
179        operation: &Reader<'d>,
180    ) -> Result<
181        structured_ltv::timestamp::TimestampRequirements<Node, DocumentId<'m, 'd>, (), Cades>,
182        ScopeError,
183    > {
184        super::requirements::derive(&self.timestamp, &self.coverage, prepared, operation)
185    }
186}
187
188/// An earlier archive scope requires this exact successful token and source.
189#[derive(Debug)]
190pub enum ScopeError {
191    /// Original session/work/storage refusal.
192    Processing(Error),
193    /// Original archive has no complete common mathematical evidence.
194    Unproven,
195    /// Another parallel signer/preparation in the same original `SignedData`.
196    Foreign,
197    /// Actual coverage/source/current-query refusal, not a missing actor.
198    Coverage(structured_ltv::material::CoverageError<Node>),
199}
200
201struct Input<'s, 'm, 'd> {
202    prepared: &'s Preparation<'m, 'd>,
203    index: &'s HashIndex<'d>,
204    hash: &'s Cell<Option<OwnedVec<u8>>>,
205}
206impl Source for Input<'_, '_, '_> {
207    type Error = ArchiveStreamError;
208    fn stream<W: Work>(
209        &mut self,
210        receiver: &mut impl Receiver,
211        work: &W,
212    ) -> Result<(), Self::Error> {
213        let hash = self.hash.take().ok_or(ArchiveStreamError::MissingDigest)?;
214        let ranges = archive::imprint_input(
215            self.prepared.document().message(),
216            self.prepared.signer(),
217            self.index,
218            &hash,
219        )
220        .map_err(ArchiveStreamError::Source)?;
221        for range in ranges {
222            work.charge_work(1).map_err(ArchiveStreamError::Resource)?;
223            receiver
224                .write(range)
225                .map_err(ArchiveStreamError::Receiver)?;
226        }
227        Ok(())
228    }
229}
230
231/// Full original archive check including its preflight, with original source
232/// map preservation for failures from the carried `TSTInfo` profile callback.
233#[derive(Debug)]
234pub enum ArchiveCheckError<SE, PE, RE> {
235    /// Original index/source failure before common timestamp processing.
236    Before(ArchiveError<SE, PE>),
237    /// Original timestamp/profile/source/provider failure and its source maps.
238    Timestamp(CheckError<ArchiveStreamError, PE, RE, ArchiveError<SE, PE>>),
239}
240
241impl<'m, 'd> Context<'_, 'm, 'd> {
242    /// Checks `ATSv3` against ORIGINAL attached eContent. The imprint input
243    /// contains its content digest, never a copied/normalized whole document.
244    /// This delegates index matching and range construction to the existing
245    /// CMS archive kernel (EN319122-1 §§5.5.2–5.5.3).
246    #[expect(
247        clippy::result_large_err,
248        reason = "index refusal and the original mapped token cause are returned without allocating on an already exhausted account"
249    )]
250    pub fn check_archive_carried<'a, 'p, P, PE, RE>(
251        &'a self,
252        provider: &'a P,
253        path: &'p CheckedPath<'d>,
254        certificate_parent: &Reader<'d>,
255        time: &CheckTime<'_, 'd>,
256        recognizes: impl FnMut(&tsp::TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
257    ) -> Result<
258        ArchiveChecked<'m, 'd, 'p>,
259        ArchiveCheckError<InputError<core::convert::Infallible>, PE, RE>,
260    >
261    where
262        P: for<'scope> SignatureProvider<'scope, Error = PE>,
263    {
264        let source = self
265            .prepared
266            .document()
267            .message()
268            .content()
269            .content()
270            .ok_or_else(|| {
271                ArchiveCheckError::Before(ArchiveError::Processing(Error::Foreign {
272                    at: self.entry.property().span().start,
273                }))
274            })?;
275        self.archive(
276            provider,
277            path,
278            certificate_parent,
279            time,
280            &mut Content::<NoDetached>::Carried(source),
281            recognizes,
282        )
283    }
284
285    /// Checks the explicit detached input only when original eContent is
286    /// absent. The common source driver preserves incomplete/refused EOF;
287    /// detached acquisition is never a substitute for a carried document.
288    #[expect(
289        clippy::result_large_err,
290        reason = "detached acquisition and nested archive token failures keep their distinct original provenance without boxing"
291    )]
292    pub fn check_archive_detached<'a, 'p, P, S: Source, PE, RE>(
293        &'a self,
294        provider: &'a P,
295        path: &'p CheckedPath<'d>,
296        certificate_parent: &Reader<'d>,
297        time: &CheckTime<'_, 'd>,
298        detached: &mut S,
299        recognizes: impl FnMut(&tsp::TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
300    ) -> Result<ArchiveChecked<'m, 'd, 'p>, ArchiveCheckError<InputError<S::Error>, PE, RE>>
301    where
302        P: for<'scope> SignatureProvider<'scope, Error = PE>,
303    {
304        if !self.prepared.document().message().content().is_detached() {
305            return Err(ArchiveCheckError::Before(ArchiveError::Processing(
306                Error::Foreign {
307                    at: self.entry.property().span().start,
308                },
309            )));
310        }
311        self.archive(
312            provider,
313            path,
314            certificate_parent,
315            time,
316            &mut Content::Detached(detached),
317            recognizes,
318        )
319    }
320
321    #[expect(
322        clippy::result_large_err,
323        reason = "one archive transaction propagates owned source maps and opaque source/provider causes without a refusal-time allocation"
324    )]
325    fn archive<'a, 'p, P, S: Source, PE, RE>(
326        &'a self,
327        provider: &'a P,
328        path: &'p CheckedPath<'d>,
329        certificate_parent: &Reader<'d>,
330        time: &CheckTime<'_, 'd>,
331        original: &mut S,
332        recognizes: impl FnMut(&tsp::TokenExtension<'_>, &Execution<'_>) -> Result<bool, RE>,
333    ) -> Result<ArchiveChecked<'m, 'd, 'p>, ArchiveCheckError<S::Error, PE, RE>>
334    where
335        P: for<'scope> SignatureProvider<'scope, Error = PE>,
336    {
337        let before = ArchiveCheckError::Before;
338        let processing = |cause| before(ArchiveError::Processing(cause));
339        self.require_archive().map_err(processing)?;
340        let index = HashIndex::of_token(self.token, self.reader)
341            .map_err(|cause| processing(Error::Cms(cause)))?;
342        let coverage = self.matched_index(&index, provider).map_err(before)?;
343        let hash = Cell::new(None);
344        let mut covered = Input {
345            prepared: self.prepared,
346            index: &index,
347            hash: &hash,
348        };
349        let checked = self
350            .check_profile(
351                provider,
352                path,
353                certificate_parent,
354                time,
355                &mut covered,
356                (recognizes, |input| {
357                    let failure = |cause| ProfileFailure {
358                        source: ProfileSource::Information,
359                        cause,
360                    };
361                    let tsp::HashAlgorithmReading::Identifier(algorithm) =
362                        input.info.imprint().algorithm()
363                    else {
364                        return Err(failure(ArchiveError::MalformedAlgorithm));
365                    };
366                    match archive::same_algorithm(&index, algorithm, &input.reader.execution())
367                        .map_err(|cause| failure(ArchiveError::Processing(Error::Cms(cause))))?
368                    {
369                        Resolution::Supported(true) => {}
370                        Resolution::Supported(false) => {
371                            return Err(failure(ArchiveError::AlgorithmMismatch));
372                        }
373                        Resolution::Unsupported => return Err(failure(ArchiveError::Unsupported)),
374                        Resolution::Malformed => {
375                            return Err(failure(ArchiveError::MalformedAlgorithm));
376                        }
377                    }
378                    let work = input.reader.execution();
379                    let identity = match structured_pkix::algorithm::digest::resolve(
380                        provider, algorithm, &work,
381                    )
382                    .map_err(|cause| failure(ArchiveError::Processing(Error::Pkix(cause))))?
383                    {
384                        Resolution::Supported(value) => value,
385                        Resolution::Unsupported => return Err(failure(ArchiveError::Unsupported)),
386                        Resolution::Malformed => {
387                            return Err(failure(ArchiveError::MalformedAlgorithm));
388                        }
389                    };
390                    let mut identities = work.vector();
391                    identities.try_push(identity).map_err(|cause| {
392                        failure(ArchiveError::Processing(Error::resource(
393                            cause,
394                            "cades.archive",
395                            input.info.imprint().span().start,
396                        )))
397                    })?;
398                    let batch = DigestBatch::prepare(
399                        provider,
400                        identities.into_owned(),
401                        &work,
402                        "cades.archive.content",
403                        self.entry.property().span().start,
404                    )
405                    .map_err(|cause| failure(ArchiveError::Digest(cause)))?;
406                    let memo = batch
407                        .run(original, &work)
408                        .map_err(|cause| failure(ArchiveError::Content(cause)))?;
409                    let digest = memo
410                        .for_request(0, &work)
411                        .map_err(|cause| failure(ArchiveError::Digest(cause)))?
412                        .ok_or_else(|| failure(ArchiveError::MissingDigest))?;
413                    // P::Output may borrow the scoped information source. Retain only
414                    // the finished hash bytes across that HRTB boundary; never retain
415                    // a view of the temporary joined TSTInfo, state, or identity.
416                    let mut retained = work.vector();
417                    retained
418                        .try_extend_from_slice(digest.as_bytes())
419                        .map_err(|cause| {
420                            failure(ArchiveError::Processing(Error::resource(
421                                cause,
422                                "cades.archive.content",
423                                self.entry.property().span().start,
424                            )))
425                        })?;
426                    hash.set(Some(retained.into_owned()));
427                    Ok(())
428                }),
429            )
430            .map_err(ArchiveCheckError::Timestamp)?;
431        let selected = self.coverage(&coverage).map_err(processing)?;
432        Ok(ArchiveChecked {
433            timestamp: checked,
434            index: coverage,
435            coverage: selected,
436        })
437    }
438
439    fn matched_index<'a, P, SE, PE>(
440        &'a self,
441        index: &HashIndex<'d>,
442        provider: &'a P,
443    ) -> Result<IndexCoverage, ArchiveError<SE, PE>>
444    where
445        P: for<'scope> SignatureProvider<'scope, Error = PE>,
446    {
447        let processing = ArchiveError::Processing;
448        self.reader
449            .charge(1 + index.algorithm().algorithm().as_bytes().len().min(8) as u64)
450            .map_err(|cause| processing(Error::encoding(cause, "cades.archive.index")))?;
451        if index.algorithm().algorithm()
452            == structured_asn1::Oid::from_bytes(&[42, 134, 72, 134, 247, 13, 2, 5])
453        {
454            return Err(ArchiveError::ForbiddenDigest {
455                at: index.algorithm().offset(),
456            });
457        }
458        let coverage = match archive::check_index(
459            index,
460            self.prepared.document().message(),
461            self.prepared.signer(),
462            provider,
463            self.reader,
464        )
465        .map_err(ArchiveError::Digest)?
466        {
467            Resolution::Supported(value) => value,
468            Resolution::Unsupported => return Err(ArchiveError::Unsupported),
469            Resolution::Malformed => return Err(ArchiveError::MalformedAlgorithm),
470        };
471        if !coverage.is_valid() {
472            return Err(ArchiveError::UnmatchedIndex);
473        }
474        Ok(coverage)
475    }
476    fn coverage(
477        &self,
478        index: &IndexCoverage,
479    ) -> crate::Result<Coverage<Node, DocumentId<'m, 'd>, (), Memory, Cades>> {
480        let work = self.reader.execution();
481        let resource = |cause| {
482            Error::resource(
483                cause,
484                "cades.archive.coverage",
485                self.entry.property().span().start,
486            )
487        };
488        let mut items = work.map();
489        let message = self.prepared.document().message();
490        for (choice, covered) in message
491            .certificate_choices()
492            .iter()
493            .zip(index.certificates())
494        {
495            work.charge_work(1).map_err(resource)?;
496            let span = match choice {
497                structured_cms::signed::CertificateChoice::X509(ordinal) => {
498                    message.certificates()[*ordinal].span()
499                }
500                structured_cms::signed::CertificateChoice::Extended(value)
501                | structured_cms::signed::CertificateChoice::AttributeV1(value)
502                | structured_cms::signed::CertificateChoice::AttributeV2(value)
503                | structured_cms::signed::CertificateChoice::Other(value) => value.span(),
504            };
505            items
506                .try_insert(Node::of(span), *covered)
507                .map_err(resource)?;
508        }
509        for (value, covered) in message.revocations().iter().zip(index.revocations()) {
510            work.charge_work(1).map_err(resource)?;
511            items
512                .try_insert(Node::of(value.span()), *covered)
513                .map_err(resource)?;
514        }
515        let mut ranges = work.vector();
516        if let Some(attributes) = self.prepared.signer().signed_attributes() {
517            // EN319122-1 §5.5.3 input3 includes the original signedAttrs,
518            // independently of the index of UNSIGNED attribute values. Their
519            // root content timestamps therefore belong to the covered census.
520            // https://www.etsi.org/deliver/etsi_en/319100_319199/31912201/01.03.01_60/en_31912201v010301p.pdf
521            ranges
522                .try_push(attributes.element().span())
523                .map_err(resource)?;
524            for attribute in attributes.items() {
525                work.charge_work(1).map_err(resource)?;
526                items
527                    .try_insert(Node::of(attribute.span()), true)
528                    .map_err(resource)?;
529            }
530        }
531
532        if let Some(attributes) = self.prepared.signer().unsigned_attributes() {
533            for attribute in attributes.items() {
534                work.charge_work(1).map_err(resource)?;
535                // check_index constructs the complete original projection.
536                // Each uncovered value clears its owning attribute below;
537                // a genuinely empty set never establishes whole coverage.
538                let all = !attribute.values().is_empty();
539                items
540                    .try_insert(Node::of(attribute.span()), all)
541                    .map_err(resource)?;
542            }
543            for (position, covered) in index.attribute_values() {
544                work.charge_work(1).map_err(resource)?;
545                let attribute = &attributes.items()[position.attribute];
546                let value = &attribute.values()[position.value];
547                items
548                    .try_insert(Node::of(value.span()), *covered)
549                    .map_err(resource)?;
550                if *covered {
551                    ranges.try_push(value.span()).map_err(resource)?;
552                } else if let Some(all) = items
553                    .get_mut(&Node::of(attribute.span()), &work)
554                    .map_err(resource)?
555                {
556                    *all = false;
557                }
558            }
559        }
560        self.include_nested(&mut items, &mut ranges, &work)?;
561        let source = self.prepared.document();
562        let query = Scope::new(source, source.snapshot(), &work);
563        query
564            .admit(self.entry.property(), 1)
565            .map_err(|cause| match cause {
566                structured_ltv::source::ScopeError::Changed { node } => Error::Foreign {
567                    at: node.span().start,
568                },
569                structured_ltv::source::ScopeError::Resource { cause, .. } => resource(cause),
570            })?;
571        Ok(Coverage::for_token(
572            ISSUER,
573            source.snapshot(),
574            Node::of(self.prepared.signer().span()),
575            self.entry.property(),
576            Node::of(
577                self.entry
578                    .value()
579                    .expect("archive requires original token")
580                    .span(),
581            ),
582            Purpose::Archive,
583            items.into_owned(),
584        ))
585    }
586    fn include_nested<W: Work>(
587        &self,
588        items: &mut structured_asn1::memory::OwnedMap<Node, bool, W>,
589        ranges: &mut OwnedVec<structured_asn1::Span, W>,
590        work: &W,
591    ) -> crate::Result<()> {
592        let resource = |cause| {
593            Error::resource(
594                cause,
595                "cades.archive.coverage",
596                self.entry.property().span().start,
597            )
598        };
599        ranges
600            .try_sort_unstable_by(|left, right| {
601                work.charge_work(1)?;
602                Ok::<_, structured_asn1::memory::MemoryError>(left.start.cmp(&right.start))
603            })
604            .map_err(|cause| match cause {
605                structured_asn1::memory::SortError::Resource(cause)
606                | structured_asn1::memory::SortError::Comparison(cause) => resource(cause),
607            })?;
608        let mut maximum = 0;
609        for span in &mut *ranges {
610            work.charge_work(1).map_err(resource)?;
611            maximum = maximum.max(span.start + span.length);
612            span.length = maximum - span.start;
613        }
614        for counter in self.prepared.counters() {
615            let covered = contains(ranges, counter.node(), work).map_err(resource)?;
616            items
617                .try_insert(counter.node(), covered)
618                .map_err(resource)?;
619        }
620        for token in self.prepared.timestamps() {
621            let covered = contains(ranges, token.property(), work).map_err(resource)?;
622            if token.owner() != self.entry.owner() {
623                items
624                    .try_insert(token.property(), covered)
625                    .map_err(resource)?;
626            }
627            if let Some(value) = token.value() {
628                items
629                    .try_insert(
630                        Node::of(value.span()),
631                        contains(ranges, Node::of(value.span()), work).map_err(resource)?,
632                    )
633                    .map_err(resource)?;
634            }
635        }
636        Ok(())
637    }
638
639    fn require_archive(&self) -> crate::Result<()> {
640        // The common ATSv3 input selects a root SignedData.signerInfos item
641        // (EN319122-1 §5.5.3(3)). Preparation retains counter-owned occurrences
642        // with an AttributePlacement finding, never as conforming obligations
643        // whose mathematical actor this root-input operation could establish.
644        if self.entry.purpose() != Purpose::Archive
645            || self.entry.preservation_kind().is_some()
646            || self.entry.owner() != Node::of(self.prepared.signer().span())
647        {
648            return Err(Error::Foreign {
649                at: self.entry.property().span().start,
650            });
651        }
652        Ok(())
653    }
654}
655
656fn contains(
657    ranges: &[structured_asn1::Span],
658    node: Node,
659    work: &impl Work,
660) -> Result<bool, structured_asn1::memory::MemoryError> {
661    let mut from = 0;
662    let mut to = ranges.len();
663    let span = node.span();
664    while from < to {
665        work.charge_work(1)?;
666        let middle = from + (to - from) / 2;
667        if ranges[middle].start <= span.start {
668            from = middle + 1;
669        } else {
670            to = middle;
671        }
672    }
673    if from == 0 {
674        return Ok(false);
675    }
676    let range = ranges[from - 1];
677    Ok(range.start + range.length >= span.start + span.length)
678}