Skip to main content

typst_pack/opendal/pack_assembly/
package.rs

1use std::fmt;
2
3use typst::syntax::package::PackageSpec;
4
5use super::super::BoxError;
6use super::super::read::recursive::{
7    PackageTreeRecursiveReadOperation, RecursiveReadLimits, RecursiveReadOperation,
8    RecursiveReadResource, RecursiveSurveyIssue, RecursiveSurveyIssueKind,
9    read_first_present_package_tree_prefix_with_resolved,
10};
11use super::super::read::{ExactPathReadOperation, ResolvedOperators, read_exact_path};
12use super::super::{Location, LocationRoleError, OperatorResolver};
13use crate::limits::{LimitError, Limits, ResourceKind};
14use crate::package_catalog::PackageTreeError;
15use crate::read_layout;
16use crate::redacted_error::RedactedError;
17
18/// Named finite ceilings for one OpenDAL Package Tree Read.
19#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20pub struct PackageTreeReadCeilings {
21    /// Entries yielded by recursive listings across attempted tree sources.
22    pub listed_entries: u64,
23    /// Bytes in one yielded operation path.
24    pub listed_path_bytes: u64,
25    /// Bytes retained for paths and structural evidence across tree sources.
26    pub total_listed_path_bytes: u64,
27    /// File objects selected from one present tree source.
28    pub selected_files: u64,
29    /// Exact bytes retained for one selected file.
30    pub object_bytes: u64,
31    /// Exact file bytes retained for the selected Package Tree.
32    pub total_bytes: u64,
33}
34
35impl PackageTreeReadCeilings {
36    /// The first-party version-1 Package Tree Read profile.
37    pub const fn reference_v1() -> Self {
38        Self {
39            listed_entries: 100_000,
40            listed_path_bytes: 64 * 1024,
41            total_listed_path_bytes: 64 * 1024 * 1024,
42            selected_files: 50_000,
43            object_bytes: 64 * 1024 * 1024,
44            total_bytes: 512 * 1024 * 1024,
45        }
46    }
47}
48
49/// A resource bounded during OpenDAL Package Tree Read.
50pub type PackageTreeReadResource = ResourceKind<11>;
51
52#[allow(non_upper_case_globals)]
53impl ResourceKind<11> {
54    /// Entries yielded by recursive listings.
55    pub const ListedEntries: Self = Self::new(0);
56    /// Bytes in one yielded operation path.
57    pub const ListedPathBytes: Self = Self::new(1);
58    /// Bytes retained for paths and structural evidence.
59    pub const TotalListedPathBytes: Self = Self::new(2);
60    /// Selected file objects.
61    pub const SelectedFiles: Self = Self::new(3);
62    /// Bytes retained for one selected file.
63    pub const ObjectBytes: Self = Self::new(4);
64    /// Bytes retained across the selected tree.
65    pub const TotalBytes: Self = Self::new(5);
66}
67
68/// Mandatory finite limits for OpenDAL Package Tree Read.
69pub type PackageTreeReadLimits = Limits<PackageTreeReadResource>;
70
71impl Limits<PackageTreeReadResource> {
72    /// Validates every named Package Tree Read ceiling.
73    #[track_caller]
74    pub fn new(ceilings: PackageTreeReadCeilings) -> Self {
75        let limits = Self::from_ceilings([
76            ceilings.listed_entries,
77            ceilings.listed_path_bytes,
78            ceilings.total_listed_path_bytes,
79            ceilings.selected_files,
80            ceilings.object_bytes,
81            ceilings.total_bytes,
82            0,
83        ])
84        .assert_probe_resources([
85            PackageTreeReadResource::ListedEntries,
86            PackageTreeReadResource::ListedPathBytes,
87            PackageTreeReadResource::TotalListedPathBytes,
88            PackageTreeReadResource::SelectedFiles,
89            PackageTreeReadResource::ObjectBytes,
90            PackageTreeReadResource::TotalBytes,
91        ]);
92        assert!(
93            ceilings.object_bytes <= ceilings.total_bytes,
94            "the ObjectBytes ceiling {} exceeds the TotalBytes ceiling {}",
95            ceilings.object_bytes,
96            ceilings.total_bytes
97        );
98        limits
99    }
100
101    /// The validated first-party version-1 limits.
102    pub const fn reference_v1() -> Self {
103        Self::from_ceilings([
104            100_000,
105            64 * 1024,
106            64 * 1024 * 1024,
107            50_000,
108            64 * 1024 * 1024,
109            512 * 1024 * 1024,
110            0,
111        ])
112    }
113
114    /// The maximum number of entries yielded across attempted tree sources.
115    pub const fn listed_entries(&self) -> u64 {
116        self.ceilings[0]
117    }
118
119    /// The maximum byte length of one yielded operation path.
120    pub const fn listed_path_bytes(&self) -> u64 {
121        self.ceilings[1]
122    }
123
124    /// The maximum retained bytes for paths and structural evidence.
125    pub const fn total_listed_path_bytes(&self) -> u64 {
126        self.ceilings[2]
127    }
128
129    /// The maximum selected file count.
130    pub const fn selected_files(&self) -> u64 {
131        self.ceilings[3]
132    }
133
134    /// The maximum exact bytes retained for one selected file.
135    pub const fn object_bytes(&self) -> u64 {
136        self.ceilings[4]
137    }
138
139    /// The maximum exact bytes retained for the selected Package Tree.
140    pub const fn total_bytes(&self) -> u64 {
141        self.ceilings[5]
142    }
143}
144
145/// Package Tree Read exceeded or could not account for a mandatory limit.
146pub type PackageTreeReadLimitError = LimitError<PackageTreeReadResource>;
147
148/// Named finite ceilings for one raw Package Archive Read.
149#[derive(Clone, Copy, Debug, Eq, PartialEq)]
150pub struct PackageArchiveReadCeilings {
151    /// Exact raw archive bytes retained from one candidate.
152    pub archive_bytes: u64,
153}
154
155impl PackageArchiveReadCeilings {
156    /// The first-party 128 MiB archive profile.
157    pub const fn reference_v1() -> Self {
158        Self {
159            archive_bytes: 128 * 1024 * 1024,
160        }
161    }
162}
163
164/// A resource bounded while reading a raw Package Archive.
165pub type PackageArchiveReadResource = ResourceKind<12>;
166
167#[allow(non_upper_case_globals)]
168impl ResourceKind<12> {
169    /// Exact raw Package Archive bytes.
170    pub const ArchiveBytes: Self = Self::new(0);
171}
172
173/// Mandatory finite limits for one raw Package Archive Read.
174pub type PackageArchiveReadLimits = Limits<PackageArchiveReadResource>;
175
176impl Limits<PackageArchiveReadResource> {
177    /// Validates the named raw archive ceiling.
178    #[track_caller]
179    pub fn new(ceilings: PackageArchiveReadCeilings) -> Self {
180        Self::from_ceilings([ceilings.archive_bytes, 0, 0, 0, 0, 0, 0])
181            .assert_probe_resources([PackageArchiveReadResource::ArchiveBytes])
182    }
183
184    /// The validated first-party version-1 limits.
185    pub const fn reference_v1() -> Self {
186        Self::from_ceilings([128 * 1024 * 1024, 0, 0, 0, 0, 0, 0])
187    }
188
189    /// The maximum exact raw archive bytes retained from one candidate.
190    pub const fn archive_bytes(&self) -> u64 {
191        self.ceilings[0]
192    }
193}
194
195/// A raw Package Archive exceeded or could not account for its limit.
196pub type PackageArchiveReadLimitError = LimitError<PackageArchiveReadResource>;
197
198/// Named finite ceilings for Package Read fallback.
199#[derive(Clone, Copy, Debug, Eq, PartialEq)]
200pub struct PackageReadCeilings {
201    /// Ceilings shared across ordered Package Tree candidates.
202    pub trees: PackageTreeReadCeilings,
203    /// Ceilings applied independently to cache and registry candidates.
204    pub archives: PackageArchiveReadCeilings,
205}
206
207impl PackageReadCeilings {
208    /// The first-party version-1 composite profile.
209    pub const fn reference_v1() -> Self {
210        Self {
211            trees: PackageTreeReadCeilings::reference_v1(),
212            archives: PackageArchiveReadCeilings::reference_v1(),
213        }
214    }
215}
216
217/// A resource bounded across Package Read fallback.
218pub type PackageReadResource = ResourceKind<13>;
219
220#[allow(non_upper_case_globals)]
221impl ResourceKind<13> {
222    pub const TreeListedEntries: Self = Self::new(0);
223    pub const TreeListedPathBytes: Self = Self::new(1);
224    pub const TreeTotalListedPathBytes: Self = Self::new(2);
225    pub const TreeSelectedFiles: Self = Self::new(3);
226    pub const TreeObjectBytes: Self = Self::new(4);
227    pub const TreeTotalBytes: Self = Self::new(5);
228    pub const ArchiveBytes: Self = Self::new(6);
229}
230
231/// Mandatory finite limits for Package Read fallback.
232pub type PackageReadLimits = Limits<PackageReadResource>;
233
234impl Limits<PackageReadResource> {
235    /// Validates both Package Read limit families.
236    #[track_caller]
237    pub fn new(ceilings: PackageReadCeilings) -> Self {
238        let trees = PackageTreeReadLimits::new(ceilings.trees);
239        let archives = PackageArchiveReadLimits::new(ceilings.archives);
240        Self::from_ceilings([
241            trees.listed_entries(),
242            trees.listed_path_bytes(),
243            trees.total_listed_path_bytes(),
244            trees.selected_files(),
245            trees.object_bytes(),
246            trees.total_bytes(),
247            archives.archive_bytes(),
248        ])
249    }
250
251    /// Limits shared across ordered Package Tree candidates.
252    pub const fn trees(&self) -> PackageTreeReadLimits {
253        PackageTreeReadLimits::from_ceilings([
254            self.ceilings[0],
255            self.ceilings[1],
256            self.ceilings[2],
257            self.ceilings[3],
258            self.ceilings[4],
259            self.ceilings[5],
260            0,
261        ])
262    }
263
264    /// Limits applied independently to cache and registry candidates.
265    pub const fn archives(&self) -> PackageArchiveReadLimits {
266        PackageArchiveReadLimits::from_ceilings([self.ceilings[6], 0, 0, 0, 0, 0, 0])
267    }
268
269    /// The validated first-party version-1 composite limits.
270    pub const fn reference_v1() -> Self {
271        Self::from_ceilings([
272            100_000,
273            64 * 1024,
274            64 * 1024 * 1024,
275            50_000,
276            64 * 1024 * 1024,
277            512 * 1024 * 1024,
278            128 * 1024 * 1024,
279        ])
280    }
281}
282
283/// One explicitly configured prefix that may hold Package Trees.
284#[derive(Clone, Debug, Eq, PartialEq)]
285pub struct PackageTreeSource {
286    source: Location,
287}
288
289impl PackageTreeSource {
290    /// Configures one Package Tree prefix.
291    pub fn new(source: Location) -> Self {
292        Self { source }
293    }
294
295    /// The normalized configured prefix.
296    pub fn source(&self) -> &Location {
297        &self.source
298    }
299}
300
301/// A validated request to read one exact package from ordered sources.
302#[derive(Clone, Debug)]
303pub struct PackageReadRequest {
304    spec: PackageSpec,
305    tree_sources: Vec<PackageTreeSource>,
306    archive_cache: Option<Location>,
307    registry: Option<Location>,
308    limits: PackageReadLimits,
309}
310
311impl PackageReadRequest {
312    /// Validates every configured prefix role before accepting the request.
313    pub fn new(
314        spec: PackageSpec,
315        tree_sources: impl IntoIterator<Item = PackageTreeSource>,
316        archive_cache: Option<Location>,
317        registry: Option<Location>,
318        limits: PackageReadLimits,
319    ) -> Result<Self, PackageReadRequestRejection> {
320        let tree_sources = tree_sources.into_iter().collect::<Vec<_>>();
321        let mut issues = tree_sources
322            .iter()
323            .enumerate()
324            .filter_map(|(source_index, configured)| {
325                configured.source.require_prefix().err().map(|source| {
326                    PackageReadRequestIssue::InvalidTreeSourceRole {
327                        source_index,
328                        location: configured.source.clone(),
329                        source,
330                    }
331                })
332            })
333            .collect::<Vec<_>>();
334        if let Some(location) = &archive_cache
335            && let Err(source) = location.require_prefix()
336        {
337            issues.push(PackageReadRequestIssue::InvalidArchiveCacheRole {
338                location: location.clone(),
339                source,
340            });
341        }
342        if let Some(location) = &registry
343            && let Err(source) = location.require_prefix()
344        {
345            issues.push(PackageReadRequestIssue::InvalidRegistryRole {
346                location: location.clone(),
347                source,
348            });
349        }
350        if !issues.is_empty() {
351            return Err(PackageReadRequestRejection { spec, issues });
352        }
353        Ok(Self {
354            spec,
355            tree_sources,
356            archive_cache,
357            registry,
358            limits,
359        })
360    }
361
362    /// The exact package specification being read.
363    pub fn spec(&self) -> &PackageSpec {
364        &self.spec
365    }
366
367    /// Package Tree prefixes in caller precedence order.
368    pub fn tree_sources(&self) -> &[PackageTreeSource] {
369        &self.tree_sources
370    }
371
372    /// The optional raw Package Archive cache prefix.
373    pub fn archive_cache(&self) -> Option<&Location> {
374        self.archive_cache.as_ref()
375    }
376
377    /// The optional official Package Registry prefix.
378    pub fn registry(&self) -> Option<&Location> {
379        self.registry.as_ref()
380    }
381
382    /// Mandatory finite limits for this fallback operation.
383    pub const fn limits(&self) -> PackageReadLimits {
384        self.limits
385    }
386}
387
388/// Every invalid source role in a rejected Package Read request.
389#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
390#[error(
391    "Package Read request for {spec} was rejected with {issue_count} issue(s)",
392    issue_count = .issues.len()
393)]
394pub struct PackageReadRequestRejection {
395    spec: PackageSpec,
396    issues: Vec<PackageReadRequestIssue>,
397}
398
399impl PackageReadRequestRejection {
400    /// The exact package specification from the rejected request.
401    pub fn spec(&self) -> &PackageSpec {
402        &self.spec
403    }
404
405    /// Invalid source roles in request-field and caller-source order.
406    pub fn issues(&self) -> &[PackageReadRequestIssue] {
407        &self.issues
408    }
409}
410
411/// One invalid source role in a Package Read request.
412#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
413#[non_exhaustive]
414pub enum PackageReadRequestIssue {
415    /// A configured Package Tree source is not a prefix.
416    #[error("Package Tree source {source_index} at {location} is not a prefix: {source}")]
417    InvalidTreeSourceRole {
418        source_index: usize,
419        location: Location,
420        #[source]
421        source: LocationRoleError,
422    },
423    /// The configured raw archive cache is not a prefix.
424    #[error("Package Archive cache at {location} is not a prefix: {source}")]
425    InvalidArchiveCacheRole {
426        location: Location,
427        #[source]
428        source: LocationRoleError,
429    },
430    /// The configured Package Registry is not a prefix.
431    #[error("Package Registry at {location} is not a prefix: {source}")]
432    InvalidRegistryRole {
433        location: Location,
434        #[source]
435        source: LocationRoleError,
436    },
437}
438
439/// One exact file read below a Package Tree candidate prefix.
440pub struct PackageTreeReadEntry {
441    relative_path: String,
442    bytes: Vec<u8>,
443}
444
445impl PackageTreeReadEntry {
446    /// The canonical package-relative file path.
447    pub fn relative_path(&self) -> &str {
448        &self.relative_path
449    }
450
451    /// The exact bytes observed by the completed object read.
452    pub fn bytes(&self) -> &[u8] {
453        &self.bytes
454    }
455
456    /// The exact read byte length.
457    pub fn len(&self) -> u64 {
458        self.bytes.len() as u64
459    }
460
461    /// Whether the read file was empty.
462    pub fn is_empty(&self) -> bool {
463        self.bytes.is_empty()
464    }
465
466    /// Recovers the owned path and exact bytes.
467    pub fn into_parts(self) -> (String, Vec<u8>) {
468        (self.relative_path, self.bytes)
469    }
470}
471
472impl fmt::Debug for PackageTreeReadEntry {
473    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
474        formatter
475            .debug_struct("PackageTreeReadEntry")
476            .field("relative_path", &self.relative_path)
477            .field("byte_length", &self.bytes.len())
478            .finish()
479    }
480}
481
482/// Exact files read from the first present Package Tree candidate.
483pub struct PackageTreeRead {
484    spec: PackageSpec,
485    source_index: usize,
486    configured_source: Location,
487    candidate_location: Location,
488    entries: Vec<PackageTreeReadEntry>,
489}
490
491impl PackageTreeRead {
492    /// The exact package specification read.
493    pub fn spec(&self) -> &PackageSpec {
494        &self.spec
495    }
496
497    /// The caller-order index of the selected tree source.
498    pub fn source_index(&self) -> usize {
499        self.source_index
500    }
501
502    /// The configured Package Tree prefix.
503    pub fn configured_source(&self) -> &Location {
504        &self.configured_source
505    }
506
507    /// The specification-derived candidate prefix.
508    pub fn candidate_location(&self) -> &Location {
509        &self.candidate_location
510    }
511
512    /// Read entries in canonical package-relative path order.
513    pub fn entries(&self) -> &[PackageTreeReadEntry] {
514        &self.entries
515    }
516
517    /// Recovers the specification, source evidence, and owned entries.
518    pub fn into_parts(
519        self,
520    ) -> (
521        PackageSpec,
522        usize,
523        Location,
524        Location,
525        Vec<PackageTreeReadEntry>,
526    ) {
527        (
528            self.spec,
529            self.source_index,
530            self.configured_source,
531            self.candidate_location,
532            self.entries,
533        )
534    }
535}
536
537impl fmt::Debug for PackageTreeRead {
538    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
539        formatter
540            .debug_struct("PackageTreeRead")
541            .field("spec", &self.spec)
542            .field("source_index", &self.source_index)
543            .field("configured_source", &self.configured_source)
544            .field("candidate_location", &self.candidate_location)
545            .field("entries", &self.entries)
546            .finish()
547    }
548}
549
550/// Exact raw Package Archive bytes read from a configured cache.
551pub struct CachedPackageArchiveRead {
552    spec: PackageSpec,
553    configured_source: Location,
554    candidate_location: Location,
555    bytes: Vec<u8>,
556}
557
558impl CachedPackageArchiveRead {
559    /// The exact package specification read.
560    pub fn spec(&self) -> &PackageSpec {
561        &self.spec
562    }
563
564    /// The configured raw archive cache prefix.
565    pub fn configured_source(&self) -> &Location {
566        &self.configured_source
567    }
568
569    /// The specification-derived exact cache object.
570    pub fn candidate_location(&self) -> &Location {
571        &self.candidate_location
572    }
573
574    /// The exact raw Package Archive bytes.
575    pub fn bytes(&self) -> &[u8] {
576        &self.bytes
577    }
578
579    /// The exact raw archive byte length.
580    pub fn len(&self) -> u64 {
581        self.bytes.len() as u64
582    }
583
584    /// Whether the present raw archive object was empty.
585    pub fn is_empty(&self) -> bool {
586        self.bytes.is_empty()
587    }
588
589    /// Recovers the specification, source evidence, and exact bytes.
590    pub fn into_parts(self) -> (PackageSpec, Location, Location, Vec<u8>) {
591        (
592            self.spec,
593            self.configured_source,
594            self.candidate_location,
595            self.bytes,
596        )
597    }
598}
599
600impl fmt::Debug for CachedPackageArchiveRead {
601    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
602        formatter
603            .debug_struct("CachedPackageArchiveRead")
604            .field("spec", &self.spec)
605            .field("configured_source", &self.configured_source)
606            .field("candidate_location", &self.candidate_location)
607            .field("byte_length", &self.bytes.len())
608            .finish()
609    }
610}
611
612/// Exact raw Package Archive bytes read from the official registry.
613pub struct RegistryPackageArchiveRead {
614    spec: PackageSpec,
615    configured_source: Location,
616    candidate_location: Location,
617    cache_destination: Option<Location>,
618    bytes: Vec<u8>,
619}
620
621impl RegistryPackageArchiveRead {
622    /// The exact package specification read.
623    pub fn spec(&self) -> &PackageSpec {
624        &self.spec
625    }
626
627    /// The configured official Package Registry prefix.
628    pub fn configured_source(&self) -> &Location {
629        &self.configured_source
630    }
631
632    /// The specification-derived exact registry object.
633    pub fn candidate_location(&self) -> &Location {
634        &self.candidate_location
635    }
636
637    /// The derived cache object available for later optional write.
638    pub fn cache_destination(&self) -> Option<&Location> {
639        self.cache_destination.as_ref()
640    }
641
642    /// The exact raw bytes returned by the registry.
643    pub fn bytes(&self) -> &[u8] {
644        &self.bytes
645    }
646
647    /// The exact raw archive byte length.
648    pub fn len(&self) -> u64 {
649        self.bytes.len() as u64
650    }
651
652    /// Whether the present registry object was empty.
653    pub fn is_empty(&self) -> bool {
654        self.bytes.is_empty()
655    }
656
657    /// Recovers the specification, source evidence, cache destination, and bytes.
658    pub fn into_parts(self) -> (PackageSpec, Location, Location, Option<Location>, Vec<u8>) {
659        (
660            self.spec,
661            self.configured_source,
662            self.candidate_location,
663            self.cache_destination,
664            self.bytes,
665        )
666    }
667}
668
669impl fmt::Debug for RegistryPackageArchiveRead {
670    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
671        formatter
672            .debug_struct("RegistryPackageArchiveRead")
673            .field("spec", &self.spec)
674            .field("configured_source", &self.configured_source)
675            .field("candidate_location", &self.candidate_location)
676            .field("cache_destination", &self.cache_destination)
677            .field("byte_length", &self.bytes.len())
678            .finish()
679    }
680}
681
682/// Evidence that every applicable configured package source was absent.
683#[derive(Debug)]
684pub struct UnavailablePackageRead {
685    spec: PackageSpec,
686    failure: crate::PackageReadFailure,
687}
688
689impl UnavailablePackageRead {
690    /// The exact package specification that was unavailable.
691    pub fn spec(&self) -> &PackageSpec {
692        &self.spec
693    }
694
695    /// The stable failure to carry into resumed Pack Creation.
696    pub fn failure(&self) -> &crate::PackageReadFailure {
697        &self.failure
698    }
699
700    /// The stable reason the package was unavailable.
701    pub fn reason(&self) -> &crate::PackageReadFailureReason {
702        self.failure.reason()
703    }
704
705    /// Recovers the exact specification and owned failure.
706    pub fn into_parts(self) -> (PackageSpec, crate::PackageReadFailure) {
707        (self.spec, self.failure)
708    }
709}
710
711/// The raw result of reading one package through configured OpenDAL sources.
712#[derive(Debug)]
713#[non_exhaustive]
714pub enum PackageRead {
715    /// A present Package Tree candidate and its exact files.
716    Tree(PackageTreeRead),
717    /// A present raw Package Archive cache object.
718    CachedArchive(CachedPackageArchiveRead),
719    /// A present official Package Registry object.
720    RegistryArchive(RegistryPackageArchiveRead),
721    /// Every applicable configured candidate was definitely absent.
722    Unavailable(UnavailablePackageRead),
723}
724
725impl PackageRead {
726    /// The selected configured source, or `None` when unavailable.
727    pub fn configured_source(&self) -> Option<&Location> {
728        match self {
729            Self::Tree(value) => Some(value.configured_source()),
730            Self::CachedArchive(value) => Some(value.configured_source()),
731            Self::RegistryArchive(value) => Some(value.configured_source()),
732            Self::Unavailable(_) => None,
733        }
734    }
735
736    /// The selected derived candidate, or `None` when unavailable.
737    pub fn candidate_location(&self) -> Option<&Location> {
738        match self {
739            Self::Tree(value) => Some(value.candidate_location()),
740            Self::CachedArchive(value) => Some(value.candidate_location()),
741            Self::RegistryArchive(value) => Some(value.candidate_location()),
742            Self::Unavailable(_) => None,
743        }
744    }
745}
746
747/// Reads one package from ordered trees, an optional cache, then an optional registry.
748///
749/// Only definite absence advances fallback. Registry lookup is skipped when the
750/// official registry does not serve the requested namespace. This operation
751/// returns exact raw values and performs no archive expansion or cache write.
752#[allow(clippy::result_large_err)]
753pub async fn read_package<R: OperatorResolver + ?Sized>(
754    resolver: &R,
755    request: &PackageReadRequest,
756) -> Result<PackageRead, PackageReadError> {
757    let mut resolved = ResolvedOperators::new(resolver);
758    match read_package_tree_candidates_with_resolved(
759        &mut resolved,
760        request.spec(),
761        request.tree_sources(),
762        request.limits().trees(),
763    )
764    .await
765    {
766        Ok(Some(tree)) => return Ok(PackageRead::Tree(tree)),
767        Ok(None) => {}
768        Err(error) => return Err(error),
769    }
770
771    if let Some(configured_source) = request.archive_cache() {
772        let candidate_location = compose_candidate(
773            configured_source,
774            &read_layout::package_archive_cache_key(request.spec()),
775        );
776        match read_archive_candidate(
777            &mut resolved,
778            request.spec(),
779            configured_source,
780            &candidate_location,
781            ArchiveSource::Cache,
782            request.limits().archives().archive_bytes(),
783        )
784        .await
785        {
786            Ok(Some(bytes)) => {
787                return Ok(PackageRead::CachedArchive(CachedPackageArchiveRead {
788                    spec: request.spec().clone(),
789                    configured_source: configured_source.clone(),
790                    candidate_location,
791                    bytes,
792                }));
793            }
794            Ok(None) => {}
795            Err(error) => return Err(error),
796        }
797    }
798
799    if let (Some(configured_source), Some(registry_key)) = (
800        request.registry(),
801        read_layout::official_registry_archive_key(request.spec()),
802    ) {
803        let candidate_location = compose_candidate(configured_source, &registry_key);
804        match read_archive_candidate(
805            &mut resolved,
806            request.spec(),
807            configured_source,
808            &candidate_location,
809            ArchiveSource::Registry,
810            request.limits().archives().archive_bytes(),
811        )
812        .await
813        {
814            Ok(Some(bytes)) => {
815                let cache_destination = request.archive_cache().map(|cache| {
816                    compose_candidate(
817                        cache,
818                        &read_layout::package_archive_cache_key(request.spec()),
819                    )
820                });
821                return Ok(PackageRead::RegistryArchive(RegistryPackageArchiveRead {
822                    spec: request.spec().clone(),
823                    configured_source: configured_source.clone(),
824                    candidate_location,
825                    cache_destination,
826                    bytes,
827                }));
828            }
829            Ok(None) => {}
830            Err(error) => return Err(error),
831        }
832    }
833
834    let failure = crate::PackageReadFailure::new(
835        request.spec().clone(),
836        crate::PackageReadFailureReason::NotFound,
837    );
838    Ok(PackageRead::Unavailable(UnavailablePackageRead {
839        spec: request.spec().clone(),
840        failure,
841    }))
842}
843
844#[derive(Clone, Copy)]
845enum ArchiveSource {
846    Cache,
847    Registry,
848}
849
850#[allow(clippy::result_large_err)]
851async fn read_archive_candidate<R: OperatorResolver + ?Sized>(
852    resolved: &mut ResolvedOperators<'_, R>,
853    spec: &PackageSpec,
854    configured_source: &Location,
855    candidate_location: &Location,
856    archive_source: ArchiveSource,
857    ceiling: u64,
858) -> Result<Option<Vec<u8>>, PackageReadError> {
859    debug_assert!(candidate_location.require_object().is_ok());
860    let operator = resolved
861        .resolve(candidate_location.binding())
862        .map_err(|source| {
863            PackageReadError::from_archive(
864                spec,
865                configured_source.clone(),
866                candidate_location.clone(),
867                PackageReadErrorCause::ResolveOperator(Box::new(source)),
868            )
869        })?;
870    if !operator.read {
871        return Err(PackageReadError::from_archive(
872            spec,
873            configured_source.clone(),
874            candidate_location.clone(),
875            PackageReadErrorCause::UnsupportedArchiveRead,
876        ));
877    }
878
879    read_exact_path(
880        &operator.operator,
881        candidate_location.dispatch_path(),
882        ceiling,
883        ceiling,
884        &PackageArchiveExactPathOperation {
885            spec,
886            configured_source,
887            candidate_location,
888            archive_source,
889        },
890    )
891    .await
892}
893
894struct PackageArchiveExactPathOperation<'a> {
895    spec: &'a PackageSpec,
896    configured_source: &'a Location,
897    candidate_location: &'a Location,
898    archive_source: ArchiveSource,
899}
900
901impl PackageArchiveExactPathOperation<'_> {
902    fn error(&self, cause: PackageReadErrorCause) -> PackageReadError {
903        PackageReadError::from_archive(
904            self.spec,
905            self.configured_source.clone(),
906            self.candidate_location.clone(),
907            cause,
908        )
909    }
910}
911
912impl ExactPathReadOperation for PackageArchiveExactPathOperation<'_> {
913    type Error = PackageReadError;
914
915    fn read(&self, source: ::opendal::Error) -> PackageReadError {
916        self.error(match self.archive_source {
917            ArchiveSource::Cache => PackageReadErrorCause::CacheRead(source),
918            ArchiveSource::Registry => PackageReadErrorCause::RegistryRead(source),
919        })
920    }
921
922    fn limit_exceeded(&self, ceiling: u64, _: u64) -> PackageReadError {
923        self.error(PackageReadErrorCause::ArchiveLimit(
924            PackageArchiveReadLimitError::exceeded(
925                PackageArchiveReadResource::ArchiveBytes,
926                ceiling,
927            ),
928        ))
929    }
930
931    fn accounting_overflow(&self) -> PackageReadError {
932        self.error(PackageReadErrorCause::ArchiveLimit(
933            PackageArchiveReadLimitError::AccountingOverflow {
934                resource: PackageArchiveReadResource::ArchiveBytes,
935            },
936        ))
937    }
938}
939
940/// A terminal failure while reading one package through OpenDAL.
941#[derive(Debug, thiserror::Error)]
942#[error(
943    "Package Read failed for {spec}{tree_source}{candidate}: {cause}",
944    tree_source = package_tree_source_context(.source_index),
945    candidate = package_candidate_context(.candidate_location.as_ref())
946)]
947pub struct PackageReadError {
948    spec: PackageSpec,
949    source_index: Option<usize>,
950    configured_source: Option<Location>,
951    candidate_location: Option<Location>,
952    failed_path: Option<String>,
953    failure: crate::PackageReadFailure,
954    #[source]
955    cause: RedactedError<PackageReadErrorCause>,
956}
957
958impl PackageReadError {
959    /// The exact package specification whose read failed.
960    pub fn spec(&self) -> &PackageSpec {
961        &self.spec
962    }
963
964    /// The caller-order tree source index, when failure occurred at a tree.
965    pub fn source_index(&self) -> Option<usize> {
966        self.source_index
967    }
968
969    /// The configured source reached when failure occurred.
970    pub fn configured_source(&self) -> Option<&Location> {
971        self.configured_source.as_ref()
972    }
973
974    /// The specification-derived candidate reached when available.
975    pub fn candidate_location(&self) -> Option<&Location> {
976        self.candidate_location.as_ref()
977    }
978
979    /// The listed tree object that failed to read, when applicable.
980    pub fn failed_path(&self) -> Option<&str> {
981        self.failed_path.as_deref()
982    }
983
984    /// The stable failure suitable for resumed Pack Creation.
985    pub fn failure(&self) -> &crate::PackageReadFailure {
986        &self.failure
987    }
988
989    /// The stable Package Read Failure reason.
990    pub fn reason(&self) -> &crate::PackageReadFailureReason {
991        self.failure.reason()
992    }
993
994    /// The typed adapter cause retained by this failure.
995    pub fn cause(&self) -> &PackageReadErrorCause {
996        self.cause.inner()
997    }
998
999    fn from_tree(
1000        spec: &PackageSpec,
1001        sources: &[PackageTreeSource],
1002        child: &str,
1003        source_index: usize,
1004        failed_path: Option<String>,
1005        cause: PackageReadErrorCause,
1006    ) -> Self {
1007        let configured_source = sources[source_index].source.clone();
1008        let candidate_location = configured_source
1009            .require_prefix()
1010            .is_ok()
1011            .then(|| compose_candidate(&configured_source, child));
1012        Self {
1013            spec: spec.clone(),
1014            source_index: Some(source_index),
1015            configured_source: Some(configured_source),
1016            candidate_location,
1017            failed_path,
1018            failure: other_failure(spec),
1019            cause: RedactedError::new(cause),
1020        }
1021    }
1022
1023    fn from_archive(
1024        spec: &PackageSpec,
1025        configured_source: Location,
1026        candidate_location: Location,
1027        cause: PackageReadErrorCause,
1028    ) -> Self {
1029        Self {
1030            spec: spec.clone(),
1031            source_index: None,
1032            configured_source: Some(configured_source),
1033            candidate_location: Some(candidate_location),
1034            failed_path: None,
1035            failure: other_failure(spec),
1036            cause: RedactedError::new(cause),
1037        }
1038    }
1039}
1040
1041fn package_tree_source_context(source_index: &Option<usize>) -> String {
1042    source_index
1043        .map(|source_index| format!(" at tree source {source_index}"))
1044        .unwrap_or_default()
1045}
1046
1047fn package_candidate_context(candidate: Option<&Location>) -> String {
1048    candidate
1049        .map(|candidate| format!(" at candidate {candidate}"))
1050        .unwrap_or_default()
1051}
1052
1053/// The typed cause of a terminal OpenDAL Package Read failure.
1054#[derive(Debug, thiserror::Error)]
1055#[non_exhaustive]
1056pub enum PackageReadErrorCause {
1057    /// The reached binding could not be resolved.
1058    #[error("operator resolution failed")]
1059    ResolveOperator(#[source] BoxError),
1060    /// A reached tree binding cannot recursively list or read selected files.
1061    #[error("required Package Tree capabilities are unsupported")]
1062    UnsupportedTreeCapabilities {
1063        list: bool,
1064        list_with_recursive: bool,
1065        read: bool,
1066    },
1067    /// A reached raw archive binding cannot read objects.
1068    #[error("Package Archive read capability is unsupported")]
1069    UnsupportedArchiveRead,
1070    /// A Package Tree recursive listing failed.
1071    #[error("the Package Tree listing failed")]
1072    TreeList(#[source] ::opendal::Error),
1073    /// A listed Package Tree object read failed.
1074    #[error("a Package Tree object read failed")]
1075    TreeRead(#[source] ::opendal::Error),
1076    /// A listed Package Tree object became absent when read.
1077    #[error("a listed Package Tree object became absent")]
1078    ListedTreeObjectAbsent(#[source] ::opendal::Error),
1079    /// The raw Package Archive cache read failed.
1080    #[error("the Package Archive cache read failed")]
1081    CacheRead(#[source] ::opendal::Error),
1082    /// The official Package Registry read failed.
1083    #[error("the Package Registry read failed")]
1084    RegistryRead(#[source] ::opendal::Error),
1085    /// A completed Package Tree listing had envelope issues.
1086    #[error("the Package Tree listing had structural issues")]
1087    TreeStructural(#[source] PackageTreeReadSurveyError),
1088    /// Listed paths do not form a valid Package Tree.
1089    #[error("the listed objects do not form a Package Tree")]
1090    InvalidPackageTree(#[source] PackageTreeError),
1091    /// Package Tree Read exceeded a mandatory limit.
1092    #[error("a Package Tree Read limit failed")]
1093    TreeLimit(#[source] PackageTreeReadLimitError),
1094    /// Raw Package Archive Read exceeded a mandatory limit.
1095    #[error("a Package Archive Read limit failed")]
1096    ArchiveLimit(#[source] PackageArchiveReadLimitError),
1097}
1098
1099struct PackageTreeReadOperation<'a> {
1100    spec: &'a PackageSpec,
1101    sources: &'a [PackageTreeSource],
1102    child: &'a str,
1103}
1104
1105impl PackageTreeReadOperation<'_> {
1106    fn error(
1107        &self,
1108        source_index: usize,
1109        failed_path: Option<String>,
1110        cause: PackageReadErrorCause,
1111    ) -> PackageReadError {
1112        PackageReadError::from_tree(
1113            self.spec,
1114            self.sources,
1115            self.child,
1116            source_index,
1117            failed_path,
1118            cause,
1119        )
1120    }
1121}
1122
1123impl RecursiveReadOperation for PackageTreeReadOperation<'_> {
1124    type Error = PackageReadError;
1125
1126    fn invalid_location_role(&self, _: usize, _: LocationRoleError) -> PackageReadError {
1127        unreachable!("PackageReadRequest validates every tree prefix")
1128    }
1129
1130    fn resolve_operator(&self, source_index: usize, source: BoxError) -> PackageReadError {
1131        self.error(
1132            source_index,
1133            None,
1134            PackageReadErrorCause::ResolveOperator(source),
1135        )
1136    }
1137
1138    fn unsupported_capabilities(
1139        &self,
1140        source_index: usize,
1141        list: bool,
1142        list_with_recursive: bool,
1143        read: bool,
1144    ) -> PackageReadError {
1145        self.error(
1146            source_index,
1147            None,
1148            PackageReadErrorCause::UnsupportedTreeCapabilities {
1149                list,
1150                list_with_recursive,
1151                read,
1152            },
1153        )
1154    }
1155
1156    fn list(&self, source_index: usize, source: ::opendal::Error) -> PackageReadError {
1157        self.error(source_index, None, PackageReadErrorCause::TreeList(source))
1158    }
1159
1160    fn read(
1161        &self,
1162        source_index: usize,
1163        operation_path: String,
1164        source: ::opendal::Error,
1165    ) -> PackageReadError {
1166        self.error(
1167            source_index,
1168            Some(operation_path),
1169            PackageReadErrorCause::TreeRead(source),
1170        )
1171    }
1172
1173    fn listed_object_absent(
1174        &self,
1175        source_index: usize,
1176        operation_path: String,
1177        source: ::opendal::Error,
1178    ) -> PackageReadError {
1179        self.error(
1180            source_index,
1181            Some(operation_path),
1182            PackageReadErrorCause::ListedTreeObjectAbsent(source),
1183        )
1184    }
1185
1186    fn structural(
1187        &self,
1188        source_index: usize,
1189        issues: Vec<RecursiveSurveyIssue>,
1190    ) -> PackageReadError {
1191        self.error(
1192            source_index,
1193            None,
1194            PackageReadErrorCause::TreeStructural(PackageTreeReadSurveyError {
1195                issues: issues.into_iter().map(map_issue).collect(),
1196            }),
1197        )
1198    }
1199
1200    fn limit(
1201        &self,
1202        source_index: usize,
1203        resource: RecursiveReadResource,
1204        ceiling: u64,
1205        _: u64,
1206    ) -> PackageReadError {
1207        self.error(
1208            source_index,
1209            None,
1210            PackageReadErrorCause::TreeLimit(PackageTreeReadLimitError::exceeded(
1211                map_resource(resource),
1212                ceiling,
1213            )),
1214        )
1215    }
1216
1217    fn accounting_overflow(
1218        &self,
1219        source_index: usize,
1220        resource: RecursiveReadResource,
1221    ) -> PackageReadError {
1222        self.error(
1223            source_index,
1224            None,
1225            PackageReadErrorCause::TreeLimit(PackageTreeReadLimitError::AccountingOverflow {
1226                resource: map_resource(resource),
1227            }),
1228        )
1229    }
1230}
1231
1232impl PackageTreeRecursiveReadOperation for PackageTreeReadOperation<'_> {
1233    fn invalid_package_tree(
1234        &self,
1235        source_index: usize,
1236        source: PackageTreeError,
1237    ) -> PackageReadError {
1238        self.error(
1239            source_index,
1240            None,
1241            PackageReadErrorCause::InvalidPackageTree(source),
1242        )
1243    }
1244}
1245
1246fn other_failure(spec: &PackageSpec) -> crate::PackageReadFailure {
1247    crate::PackageReadFailure::new(
1248        spec.clone(),
1249        crate::PackageReadFailureReason::Other { detail: None },
1250    )
1251}
1252
1253/// Exact registry bytes retained after successful validation and insertion.
1254#[cfg(feature = "package-reading")]
1255pub struct RegistryArchiveResidue {
1256    spec: PackageSpec,
1257    destination: Location,
1258    bytes: Vec<u8>,
1259}
1260
1261#[cfg(feature = "package-reading")]
1262impl RegistryArchiveResidue {
1263    /// The exact package specification validated and inserted.
1264    pub fn spec(&self) -> &PackageSpec {
1265        &self.spec
1266    }
1267
1268    /// The derived exact cache object for optional write.
1269    pub fn destination(&self) -> &Location {
1270        &self.destination
1271    }
1272
1273    /// The exact original registry bytes.
1274    pub fn bytes(&self) -> &[u8] {
1275        &self.bytes
1276    }
1277
1278    /// The exact raw archive byte length.
1279    pub fn len(&self) -> u64 {
1280        self.bytes.len() as u64
1281    }
1282
1283    /// Whether the original registry object was empty.
1284    pub fn is_empty(&self) -> bool {
1285        self.bytes.is_empty()
1286    }
1287
1288    /// Recovers the specification, cache destination, and exact registry bytes.
1289    pub fn into_parts(self) -> (PackageSpec, Location, Vec<u8>) {
1290        (self.spec, self.destination, self.bytes)
1291    }
1292}
1293
1294#[cfg(feature = "package-reading")]
1295impl fmt::Debug for RegistryArchiveResidue {
1296    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1297        formatter
1298            .debug_struct("RegistryArchiveResidue")
1299            .field("spec", &self.spec)
1300            .field("destination", &self.destination)
1301            .field("byte_length", &self.bytes.len())
1302            .finish()
1303    }
1304}
1305
1306/// The stage at which an read package could not be inserted.
1307#[derive(Clone, Debug, Eq, PartialEq)]
1308#[non_exhaustive]
1309#[cfg(feature = "package-reading")]
1310pub enum ReadPackageInsertionTarget {
1311    /// Construction of an read Package Tree failed.
1312    PackageTree,
1313    /// Expansion of cached raw archive bytes failed.
1314    CachedArchive,
1315    /// Expansion of registry raw archive bytes failed.
1316    RegistryArchive,
1317    /// Package Catalog validation or insertion failed.
1318    PackageCatalog,
1319}
1320
1321/// A failure while converting raw read into Pack Creation inputs.
1322#[cfg(feature = "package-reading")]
1323#[derive(Debug, thiserror::Error)]
1324#[error(
1325    "failed to insert read package {spec} at {target:?}",
1326    spec = .failure.spec()
1327)]
1328pub struct ReadPackageInsertionError {
1329    failure: Box<crate::PackageReadFailure>,
1330    target: ReadPackageInsertionTarget,
1331    #[source]
1332    cause: Box<ReadPackageInsertionErrorCause>,
1333}
1334
1335#[cfg(feature = "package-reading")]
1336impl ReadPackageInsertionError {
1337    /// The exact package specification that could not be inserted.
1338    pub fn spec(&self) -> &PackageSpec {
1339        self.failure.spec()
1340    }
1341
1342    /// The stable failure recorded for resumed Pack Creation.
1343    pub fn failure(&self) -> &crate::PackageReadFailure {
1344        &self.failure
1345    }
1346
1347    /// The stable Package Read Failure reason.
1348    pub fn reason(&self) -> &crate::PackageReadFailureReason {
1349        self.failure.reason()
1350    }
1351
1352    /// The insertion stage that failed.
1353    pub fn target(&self) -> &ReadPackageInsertionTarget {
1354        &self.target
1355    }
1356
1357    /// The typed core cause retained by this failure.
1358    pub fn cause(&self) -> &ReadPackageInsertionErrorCause {
1359        &self.cause
1360    }
1361}
1362
1363/// The typed cause of an read-package insertion failure.
1364#[derive(Debug, thiserror::Error)]
1365#[non_exhaustive]
1366#[cfg(feature = "package-reading")]
1367pub enum ReadPackageInsertionErrorCause {
1368    /// Read entries could not construct a Package Tree.
1369    #[error("read entries could not construct a Package Tree")]
1370    PackageTree(#[source] crate::PackageTreeError),
1371    /// Raw archive bytes could not expand into a Package Tree.
1372    #[error("raw archive bytes could not expand into a Package Tree")]
1373    ArchiveExpansion(#[source] Box<crate::PackageReadError>),
1374    /// The Package Catalog rejected the constructed tree.
1375    #[error("the Package Catalog rejected the constructed tree")]
1376    PackageCatalog(#[source] crate::PackageCatalogError),
1377}
1378
1379/// Expands or constructs an read package, inserts it, and updates failures.
1380///
1381/// Registry residue is returned only after expansion, validation, and catalog
1382/// insertion succeed. Writing those exact bytes is a separate low-level
1383/// operation; writing before this function succeeds can poison a cache with
1384/// terminal malformed bytes.
1385#[cfg(feature = "package-reading")]
1386#[allow(unreachable_patterns)]
1387pub fn insert_read_package(
1388    catalog: &mut crate::PackageCatalog,
1389    failures: &mut crate::PackageReadFailures,
1390    read: PackageRead,
1391    disposition: crate::PackageDisposition,
1392    expansion_limits: crate::PackageExpansionLimits,
1393) -> Result<Option<RegistryArchiveResidue>, ReadPackageInsertionError> {
1394    let (spec, tree, residue) = match read {
1395        PackageRead::Tree(read) => {
1396            let (spec, _, _, _, entries) = read.into_parts();
1397            let tree = crate::PackageTree::from_owned_entries(
1398                entries.into_iter().map(PackageTreeReadEntry::into_parts),
1399            )
1400            .map_err(|source| {
1401                insertion_error(
1402                    &spec,
1403                    ReadPackageInsertionTarget::PackageTree,
1404                    ReadPackageInsertionErrorCause::PackageTree(source),
1405                    crate::PackageReadFailureReason::Other { detail: None },
1406                    failures,
1407                )
1408            })?;
1409            (spec, tree, None)
1410        }
1411        PackageRead::CachedArchive(read) => {
1412            let (spec, _, _, bytes) = read.into_parts();
1413            let tree = expand_read_archive(
1414                &spec,
1415                &bytes,
1416                ReadPackageInsertionTarget::CachedArchive,
1417                expansion_limits,
1418                failures,
1419            )?;
1420            (spec, tree, None)
1421        }
1422        PackageRead::RegistryArchive(read) => {
1423            let (spec, _, _, destination, bytes) = read.into_parts();
1424            let tree = expand_read_archive(
1425                &spec,
1426                &bytes,
1427                ReadPackageInsertionTarget::RegistryArchive,
1428                expansion_limits,
1429                failures,
1430            )?;
1431            let residue = destination.map(|destination| RegistryArchiveResidue {
1432                spec: spec.clone(),
1433                destination,
1434                bytes,
1435            });
1436            (spec, tree, residue)
1437        }
1438        PackageRead::Unavailable(read) => {
1439            let (_, failure) = read.into_parts();
1440            failures.insert(failure);
1441            return Ok(None);
1442        }
1443        _ => unreachable!("future Package Read outcomes require explicit composition"),
1444    };
1445
1446    catalog
1447        .insert(spec.clone(), tree, disposition)
1448        .map_err(|source| {
1449            insertion_error(
1450                &spec,
1451                ReadPackageInsertionTarget::PackageCatalog,
1452                ReadPackageInsertionErrorCause::PackageCatalog(source),
1453                crate::PackageReadFailureReason::Other { detail: None },
1454                failures,
1455            )
1456        })?;
1457    failures.remove(&spec);
1458    Ok(residue)
1459}
1460
1461#[cfg(feature = "package-reading")]
1462fn expand_read_archive(
1463    spec: &PackageSpec,
1464    bytes: &[u8],
1465    target: ReadPackageInsertionTarget,
1466    limits: crate::PackageExpansionLimits,
1467    failures: &mut crate::PackageReadFailures,
1468) -> Result<crate::PackageTree, ReadPackageInsertionError> {
1469    crate::expand_package_archive(spec.clone(), bytes, limits).map_err(|source| {
1470        let reason = match &source {
1471            crate::PackageReadError::MalformedArchive { .. }
1472            | crate::PackageReadError::InvalidPackageTree { .. } => {
1473                crate::PackageReadFailureReason::MalformedArchive { detail: None }
1474            }
1475            crate::PackageReadError::UnservedNamespace { .. }
1476            | crate::PackageReadError::ExpansionLimit { .. } => {
1477                crate::PackageReadFailureReason::Other { detail: None }
1478            }
1479        };
1480        insertion_error(
1481            spec,
1482            target,
1483            ReadPackageInsertionErrorCause::ArchiveExpansion(Box::new(source)),
1484            reason,
1485            failures,
1486        )
1487    })
1488}
1489
1490#[cfg(feature = "package-reading")]
1491fn insertion_error(
1492    spec: &PackageSpec,
1493    target: ReadPackageInsertionTarget,
1494    cause: ReadPackageInsertionErrorCause,
1495    reason: crate::PackageReadFailureReason,
1496    failures: &mut crate::PackageReadFailures,
1497) -> ReadPackageInsertionError {
1498    let failure = crate::PackageReadFailure::new(spec.clone(), reason);
1499    failures.insert(failure.clone());
1500    ReadPackageInsertionError {
1501        failure: Box::new(failure),
1502        target,
1503        cause: Box::new(cause),
1504    }
1505}
1506
1507/// An unsupported entry kind yielded by a Package Tree listing.
1508#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1509#[non_exhaustive]
1510pub enum PackageTreeReadEntryKind {
1511    /// OpenDAL did not classify the entry as a file or directory.
1512    Unknown,
1513}
1514
1515/// One storage-envelope issue found during a Package Tree survey.
1516#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
1517#[non_exhaustive]
1518pub enum PackageTreeReadIssue {
1519    /// A yielded path is outside the candidate prefix.
1520    #[error("listed operation path {operation_path:?} is outside the Package Tree prefix")]
1521    ListedPathOutsidePrefix { operation_path: String },
1522    /// A yielded file names the candidate prefix itself or ends in a separator.
1523    #[error("listed operation path {operation_path:?} is a prefix marker where a file is required")]
1524    PrefixMarkerWhereFileRequired { operation_path: String },
1525    /// A yielded path has no package-relative component.
1526    #[error("listed operation path {operation_path:?} has an empty relative path")]
1527    EmptyRelativeOperationPath { operation_path: String },
1528    /// A yielded entry has an unsupported kind.
1529    #[error("listed operation path {operation_path:?} has unsupported kind {kind:?}")]
1530    UnsupportedEntryKind {
1531        operation_path: String,
1532        kind: PackageTreeReadEntryKind,
1533    },
1534}
1535
1536/// The nonempty canonical set of Package Tree survey envelope issues.
1537#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
1538#[error("{message}", message = package_tree_survey_message(.issues.as_slice()))]
1539pub struct PackageTreeReadSurveyError {
1540    issues: Vec<PackageTreeReadIssue>,
1541}
1542
1543impl PackageTreeReadSurveyError {
1544    /// Every independently detectable issue in canonical path and kind order.
1545    pub fn issues(&self) -> &[PackageTreeReadIssue] {
1546        &self.issues
1547    }
1548}
1549
1550fn package_tree_survey_message(issues: &[PackageTreeReadIssue]) -> String {
1551    if let [issue] = issues {
1552        issue.to_string()
1553    } else {
1554        format!("Package Tree survey failed with {} issue(s)", issues.len())
1555    }
1556}
1557
1558#[cfg(test)]
1559#[allow(clippy::result_large_err)]
1560pub(crate) async fn read_package_tree_candidates<R: OperatorResolver + ?Sized>(
1561    resolver: &R,
1562    spec: &PackageSpec,
1563    sources: &[PackageTreeSource],
1564    limits: PackageTreeReadLimits,
1565) -> Result<Option<PackageTreeRead>, PackageReadError> {
1566    let mut resolved = ResolvedOperators::new(resolver);
1567    read_package_tree_candidates_with_resolved(&mut resolved, spec, sources, limits).await
1568}
1569
1570#[allow(clippy::result_large_err)]
1571async fn read_package_tree_candidates_with_resolved<R: OperatorResolver + ?Sized>(
1572    resolved: &mut ResolvedOperators<'_, R>,
1573    spec: &PackageSpec,
1574    sources: &[PackageTreeSource],
1575    limits: PackageTreeReadLimits,
1576) -> Result<Option<PackageTreeRead>, PackageReadError> {
1577    let child = format!("{}/", read_layout::package_tree_key(spec));
1578    let candidates = sources
1579        .iter()
1580        .map(|source| {
1581            source.source.require_prefix()?;
1582            Ok(compose_candidate(&source.source, &child))
1583        })
1584        .collect::<Vec<_>>();
1585    let Some((source_index, candidate_location, objects)) =
1586        read_first_present_package_tree_prefix_with_resolved(
1587            resolved,
1588            candidates,
1589            limits.into(),
1590            &PackageTreeReadOperation {
1591                spec,
1592                sources,
1593                child: &child,
1594            },
1595        )
1596        .await?
1597    else {
1598        return Ok(None);
1599    };
1600
1601    Ok(Some(PackageTreeRead {
1602        spec: spec.clone(),
1603        source_index,
1604        configured_source: sources[source_index].source.clone(),
1605        candidate_location,
1606        entries: objects
1607            .into_iter()
1608            .map(|object| PackageTreeReadEntry {
1609                relative_path: object.relative_path,
1610                bytes: object.bytes,
1611            })
1612            .collect(),
1613    }))
1614}
1615
1616fn compose_candidate(source: &Location, child: &str) -> Location {
1617    source
1618        .compose(child)
1619        .expect("a package key composed below a canonical prefix remains canonical")
1620}
1621
1622impl From<PackageTreeReadLimits> for RecursiveReadLimits {
1623    fn from(limits: PackageTreeReadLimits) -> Self {
1624        Self::new(
1625            limits.listed_entries(),
1626            limits.listed_path_bytes(),
1627            limits.total_listed_path_bytes(),
1628            limits.selected_files(),
1629            limits.object_bytes(),
1630            limits.total_bytes(),
1631        )
1632    }
1633}
1634
1635fn map_resource(resource: RecursiveReadResource) -> PackageTreeReadResource {
1636    match resource {
1637        RecursiveReadResource::ListedEntries => PackageTreeReadResource::ListedEntries,
1638        RecursiveReadResource::ListedPathBytes => PackageTreeReadResource::ListedPathBytes,
1639        RecursiveReadResource::TotalListedPathBytes => {
1640            PackageTreeReadResource::TotalListedPathBytes
1641        }
1642        RecursiveReadResource::SelectedObjects => PackageTreeReadResource::SelectedFiles,
1643        RecursiveReadResource::ObjectBytes => PackageTreeReadResource::ObjectBytes,
1644        RecursiveReadResource::TotalBytes => PackageTreeReadResource::TotalBytes,
1645        _ => unreachable!("unknown recursive read resource"),
1646    }
1647}
1648
1649fn map_issue(issue: RecursiveSurveyIssue) -> PackageTreeReadIssue {
1650    let operation_path = issue.operation_path;
1651    match issue.kind {
1652        RecursiveSurveyIssueKind::ListedPathOutsidePrefix => {
1653            PackageTreeReadIssue::ListedPathOutsidePrefix { operation_path }
1654        }
1655        RecursiveSurveyIssueKind::PrefixMarkerWhereFileRequired => {
1656            PackageTreeReadIssue::PrefixMarkerWhereFileRequired { operation_path }
1657        }
1658        RecursiveSurveyIssueKind::EmptyRelativeOperationPath => {
1659            PackageTreeReadIssue::EmptyRelativeOperationPath { operation_path }
1660        }
1661        RecursiveSurveyIssueKind::UnsupportedEntryKind => {
1662            PackageTreeReadIssue::UnsupportedEntryKind {
1663                operation_path,
1664                kind: PackageTreeReadEntryKind::Unknown,
1665            }
1666        }
1667        RecursiveSurveyIssueKind::InvalidRelativeOperationPath
1668        | RecursiveSurveyIssueKind::DuplicateListedObject => {
1669            unreachable!("Package Tree path issues are owned by core preflight")
1670        }
1671    }
1672}
1673
1674#[cfg(test)]
1675mod tests {
1676    use std::cell::Cell;
1677    use std::convert::Infallible;
1678    use std::future::Future;
1679    use std::pin::pin;
1680    use std::task::{Context, Poll, Waker};
1681
1682    use opendal::ErrorKind;
1683    use typst::syntax::package::PackageSpec;
1684
1685    use crate::opendal::scripted_service::{
1686        Capabilities, DroppedOperation, ListEntry, ListScript, ListStep, OperationLogEntry,
1687        PendingPoint, ReadScript, ReadStep, ScriptedService,
1688    };
1689    use crate::opendal::{Location, OperatorBinding, OperatorBindings, OperatorResolver};
1690    use crate::{PackageTree, PackageTreeIssue};
1691
1692    use super::{
1693        PackageReadErrorCause, PackageTreeReadCeilings, PackageTreeReadLimitError,
1694        PackageTreeReadLimits, PackageTreeReadResource, PackageTreeSource,
1695        read_package_tree_candidates,
1696    };
1697
1698    #[test]
1699    fn empty_candidate_falls_through_and_present_candidate_stops_fallback() {
1700        let service = ScriptedService::new(
1701            Capabilities::all(),
1702            [
1703                ListScript::new("first/preview/example/1.2.3/", 0, []).unwrap(),
1704                ListScript::new(
1705                    "second/preview/example/1.2.3/",
1706                    2,
1707                    [ListStep::page([
1708                        ListEntry::file("second/preview/example/1.2.3/z.typ"),
1709                        ListEntry::file("second/preview/example/1.2.3/a.typ"),
1710                    ])],
1711                )
1712                .unwrap(),
1713            ],
1714            [
1715                ReadScript::new(
1716                    "second/preview/example/1.2.3/a.typ",
1717                    1,
1718                    [ReadStep::chunk(b"a")],
1719                )
1720                .unwrap(),
1721                ReadScript::new(
1722                    "second/preview/example/1.2.3/z.typ",
1723                    1,
1724                    [ReadStep::chunk(b"z")],
1725                )
1726                .unwrap(),
1727            ],
1728            16,
1729        );
1730        let binding = OperatorBinding::new("trees").unwrap();
1731        let resolver = CountingResolver::new(service.operator());
1732        let sources = [
1733            PackageTreeSource::new(
1734                Location::from_operation_path(binding.clone(), "first/").unwrap(),
1735            ),
1736            PackageTreeSource::new(Location::from_operation_path(binding, "second/").unwrap()),
1737            PackageTreeSource::new("unreached:/not-a-prefix".parse().unwrap()),
1738        ];
1739
1740        let read = expect_ready(pin!(read_package_tree_candidates(
1741            &resolver,
1742            &"@preview/example:1.2.3".parse().unwrap(),
1743            &sources,
1744            PackageTreeReadLimits::reference_v1(),
1745        )))
1746        .unwrap()
1747        .unwrap();
1748
1749        assert_eq!(read.source_index(), 1);
1750        assert_eq!(read.configured_source(), sources[1].source());
1751        assert_eq!(
1752            read.candidate_location().operation_path(),
1753            "second/preview/example/1.2.3/"
1754        );
1755        assert_eq!(
1756            read.entries()
1757                .iter()
1758                .map(|entry| (entry.relative_path(), entry.bytes()))
1759                .collect::<Vec<_>>(),
1760            [("a.typ", b"a".as_slice()), ("z.typ", b"z".as_slice())]
1761        );
1762        assert_eq!(
1763            service
1764                .log()
1765                .entries()
1766                .iter()
1767                .filter(|entry| matches!(entry, OperationLogEntry::ListInvoked { .. }))
1768                .count(),
1769            2
1770        );
1771        assert_eq!(resolver.calls(), 1);
1772    }
1773
1774    #[test]
1775    fn named_limits_keep_the_finite_reference_profile_and_validate_payload_ceilings() {
1776        let reference = PackageTreeReadCeilings::reference_v1();
1777        assert_eq!(reference.listed_entries, 100_000);
1778        assert_eq!(reference.listed_path_bytes, 64 * 1024);
1779        assert_eq!(reference.total_listed_path_bytes, 64 * 1024 * 1024);
1780        assert_eq!(reference.selected_files, 50_000);
1781        assert_eq!(reference.object_bytes, 64 * 1024 * 1024);
1782        assert_eq!(reference.total_bytes, 512 * 1024 * 1024);
1783
1784        let narrowed = PackageTreeReadLimits::new(PackageTreeReadCeilings {
1785            listed_entries: u64::MAX - 1,
1786            listed_path_bytes: u64::MAX - 1,
1787            total_listed_path_bytes: u64::MAX - 1,
1788            total_bytes: reference.object_bytes,
1789            ..reference
1790        });
1791        assert_eq!(narrowed.listed_entries(), u64::MAX - 1);
1792        assert_eq!(narrowed.listed_path_bytes(), u64::MAX - 1);
1793        assert_eq!(narrowed.total_listed_path_bytes(), u64::MAX - 1);
1794        assert_eq!(narrowed.selected_files(), reference.selected_files);
1795        assert_eq!(narrowed.object_bytes(), reference.object_bytes);
1796        assert_eq!(narrowed.total_bytes(), reference.object_bytes);
1797
1798        for ceilings in [
1799            PackageTreeReadCeilings {
1800                listed_entries: u64::MAX,
1801                ..reference
1802            },
1803            PackageTreeReadCeilings {
1804                listed_path_bytes: u64::MAX,
1805                ..reference
1806            },
1807            PackageTreeReadCeilings {
1808                total_listed_path_bytes: u64::MAX,
1809                ..reference
1810            },
1811            PackageTreeReadCeilings {
1812                selected_files: u64::MAX,
1813                ..reference
1814            },
1815            PackageTreeReadCeilings {
1816                object_bytes: u64::MAX,
1817                total_bytes: u64::MAX,
1818                ..reference
1819            },
1820            PackageTreeReadCeilings {
1821                total_bytes: u64::MAX,
1822                ..reference
1823            },
1824        ] {
1825            assert!(std::panic::catch_unwind(|| PackageTreeReadLimits::new(ceilings)).is_err());
1826        }
1827        assert!(
1828            std::panic::catch_unwind(|| PackageTreeReadLimits::new(PackageTreeReadCeilings {
1829                object_bytes: 2,
1830                total_bytes: 1,
1831                ..reference
1832            }))
1833            .is_err()
1834        );
1835    }
1836
1837    #[test]
1838    fn tree_resources_map_shared_survey_and_payload_boundaries() {
1839        let reference = PackageTreeReadCeilings::reference_v1();
1840        let survey_cases = [
1841            (
1842                PackageTreeReadResource::ListedEntries,
1843                PackageTreeReadCeilings {
1844                    listed_entries: 0,
1845                    ..reference
1846                },
1847                ListEntry::directory("trees/preview/example/1.2.3/dir/"),
1848            ),
1849            (
1850                PackageTreeReadResource::ListedPathBytes,
1851                PackageTreeReadCeilings {
1852                    listed_path_bytes: 1,
1853                    ..reference
1854                },
1855                ListEntry::directory("trees/preview/example/1.2.3/dir/"),
1856            ),
1857            (
1858                PackageTreeReadResource::TotalListedPathBytes,
1859                PackageTreeReadCeilings {
1860                    total_listed_path_bytes: 0,
1861                    ..reference
1862                },
1863                ListEntry::file("trees/preview/example/1.2.3/a.typ"),
1864            ),
1865            (
1866                PackageTreeReadResource::SelectedFiles,
1867                PackageTreeReadCeilings {
1868                    selected_files: 0,
1869                    ..reference
1870                },
1871                ListEntry::file("trees/preview/example/1.2.3/a.typ"),
1872            ),
1873        ];
1874        for (resource, ceilings, entry) in survey_cases {
1875            let service = ScriptedService::new(
1876                Capabilities::all(),
1877                [
1878                    ListScript::new("trees/preview/example/1.2.3/", 1, [ListStep::page([entry])])
1879                        .unwrap(),
1880                ],
1881                [],
1882                8,
1883            );
1884            let bindings = configured(&service);
1885            let error = expect_ready(pin!(read_package_tree_candidates(
1886                &bindings,
1887                &spec(),
1888                &[source("trees/")],
1889                PackageTreeReadLimits::new(ceilings),
1890            )))
1891            .unwrap_err();
1892            assert!(matches!(
1893                error.cause(),
1894                PackageReadErrorCause::TreeLimit(
1895                    PackageTreeReadLimitError::Exceeded {
1896                        resource: actual,
1897                        ..
1898                    }
1899                ) if *actual == resource
1900            ));
1901        }
1902
1903        let service = ScriptedService::new(
1904            Capabilities::all(),
1905            [ListScript::new(
1906                "trees/preview/example/1.2.3/",
1907                1,
1908                [ListStep::page([ListEntry::file(
1909                    "trees/preview/example/1.2.3/a.typ",
1910                )])],
1911            )
1912            .unwrap()],
1913            [ReadScript::new(
1914                "trees/preview/example/1.2.3/a.typ",
1915                1,
1916                [ReadStep::chunk(b"four")],
1917            )
1918            .unwrap()],
1919            8,
1920        );
1921        let bindings = configured(&service);
1922        let error = expect_ready(pin!(read_package_tree_candidates(
1923            &bindings,
1924            &spec(),
1925            &[source("trees/")],
1926            PackageTreeReadLimits::new(PackageTreeReadCeilings {
1927                object_bytes: 3,
1928                total_bytes: 8,
1929                ..reference
1930            }),
1931        )))
1932        .unwrap_err();
1933        assert!(matches!(
1934            error.cause(),
1935            PackageReadErrorCause::TreeLimit(PackageTreeReadLimitError::Exceeded {
1936                resource: PackageTreeReadResource::ObjectBytes,
1937                ceiling: 3,
1938                observed_at_least: 4,
1939            })
1940        ));
1941
1942        let service = ScriptedService::new(
1943            Capabilities::all(),
1944            [ListScript::new(
1945                "trees/preview/example/1.2.3/",
1946                2,
1947                [ListStep::page([
1948                    ListEntry::file("trees/preview/example/1.2.3/a.typ"),
1949                    ListEntry::file("trees/preview/example/1.2.3/b.typ"),
1950                ])],
1951            )
1952            .unwrap()],
1953            [
1954                ReadScript::new(
1955                    "trees/preview/example/1.2.3/a.typ",
1956                    1,
1957                    [ReadStep::chunk(b"12")],
1958                )
1959                .unwrap(),
1960                ReadScript::new(
1961                    "trees/preview/example/1.2.3/b.typ",
1962                    1,
1963                    [ReadStep::chunk(b"34")],
1964                )
1965                .unwrap(),
1966            ],
1967            12,
1968        );
1969        let bindings = configured(&service);
1970        let error = expect_ready(pin!(read_package_tree_candidates(
1971            &bindings,
1972            &spec(),
1973            &[source("trees/")],
1974            PackageTreeReadLimits::new(PackageTreeReadCeilings {
1975                object_bytes: 3,
1976                total_bytes: 3,
1977                ..reference
1978            }),
1979        )))
1980        .unwrap_err();
1981        assert!(matches!(
1982            error.cause(),
1983            PackageReadErrorCause::TreeLimit(PackageTreeReadLimitError::Exceeded {
1984                resource: PackageTreeReadResource::TotalBytes,
1985                ceiling: 3,
1986                observed_at_least: 4,
1987            })
1988        ));
1989    }
1990
1991    #[test]
1992    fn listing_limits_are_shared_across_absent_candidates() {
1993        let service = ScriptedService::new(
1994            Capabilities::all(),
1995            [
1996                ListScript::new(
1997                    "first/preview/example/1.2.3/",
1998                    1,
1999                    [ListStep::page([ListEntry::directory(
2000                        "first/preview/example/1.2.3/dir/",
2001                    )])],
2002                )
2003                .unwrap(),
2004                ListScript::new(
2005                    "second/preview/example/1.2.3/",
2006                    1,
2007                    [ListStep::page([ListEntry::directory(
2008                        "second/preview/example/1.2.3/long-directory/",
2009                    )])],
2010                )
2011                .unwrap(),
2012            ],
2013            [],
2014            8,
2015        );
2016        let bindings = configured(&service);
2017        let limits = PackageTreeReadLimits::new(PackageTreeReadCeilings {
2018            listed_entries: 1,
2019            ..PackageTreeReadCeilings::reference_v1()
2020        });
2021
2022        let error = expect_ready(pin!(read_package_tree_candidates(
2023            &bindings,
2024            &spec(),
2025            &[source("first/"), source("second/")],
2026            limits,
2027        )))
2028        .unwrap_err();
2029
2030        assert_eq!(error.source_index(), Some(1));
2031        assert!(matches!(
2032            error.cause(),
2033            PackageReadErrorCause::TreeLimit(PackageTreeReadLimitError::Exceeded {
2034                resource: PackageTreeReadResource::ListedEntries,
2035                ceiling: 1,
2036                observed_at_least: 2,
2037            })
2038        ));
2039    }
2040
2041    #[test]
2042    fn listing_permutations_preserve_canonical_order_and_exact_boundaries() {
2043        let candidate = "trees/preview/example/1.2.3/";
2044        let paths = [format!("{candidate}a"), format!("{candidate}b")];
2045        for entries in [
2046            [ListEntry::file(&paths[0]), ListEntry::file(&paths[1])],
2047            [ListEntry::file(&paths[1]), ListEntry::file(&paths[0])],
2048        ] {
2049            let service = ScriptedService::new(
2050                Capabilities::all(),
2051                [ListScript::new(candidate, 2, [ListStep::page(entries)]).unwrap()],
2052                [
2053                    ReadScript::new(&paths[0], 1, [ReadStep::chunk(b"a")]).unwrap(),
2054                    ReadScript::new(&paths[1], 1, [ReadStep::chunk(b"b")]).unwrap(),
2055                ],
2056                12,
2057            );
2058            let bindings = configured(&service);
2059            let read = expect_ready(pin!(read_package_tree_candidates(
2060                &bindings,
2061                &spec(),
2062                &[source("trees/")],
2063                PackageTreeReadLimits::reference_v1(),
2064            )))
2065            .unwrap()
2066            .unwrap();
2067            assert_eq!(
2068                read.entries()
2069                    .iter()
2070                    .map(|entry| entry.relative_path())
2071                    .collect::<Vec<_>>(),
2072                ["a", "b"]
2073            );
2074        }
2075
2076        let object = format!("{candidate}a");
2077        let service = ScriptedService::new(
2078            Capabilities::all(),
2079            [
2080                ListScript::new(candidate, 1, [ListStep::page([ListEntry::file(&object)])])
2081                    .unwrap(),
2082            ],
2083            [ReadScript::new(&object, 1, [ReadStep::chunk(b"a")]).unwrap()],
2084            8,
2085        );
2086        let exact = PackageTreeReadLimits::new(PackageTreeReadCeilings {
2087            listed_entries: 1,
2088            listed_path_bytes: 29,
2089            total_listed_path_bytes: 33,
2090            selected_files: 1,
2091            object_bytes: 1,
2092            total_bytes: 1,
2093        });
2094        let bindings = configured(&service);
2095        let read = expect_ready(pin!(read_package_tree_candidates(
2096            &bindings,
2097            &spec(),
2098            &[source("trees/")],
2099            exact,
2100        )))
2101        .unwrap()
2102        .unwrap();
2103        assert_eq!(read.entries()[0].bytes(), b"a");
2104    }
2105
2106    #[test]
2107    fn completed_empty_observations_exhaust_to_absence() {
2108        let service = ScriptedService::new(
2109            Capabilities::all(),
2110            [
2111                ListScript::new("first/preview/example/1.2.3/", 0, []).unwrap(),
2112                ListScript::new(
2113                    "second/preview/example/1.2.3/",
2114                    1,
2115                    [ListStep::page([ListEntry::directory(
2116                        "second/preview/example/1.2.3/empty/",
2117                    )])],
2118                )
2119                .unwrap(),
2120            ],
2121            [],
2122            8,
2123        );
2124        let bindings = configured(&service);
2125
2126        let read = expect_ready(pin!(read_package_tree_candidates(
2127            &bindings,
2128            &spec(),
2129            &[source("first/"), source("second/")],
2130            PackageTreeReadLimits::reference_v1(),
2131        )))
2132        .unwrap();
2133
2134        assert!(read.is_none());
2135        assert!(
2136            service
2137                .log()
2138                .entries()
2139                .iter()
2140                .all(|entry| !matches!(entry, OperationLogEntry::ReadInvoked { .. }))
2141        );
2142    }
2143
2144    #[test]
2145    fn core_preflight_canonicalizes_before_reads_and_owned_entries_build_the_final_tree() {
2146        let candidate = "trees/preview/example/1.2.3/";
2147        let service = ScriptedService::new(
2148            Capabilities::all(),
2149            [ListScript::new(
2150                candidate,
2151                2,
2152                [ListStep::page([
2153                    ListEntry::file(format!("{candidate}./lib.typ")),
2154                    ListEntry::file(format!("{candidate}empty.typ")),
2155                ])],
2156            )
2157            .unwrap()],
2158            [
2159                ReadScript::new(
2160                    format!("{candidate}./lib.typ"),
2161                    1,
2162                    [ReadStep::chunk(b"library")],
2163                )
2164                .unwrap(),
2165                ReadScript::new(format!("{candidate}empty.typ"), 0, []).unwrap(),
2166            ],
2167            12,
2168        );
2169        let bindings = configured(&service);
2170
2171        let read = expect_ready(pin!(read_package_tree_candidates(
2172            &bindings,
2173            &spec(),
2174            &[source("trees/")],
2175            PackageTreeReadLimits::reference_v1(),
2176        )))
2177        .unwrap()
2178        .unwrap();
2179        assert_eq!(read.spec(), &spec());
2180        assert_eq!(read.entries()[0].relative_path(), "empty.typ");
2181        assert_eq!(read.entries()[0].len(), 0);
2182        assert!(read.entries()[0].is_empty());
2183        assert_eq!(read.entries()[1].relative_path(), "lib.typ");
2184
2185        let (actual_spec, index, configured, candidate, entries) = read.into_parts();
2186        assert_eq!(actual_spec, spec());
2187        assert_eq!(index, 0);
2188        assert_eq!(configured.operation_path(), "trees/");
2189        assert_eq!(candidate.operation_path(), "trees/preview/example/1.2.3/");
2190        let tree = PackageTree::from_owned_entries(
2191            entries
2192                .into_iter()
2193                .map(super::PackageTreeReadEntry::into_parts),
2194        )
2195        .unwrap();
2196        assert_eq!(tree.file("empty.typ"), Some(b"".as_slice()));
2197        assert_eq!(tree.file("lib.typ"), Some(b"library".as_slice()));
2198    }
2199
2200    #[test]
2201    fn core_package_tree_conflicts_are_typed_and_terminal_before_reads() {
2202        let first = "first/preview/example/1.2.3/";
2203        let second = "second/preview/example/1.2.3/";
2204        let service = ScriptedService::new(
2205            Capabilities::all(),
2206            [
2207                ListScript::new(
2208                    first,
2209                    2,
2210                    [ListStep::page([
2211                        ListEntry::file(format!("{first}assets")),
2212                        ListEntry::file(format!("{first}assets/logo.svg")),
2213                    ])],
2214                )
2215                .unwrap(),
2216                ListScript::new(
2217                    second,
2218                    1,
2219                    [ListStep::page([ListEntry::file(format!(
2220                        "{second}unreached.typ"
2221                    ))])],
2222                )
2223                .unwrap(),
2224            ],
2225            [],
2226            12,
2227        );
2228        let bindings = configured(&service);
2229
2230        let error = expect_ready(pin!(read_package_tree_candidates(
2231            &bindings,
2232            &spec(),
2233            &[source("first/"), source("second/")],
2234            PackageTreeReadLimits::reference_v1(),
2235        )))
2236        .unwrap_err();
2237
2238        let PackageReadErrorCause::InvalidPackageTree(source) = error.cause() else {
2239            panic!("unexpected cause: {:?}", error.cause());
2240        };
2241        assert_eq!(
2242            source.issues(),
2243            [PackageTreeIssue::PathTreeConflict {
2244                ancestor: "assets".to_owned(),
2245                descendant: "assets/logo.svg".to_owned(),
2246            }]
2247        );
2248        assert_eq!(
2249            service
2250                .log()
2251                .entries()
2252                .iter()
2253                .filter(|entry| matches!(entry, OperationLogEntry::ListInvoked { .. }))
2254                .count(),
2255            1
2256        );
2257        assert!(
2258            service
2259                .log()
2260                .entries()
2261                .iter()
2262                .all(|entry| !matches!(entry, OperationLogEntry::ReadInvoked { .. }))
2263        );
2264    }
2265
2266    #[test]
2267    fn envelope_issues_are_aggregated_and_do_not_reach_lower_candidates() {
2268        let candidate = "trees/preview/example/1.2.3/";
2269        let service = ScriptedService::new(
2270            Capabilities::all(),
2271            [ListScript::new(
2272                candidate,
2273                2,
2274                [ListStep::page([
2275                    ListEntry::unknown(format!("{candidate}unknown")),
2276                    ListEntry::file("outside/file.typ"),
2277                ])],
2278            )
2279            .unwrap()],
2280            [],
2281            8,
2282        );
2283        let bindings = configured(&service);
2284
2285        let error = expect_ready(pin!(read_package_tree_candidates(
2286            &bindings,
2287            &spec(),
2288            &[source("trees/"), source("unreached/")],
2289            PackageTreeReadLimits::reference_v1(),
2290        )))
2291        .unwrap_err();
2292        let PackageReadErrorCause::TreeStructural(survey) = error.cause() else {
2293            panic!("unexpected cause: {:?}", error.cause());
2294        };
2295        assert_eq!(survey.issues().len(), 2);
2296        assert!(matches!(
2297            &survey.issues()[0],
2298            super::PackageTreeReadIssue::ListedPathOutsidePrefix { operation_path }
2299                if operation_path == "outside/file.typ"
2300        ));
2301        assert_eq!(
2302            service
2303                .log()
2304                .entries()
2305                .iter()
2306                .filter(|entry| matches!(entry, OperationLogEntry::ListInvoked { .. }))
2307                .count(),
2308            1
2309        );
2310    }
2311
2312    #[test]
2313    fn mutation_is_observed_but_disappearance_and_list_not_found_are_terminal() {
2314        let candidate = "trees/preview/example/1.2.3/";
2315        let changing = format!("{candidate}changing.typ");
2316        let replacement =
2317            ReadScript::new(&changing, 1, [ReadStep::chunk(b"bytes after listing")]).unwrap();
2318        let mutation_service = ScriptedService::new(
2319            Capabilities::all(),
2320            [ListScript::new(
2321                candidate,
2322                1,
2323                [
2324                    ListStep::page([ListEntry::file(&changing)]),
2325                    ListStep::replace_read(replacement),
2326                ],
2327            )
2328            .unwrap()],
2329            [ReadScript::new(&changing, 1, [ReadStep::chunk(b"bytes during listing")]).unwrap()],
2330            8,
2331        );
2332        let mutation_bindings = configured(&mutation_service);
2333        let read = expect_ready(pin!(read_package_tree_candidates(
2334            &mutation_bindings,
2335            &spec(),
2336            &[source("trees/")],
2337            PackageTreeReadLimits::reference_v1(),
2338        )))
2339        .unwrap()
2340        .unwrap();
2341        assert_eq!(read.entries()[0].bytes(), b"bytes after listing");
2342
2343        let absent_service = ScriptedService::new(
2344            Capabilities::all(),
2345            [ListScript::new(
2346                candidate,
2347                1,
2348                [ListStep::page([ListEntry::file(format!(
2349                    "{candidate}gone.typ"
2350                ))])],
2351            )
2352            .unwrap()],
2353            [],
2354            8,
2355        );
2356        let absent_bindings = configured(&absent_service);
2357        let absent = expect_ready(pin!(read_package_tree_candidates(
2358            &absent_bindings,
2359            &spec(),
2360            &[source("trees/"), source("unreached/")],
2361            PackageTreeReadLimits::reference_v1(),
2362        )))
2363        .unwrap_err();
2364        assert_eq!(
2365            absent.failed_path(),
2366            Some("trees/preview/example/1.2.3/gone.typ")
2367        );
2368        assert!(matches!(
2369            absent.cause(),
2370            PackageReadErrorCause::ListedTreeObjectAbsent(source)
2371                if source.kind() == ErrorKind::NotFound
2372        ));
2373
2374        let list_failure_service = ScriptedService::new(
2375            Capabilities::all(),
2376            [ListScript::new(candidate, 0, [ListStep::failure(ErrorKind::NotFound)]).unwrap()],
2377            [],
2378            4,
2379        );
2380        let list_failure_bindings = configured(&list_failure_service);
2381        let list_failure = expect_ready(pin!(read_package_tree_candidates(
2382            &list_failure_bindings,
2383            &spec(),
2384            &[source("trees/"), source("unreached/")],
2385            PackageTreeReadLimits::reference_v1(),
2386        )))
2387        .unwrap_err();
2388        assert!(matches!(
2389            list_failure.cause(),
2390            PackageReadErrorCause::TreeList(source)
2391                if source.kind() == ErrorKind::NotFound
2392        ));
2393    }
2394
2395    #[test]
2396    fn cancellation_drops_the_reached_operation_without_reaching_fallback() {
2397        let candidate = "trees/preview/example/1.2.3/";
2398        let list_pending = PendingPoint::new();
2399        let list_service = ScriptedService::new(
2400            Capabilities::all(),
2401            [ListScript::new(candidate, 0, [ListStep::pending(list_pending.clone())]).unwrap()],
2402            [],
2403            4,
2404        );
2405        let list_bindings = configured(&list_service);
2406        let sources = [source("trees/"), source("unreached/")];
2407        {
2408            let requested_spec = spec();
2409            let mut read = pin!(read_package_tree_candidates(
2410                &list_bindings,
2411                &requested_spec,
2412                &sources,
2413                PackageTreeReadLimits::reference_v1(),
2414            ));
2415            assert!(matches!(poll_once(read.as_mut()), Poll::Pending));
2416            assert!(list_pending.was_observed());
2417        }
2418        assert_eq!(
2419            list_service.cancellations(),
2420            [DroppedOperation::List {
2421                id: 0,
2422                path: candidate.to_owned(),
2423            }]
2424        );
2425
2426        let read_pending = PendingPoint::new();
2427        let object = format!("{candidate}pending.typ");
2428        let read_service = ScriptedService::new(
2429            Capabilities::all(),
2430            [
2431                ListScript::new(candidate, 1, [ListStep::page([ListEntry::file(&object)])])
2432                    .unwrap(),
2433            ],
2434            [ReadScript::new(&object, 0, [ReadStep::pending(read_pending.clone())]).unwrap()],
2435            8,
2436        );
2437        let read_bindings = configured(&read_service);
2438        {
2439            let requested_spec = spec();
2440            let mut read = pin!(read_package_tree_candidates(
2441                &read_bindings,
2442                &requested_spec,
2443                &sources,
2444                PackageTreeReadLimits::reference_v1(),
2445            ));
2446            assert!(matches!(poll_once(read.as_mut()), Poll::Pending));
2447            assert!(read_pending.was_observed());
2448        }
2449        assert_eq!(
2450            read_service.cancellations(),
2451            [DroppedOperation::Read {
2452                id: 1,
2453                path: object,
2454            }]
2455        );
2456        assert_eq!(
2457            read_service
2458                .log()
2459                .entries()
2460                .iter()
2461                .filter(|entry| matches!(entry, OperationLogEntry::ListInvoked { .. }))
2462                .count(),
2463            1
2464        );
2465    }
2466
2467    #[test]
2468    fn memory_reads_candidates_below_root_and_non_root_configured_prefixes() {
2469        for (configured, object, expected_candidate) in [
2470            (
2471                "",
2472                "preview/example/1.2.3/lib.typ",
2473                "preview/example/1.2.3/",
2474            ),
2475            (
2476                "packages/",
2477                "packages/preview/example/1.2.3/lib.typ",
2478                "packages/preview/example/1.2.3/",
2479            ),
2480        ] {
2481            let operator = opendal::Operator::new(opendal::services::Memory::default()).unwrap();
2482            expect_ready(pin!(operator.write(object, b"memory package".to_vec()))).unwrap();
2483            let binding = OperatorBinding::new("trees").unwrap();
2484            let bindings = OperatorBindings::new([(binding.clone(), operator)]).unwrap();
2485            let sources = [PackageTreeSource::new(
2486                Location::from_operation_path(binding, configured).unwrap(),
2487            )];
2488
2489            let read = expect_ready(pin!(read_package_tree_candidates(
2490                &bindings,
2491                &spec(),
2492                &sources,
2493                PackageTreeReadLimits::reference_v1(),
2494            )))
2495            .unwrap()
2496            .unwrap();
2497
2498            assert_eq!(
2499                read.candidate_location().operation_path(),
2500                expected_candidate
2501            );
2502            assert_eq!(read.entries()[0].relative_path(), "lib.typ");
2503            assert_eq!(read.entries()[0].bytes(), b"memory package");
2504        }
2505    }
2506
2507    fn expect_ready<F: Future>(mut future: std::pin::Pin<&mut F>) -> F::Output {
2508        match future
2509            .as_mut()
2510            .poll(&mut Context::from_waker(Waker::noop()))
2511        {
2512            Poll::Ready(output) => output,
2513            Poll::Pending => panic!("future unexpectedly pending"),
2514        }
2515    }
2516
2517    fn poll_once<F: Future>(future: std::pin::Pin<&mut F>) -> Poll<F::Output> {
2518        future.poll(&mut Context::from_waker(Waker::noop()))
2519    }
2520
2521    fn spec() -> PackageSpec {
2522        "@preview/example:1.2.3".parse().unwrap()
2523    }
2524
2525    fn source(path: &str) -> PackageTreeSource {
2526        PackageTreeSource::new(
2527            Location::from_operation_path(OperatorBinding::new("trees").unwrap(), path).unwrap(),
2528        )
2529    }
2530
2531    fn configured(service: &ScriptedService) -> OperatorBindings {
2532        OperatorBindings::new([(OperatorBinding::new("trees").unwrap(), service.operator())])
2533            .unwrap()
2534    }
2535
2536    struct CountingResolver {
2537        calls: Cell<usize>,
2538        operator: opendal::Operator,
2539    }
2540
2541    impl CountingResolver {
2542        fn new(operator: opendal::Operator) -> Self {
2543            Self {
2544                calls: Cell::new(0),
2545                operator,
2546            }
2547        }
2548
2549        fn calls(&self) -> usize {
2550            self.calls.get()
2551        }
2552    }
2553
2554    impl OperatorResolver for CountingResolver {
2555        type Error = Infallible;
2556
2557        fn resolve(&self, _: &OperatorBinding) -> Result<opendal::Operator, Self::Error> {
2558            self.calls.set(self.calls.get() + 1);
2559            Ok(self.operator.clone())
2560        }
2561    }
2562}