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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20pub struct PackageTreeReadCeilings {
21 pub listed_entries: u64,
23 pub listed_path_bytes: u64,
25 pub total_listed_path_bytes: u64,
27 pub selected_files: u64,
29 pub object_bytes: u64,
31 pub total_bytes: u64,
33}
34
35impl PackageTreeReadCeilings {
36 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
49pub type PackageTreeReadResource = ResourceKind<11>;
51
52#[allow(non_upper_case_globals)]
53impl ResourceKind<11> {
54 pub const ListedEntries: Self = Self::new(0);
56 pub const ListedPathBytes: Self = Self::new(1);
58 pub const TotalListedPathBytes: Self = Self::new(2);
60 pub const SelectedFiles: Self = Self::new(3);
62 pub const ObjectBytes: Self = Self::new(4);
64 pub const TotalBytes: Self = Self::new(5);
66}
67
68pub type PackageTreeReadLimits = Limits<PackageTreeReadResource>;
70
71impl Limits<PackageTreeReadResource> {
72 #[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 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 pub const fn listed_entries(&self) -> u64 {
116 self.ceilings[0]
117 }
118
119 pub const fn listed_path_bytes(&self) -> u64 {
121 self.ceilings[1]
122 }
123
124 pub const fn total_listed_path_bytes(&self) -> u64 {
126 self.ceilings[2]
127 }
128
129 pub const fn selected_files(&self) -> u64 {
131 self.ceilings[3]
132 }
133
134 pub const fn object_bytes(&self) -> u64 {
136 self.ceilings[4]
137 }
138
139 pub const fn total_bytes(&self) -> u64 {
141 self.ceilings[5]
142 }
143}
144
145pub type PackageTreeReadLimitError = LimitError<PackageTreeReadResource>;
147
148#[derive(Clone, Copy, Debug, Eq, PartialEq)]
150pub struct PackageArchiveReadCeilings {
151 pub archive_bytes: u64,
153}
154
155impl PackageArchiveReadCeilings {
156 pub const fn reference_v1() -> Self {
158 Self {
159 archive_bytes: 128 * 1024 * 1024,
160 }
161 }
162}
163
164pub type PackageArchiveReadResource = ResourceKind<12>;
166
167#[allow(non_upper_case_globals)]
168impl ResourceKind<12> {
169 pub const ArchiveBytes: Self = Self::new(0);
171}
172
173pub type PackageArchiveReadLimits = Limits<PackageArchiveReadResource>;
175
176impl Limits<PackageArchiveReadResource> {
177 #[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 pub const fn reference_v1() -> Self {
186 Self::from_ceilings([128 * 1024 * 1024, 0, 0, 0, 0, 0, 0])
187 }
188
189 pub const fn archive_bytes(&self) -> u64 {
191 self.ceilings[0]
192 }
193}
194
195pub type PackageArchiveReadLimitError = LimitError<PackageArchiveReadResource>;
197
198#[derive(Clone, Copy, Debug, Eq, PartialEq)]
200pub struct PackageReadCeilings {
201 pub trees: PackageTreeReadCeilings,
203 pub archives: PackageArchiveReadCeilings,
205}
206
207impl PackageReadCeilings {
208 pub const fn reference_v1() -> Self {
210 Self {
211 trees: PackageTreeReadCeilings::reference_v1(),
212 archives: PackageArchiveReadCeilings::reference_v1(),
213 }
214 }
215}
216
217pub 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
231pub type PackageReadLimits = Limits<PackageReadResource>;
233
234impl Limits<PackageReadResource> {
235 #[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 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 pub const fn archives(&self) -> PackageArchiveReadLimits {
266 PackageArchiveReadLimits::from_ceilings([self.ceilings[6], 0, 0, 0, 0, 0, 0])
267 }
268
269 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#[derive(Clone, Debug, Eq, PartialEq)]
285pub struct PackageTreeSource {
286 source: Location,
287}
288
289impl PackageTreeSource {
290 pub fn new(source: Location) -> Self {
292 Self { source }
293 }
294
295 pub fn source(&self) -> &Location {
297 &self.source
298 }
299}
300
301#[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 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) = ®istry
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 pub fn spec(&self) -> &PackageSpec {
364 &self.spec
365 }
366
367 pub fn tree_sources(&self) -> &[PackageTreeSource] {
369 &self.tree_sources
370 }
371
372 pub fn archive_cache(&self) -> Option<&Location> {
374 self.archive_cache.as_ref()
375 }
376
377 pub fn registry(&self) -> Option<&Location> {
379 self.registry.as_ref()
380 }
381
382 pub const fn limits(&self) -> PackageReadLimits {
384 self.limits
385 }
386}
387
388#[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 pub fn spec(&self) -> &PackageSpec {
402 &self.spec
403 }
404
405 pub fn issues(&self) -> &[PackageReadRequestIssue] {
407 &self.issues
408 }
409}
410
411#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
413#[non_exhaustive]
414pub enum PackageReadRequestIssue {
415 #[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 #[error("Package Archive cache at {location} is not a prefix: {source}")]
425 InvalidArchiveCacheRole {
426 location: Location,
427 #[source]
428 source: LocationRoleError,
429 },
430 #[error("Package Registry at {location} is not a prefix: {source}")]
432 InvalidRegistryRole {
433 location: Location,
434 #[source]
435 source: LocationRoleError,
436 },
437}
438
439pub struct PackageTreeReadEntry {
441 relative_path: String,
442 bytes: Vec<u8>,
443}
444
445impl PackageTreeReadEntry {
446 pub fn relative_path(&self) -> &str {
448 &self.relative_path
449 }
450
451 pub fn bytes(&self) -> &[u8] {
453 &self.bytes
454 }
455
456 pub fn len(&self) -> u64 {
458 self.bytes.len() as u64
459 }
460
461 pub fn is_empty(&self) -> bool {
463 self.bytes.is_empty()
464 }
465
466 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
482pub 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 pub fn spec(&self) -> &PackageSpec {
494 &self.spec
495 }
496
497 pub fn source_index(&self) -> usize {
499 self.source_index
500 }
501
502 pub fn configured_source(&self) -> &Location {
504 &self.configured_source
505 }
506
507 pub fn candidate_location(&self) -> &Location {
509 &self.candidate_location
510 }
511
512 pub fn entries(&self) -> &[PackageTreeReadEntry] {
514 &self.entries
515 }
516
517 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
550pub struct CachedPackageArchiveRead {
552 spec: PackageSpec,
553 configured_source: Location,
554 candidate_location: Location,
555 bytes: Vec<u8>,
556}
557
558impl CachedPackageArchiveRead {
559 pub fn spec(&self) -> &PackageSpec {
561 &self.spec
562 }
563
564 pub fn configured_source(&self) -> &Location {
566 &self.configured_source
567 }
568
569 pub fn candidate_location(&self) -> &Location {
571 &self.candidate_location
572 }
573
574 pub fn bytes(&self) -> &[u8] {
576 &self.bytes
577 }
578
579 pub fn len(&self) -> u64 {
581 self.bytes.len() as u64
582 }
583
584 pub fn is_empty(&self) -> bool {
586 self.bytes.is_empty()
587 }
588
589 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
612pub 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 pub fn spec(&self) -> &PackageSpec {
624 &self.spec
625 }
626
627 pub fn configured_source(&self) -> &Location {
629 &self.configured_source
630 }
631
632 pub fn candidate_location(&self) -> &Location {
634 &self.candidate_location
635 }
636
637 pub fn cache_destination(&self) -> Option<&Location> {
639 self.cache_destination.as_ref()
640 }
641
642 pub fn bytes(&self) -> &[u8] {
644 &self.bytes
645 }
646
647 pub fn len(&self) -> u64 {
649 self.bytes.len() as u64
650 }
651
652 pub fn is_empty(&self) -> bool {
654 self.bytes.is_empty()
655 }
656
657 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#[derive(Debug)]
684pub struct UnavailablePackageRead {
685 spec: PackageSpec,
686 failure: crate::PackageReadFailure,
687}
688
689impl UnavailablePackageRead {
690 pub fn spec(&self) -> &PackageSpec {
692 &self.spec
693 }
694
695 pub fn failure(&self) -> &crate::PackageReadFailure {
697 &self.failure
698 }
699
700 pub fn reason(&self) -> &crate::PackageReadFailureReason {
702 self.failure.reason()
703 }
704
705 pub fn into_parts(self) -> (PackageSpec, crate::PackageReadFailure) {
707 (self.spec, self.failure)
708 }
709}
710
711#[derive(Debug)]
713#[non_exhaustive]
714pub enum PackageRead {
715 Tree(PackageTreeRead),
717 CachedArchive(CachedPackageArchiveRead),
719 RegistryArchive(RegistryPackageArchiveRead),
721 Unavailable(UnavailablePackageRead),
723}
724
725impl PackageRead {
726 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 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#[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, ®istry_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#[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 pub fn spec(&self) -> &PackageSpec {
961 &self.spec
962 }
963
964 pub fn source_index(&self) -> Option<usize> {
966 self.source_index
967 }
968
969 pub fn configured_source(&self) -> Option<&Location> {
971 self.configured_source.as_ref()
972 }
973
974 pub fn candidate_location(&self) -> Option<&Location> {
976 self.candidate_location.as_ref()
977 }
978
979 pub fn failed_path(&self) -> Option<&str> {
981 self.failed_path.as_deref()
982 }
983
984 pub fn failure(&self) -> &crate::PackageReadFailure {
986 &self.failure
987 }
988
989 pub fn reason(&self) -> &crate::PackageReadFailureReason {
991 self.failure.reason()
992 }
993
994 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#[derive(Debug, thiserror::Error)]
1055#[non_exhaustive]
1056pub enum PackageReadErrorCause {
1057 #[error("operator resolution failed")]
1059 ResolveOperator(#[source] BoxError),
1060 #[error("required Package Tree capabilities are unsupported")]
1062 UnsupportedTreeCapabilities {
1063 list: bool,
1064 list_with_recursive: bool,
1065 read: bool,
1066 },
1067 #[error("Package Archive read capability is unsupported")]
1069 UnsupportedArchiveRead,
1070 #[error("the Package Tree listing failed")]
1072 TreeList(#[source] ::opendal::Error),
1073 #[error("a Package Tree object read failed")]
1075 TreeRead(#[source] ::opendal::Error),
1076 #[error("a listed Package Tree object became absent")]
1078 ListedTreeObjectAbsent(#[source] ::opendal::Error),
1079 #[error("the Package Archive cache read failed")]
1081 CacheRead(#[source] ::opendal::Error),
1082 #[error("the Package Registry read failed")]
1084 RegistryRead(#[source] ::opendal::Error),
1085 #[error("the Package Tree listing had structural issues")]
1087 TreeStructural(#[source] PackageTreeReadSurveyError),
1088 #[error("the listed objects do not form a Package Tree")]
1090 InvalidPackageTree(#[source] PackageTreeError),
1091 #[error("a Package Tree Read limit failed")]
1093 TreeLimit(#[source] PackageTreeReadLimitError),
1094 #[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#[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 pub fn spec(&self) -> &PackageSpec {
1265 &self.spec
1266 }
1267
1268 pub fn destination(&self) -> &Location {
1270 &self.destination
1271 }
1272
1273 pub fn bytes(&self) -> &[u8] {
1275 &self.bytes
1276 }
1277
1278 pub fn len(&self) -> u64 {
1280 self.bytes.len() as u64
1281 }
1282
1283 pub fn is_empty(&self) -> bool {
1285 self.bytes.is_empty()
1286 }
1287
1288 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#[derive(Clone, Debug, Eq, PartialEq)]
1308#[non_exhaustive]
1309#[cfg(feature = "package-reading")]
1310pub enum ReadPackageInsertionTarget {
1311 PackageTree,
1313 CachedArchive,
1315 RegistryArchive,
1317 PackageCatalog,
1319}
1320
1321#[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 pub fn spec(&self) -> &PackageSpec {
1339 self.failure.spec()
1340 }
1341
1342 pub fn failure(&self) -> &crate::PackageReadFailure {
1344 &self.failure
1345 }
1346
1347 pub fn reason(&self) -> &crate::PackageReadFailureReason {
1349 self.failure.reason()
1350 }
1351
1352 pub fn target(&self) -> &ReadPackageInsertionTarget {
1354 &self.target
1355 }
1356
1357 pub fn cause(&self) -> &ReadPackageInsertionErrorCause {
1359 &self.cause
1360 }
1361}
1362
1363#[derive(Debug, thiserror::Error)]
1365#[non_exhaustive]
1366#[cfg(feature = "package-reading")]
1367pub enum ReadPackageInsertionErrorCause {
1368 #[error("read entries could not construct a Package Tree")]
1370 PackageTree(#[source] crate::PackageTreeError),
1371 #[error("raw archive bytes could not expand into a Package Tree")]
1373 ArchiveExpansion(#[source] Box<crate::PackageReadError>),
1374 #[error("the Package Catalog rejected the constructed tree")]
1376 PackageCatalog(#[source] crate::PackageCatalogError),
1377}
1378
1379#[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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1509#[non_exhaustive]
1510pub enum PackageTreeReadEntryKind {
1511 Unknown,
1513}
1514
1515#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
1517#[non_exhaustive]
1518pub enum PackageTreeReadIssue {
1519 #[error("listed operation path {operation_path:?} is outside the Package Tree prefix")]
1521 ListedPathOutsidePrefix { operation_path: String },
1522 #[error("listed operation path {operation_path:?} is a prefix marker where a file is required")]
1524 PrefixMarkerWhereFileRequired { operation_path: String },
1525 #[error("listed operation path {operation_path:?} has an empty relative path")]
1527 EmptyRelativeOperationPath { operation_path: String },
1528 #[error("listed operation path {operation_path:?} has unsupported kind {kind:?}")]
1530 UnsupportedEntryKind {
1531 operation_path: String,
1532 kind: PackageTreeReadEntryKind,
1533 },
1534}
1535
1536#[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 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}