Skip to main content

typst_pack/opendal/
pack_assembly.rs

1//! OpenDAL read for Pack Assembly inputs.
2//!
3//! # Complete Pack Assembly
4//!
5//! The storage operations are caller-polled async operations. Their completed,
6//! owned values compose through the existing synchronous Pack Creation loop:
7//!
8//! ```no_run
9//! # #[cfg(feature = "package-reading")]
10//! # mod complete {
11//! use std::collections::HashSet;
12//!
13//! use typst::foundations::Dict;
14//! use typst_pack::opendal::{Location, OperatorBindings};
15//! use typst_pack::opendal::pack_assembly::{
16//!     FontReadEntry, FontReadLimits, FontReadRequest, FontSource,
17//!     PackageReadLimits, PackageReadRequest, PackageTreeSource,
18//!     ProjectReadEntry, ProjectReadLimits, ProjectReadRequest,
19//!     read_fonts, read_package, read_project, insert_read_package,
20//! };
21//! use typst_pack::opendal::write::{
22//!     PackageCacheArchiveWriteRequest, write_package_cache_archive,
23//! };
24//! use typst_pack::{
25//!     DiscoverySpecification, DocumentTime, FontCatalog, FontCatalogEntry, FontContainer,
26//!     FontDisposition, Pack, PackCreationInput, PackCreationOutcome,
27//!     PackageReadFailures, PackageCatalog, PackageDisposition,
28//!     PackageExpansionLimits, ProjectSnapshotAssembly, TypstTarget, create,
29//! };
30//!
31//! async fn assemble(bindings: &OperatorBindings) -> Result<Pack, Box<dyn std::error::Error>> {
32//!     let project_request = ProjectReadRequest::new(
33//!         "project:/sources/document/".parse::<Location>()?,
34//!         ProjectReadLimits::reference_v1(),
35//!     )?;
36//!     let (_, project_entries) = read_project(bindings, &project_request).await?.into_parts();
37//!     let project = ProjectSnapshotAssembly::new("main.typ").assemble(
38//!         project_entries.into_iter().map(ProjectReadEntry::into_parts),
39//!     )?;
40//!
41//!     let font_request = FontReadRequest::new(
42//!         [FontSource::new(
43//!             "fonts:/catalog/".parse::<Location>()?,
44//!             FontDisposition::Embedded,
45//!         )],
46//!         FontReadLimits::reference_v1(),
47//!     )?;
48//!     let (_, font_entries) = read_fonts(bindings, &font_request).await?.into_parts();
49//!     let mut fonts = FontCatalog::new();
50//!     for entry in font_entries {
51//!         let (_, _, _, disposition, bytes) = FontReadEntry::into_parts(entry);
52//!         fonts.push(FontCatalogEntry::new(FontContainer::new(bytes)?, disposition));
53//!     }
54//!
55//!     let tree_source = PackageTreeSource::new("packages:/trees/".parse::<Location>()?);
56//!     let archive_cache = "packages:/cache/".parse::<Location>()?;
57//!     let registry = "registry:/packages/".parse::<Location>()?;
58//!     let discovery = DiscoverySpecification::new(
59//!         TypstTarget::Paged,
60//!         Dict::new(),
61//!         DocumentTime::Absent,
62//!         [],
63//!     )?;
64//!     let mut packages = PackageCatalog::new();
65//!     let mut failures = PackageReadFailures::new();
66//!     let mut attempted = HashSet::new();
67//!
68//!     loop {
69//!         match create(PackCreationInput {
70//!             project: &project,
71//!             packages: &packages,
72//!             fonts: &fonts,
73//!             package_failures: &failures,
74//!             discovery: &discovery,
75//!             metadata: None,
76//!         })? {
77//!             PackCreationOutcome::Created { pack, warnings: _ } => return Ok(pack),
78//!             PackCreationOutcome::MissingPackageSpecifications(missing) => {
79//!                 for spec in missing {
80//!                     if !attempted.insert(spec.to_string()) {
81//!                         return Err("Pack Creation repeated an attempted specification".into());
82//!                     }
83//!                     let request = PackageReadRequest::new(
84//!                         spec,
85//!                         [tree_source.clone()],
86//!                         Some(archive_cache.clone()),
87//!                         Some(registry.clone()),
88//!                         PackageReadLimits::reference_v1(),
89//!                     )?;
90//!                     let read = read_package(bindings, &request).await?;
91//!                     let insertion = insert_read_package(
92//!                         &mut packages,
93//!                         &mut failures,
94//!                         read,
95//!                         PackageDisposition::Embedded,
96//!                         PackageExpansionLimits::reference_v1(),
97//!                     );
98//!                     match insertion {
99//!                         Ok(Some(residue)) => {
100//!                             let write = PackageCacheArchiveWriteRequest::new(
101//!                                 residue.destination().clone(),
102//!                             )?;
103//!                             let _cache_result = write_package_cache_archive(
104//!                                 bindings,
105//!                                 &write,
106//!                                 residue.bytes(),
107//!                             ).await;
108//!                             // Cache failure is separate evidence and does not
109//!                             // invalidate the inserted Package Tree.
110//!                         }
111//!                         Ok(None) => {}
112//!                         // Insertion retained the mapped Package Read Failure.
113//!                         // Resume so Dependency Discovery can attach it to the import.
114//!                         Err(_error) => {}
115//!                     }
116//!                 }
117//!             }
118//!         }
119//!     }
120//! }
121//! # }
122//! ```
123
124mod package;
125
126pub use package::*;
127
128use std::fmt;
129
130use super::read::recursive::{
131    RecursiveReadLimits, RecursiveReadOperation, RecursiveReadResource, RecursiveReadSelection,
132    RecursiveSurveyIssue, RecursiveSurveyIssueKind, read_recursive_prefix, read_recursive_prefixes,
133};
134use super::{BoxError, Location, LocationRoleError, OperatorResolver};
135use crate::FontDisposition;
136use crate::limits::{LimitError, Limits, ResourceKind};
137use crate::redacted_error::RedactedError;
138
139fn aggregate_issue_message<T: fmt::Display>(issues: &[T], summary: &str) -> String {
140    if let [issue] = issues {
141        issue.to_string()
142    } else {
143        format!("{summary} with {} issue(s)", issues.len())
144    }
145}
146
147fn failed_path_context(path: Option<&str>) -> String {
148    path.map(|path| format!(" while reading object operation path {path:?}"))
149        .unwrap_or_default()
150}
151
152/// Named finite ceilings for one OpenDAL Project Read.
153#[derive(Clone, Copy, Debug, Eq, PartialEq)]
154pub struct ProjectReadCeilings {
155    pub listed_entries: u64,
156    pub listed_path_bytes: u64,
157    pub total_listed_path_bytes: u64,
158    pub selected_files: u64,
159    pub object_bytes: u64,
160    pub total_bytes: u64,
161}
162
163impl ProjectReadCeilings {
164    /// The first-party version-1 Project Read profile.
165    pub const fn reference_v1() -> Self {
166        Self {
167            listed_entries: 1_000_000,
168            listed_path_bytes: 64 * 1024,
169            total_listed_path_bytes: 256 * 1024 * 1024,
170            selected_files: 100_000,
171            object_bytes: 256 * 1024 * 1024,
172            total_bytes: 2 * 1024 * 1024 * 1024,
173        }
174    }
175}
176
177/// A resource bounded during OpenDAL Project Read.
178pub type ProjectReadResource = ResourceKind<9>;
179
180#[allow(non_upper_case_globals)]
181impl ResourceKind<9> {
182    pub const ListedEntries: Self = Self::new(0);
183    pub const ListedPathBytes: Self = Self::new(1);
184    pub const TotalListedPathBytes: Self = Self::new(2);
185    pub const SelectedFiles: Self = Self::new(3);
186    pub const ObjectBytes: Self = Self::new(4);
187    pub const TotalBytes: Self = Self::new(5);
188}
189
190/// Mandatory finite limits for OpenDAL Project Read.
191pub type ProjectReadLimits = Limits<ProjectReadResource>;
192
193impl Limits<ProjectReadResource> {
194    /// Validates all named read ceilings.
195    #[track_caller]
196    pub fn new(ceilings: ProjectReadCeilings) -> Self {
197        let limits = Self::from_ceilings([
198            ceilings.listed_entries,
199            ceilings.listed_path_bytes,
200            ceilings.total_listed_path_bytes,
201            ceilings.selected_files,
202            ceilings.object_bytes,
203            ceilings.total_bytes,
204            0,
205        ])
206        .assert_probe_resources([
207            ProjectReadResource::ListedEntries,
208            ProjectReadResource::ListedPathBytes,
209            ProjectReadResource::TotalListedPathBytes,
210            ProjectReadResource::SelectedFiles,
211            ProjectReadResource::ObjectBytes,
212            ProjectReadResource::TotalBytes,
213        ]);
214        assert!(
215            ceilings.object_bytes <= ceilings.total_bytes,
216            "the ObjectBytes ceiling {} exceeds the TotalBytes ceiling {}",
217            ceilings.object_bytes,
218            ceilings.total_bytes
219        );
220        limits
221    }
222
223    /// The validated first-party version-1 Project Read limits.
224    pub const fn reference_v1() -> Self {
225        Self::from_ceilings([
226            1_000_000,
227            64 * 1024,
228            256 * 1024 * 1024,
229            100_000,
230            256 * 1024 * 1024,
231            2 * 1024 * 1024 * 1024,
232            0,
233        ])
234    }
235
236    pub const fn listed_entries(&self) -> u64 {
237        self.ceilings[0]
238    }
239
240    pub const fn listed_path_bytes(&self) -> u64 {
241        self.ceilings[1]
242    }
243
244    pub const fn total_listed_path_bytes(&self) -> u64 {
245        self.ceilings[2]
246    }
247
248    pub const fn selected_files(&self) -> u64 {
249        self.ceilings[3]
250    }
251
252    pub const fn object_bytes(&self) -> u64 {
253        self.ceilings[4]
254    }
255
256    pub const fn total_bytes(&self) -> u64 {
257        self.ceilings[5]
258    }
259}
260
261/// Project Read exceeded or could not account for a mandatory limit.
262pub type ProjectReadLimitError = LimitError<ProjectReadResource>;
263
264/// A validated request to read every yielded file below one prefix.
265#[derive(Clone, Debug)]
266pub struct ProjectReadRequest {
267    source: Location,
268    limits: ProjectReadLimits,
269}
270
271impl ProjectReadRequest {
272    /// Validates a prefix source and retains its mandatory limits.
273    pub fn new(
274        source: Location,
275        limits: ProjectReadLimits,
276    ) -> Result<Self, ProjectReadRequestError> {
277        if let Err(role_error) = source.require_prefix() {
278            return Err(ProjectReadRequestError::InvalidSourceRole {
279                location: source,
280                source: role_error,
281            });
282        }
283        Ok(Self { source, limits })
284    }
285
286    /// The normalized project prefix.
287    pub fn source(&self) -> &Location {
288        &self.source
289    }
290
291    /// The mandatory finite Project Read limits.
292    pub const fn limits(&self) -> ProjectReadLimits {
293        self.limits
294    }
295}
296
297/// A reason a Project Read request is invalid.
298#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
299#[non_exhaustive]
300pub enum ProjectReadRequestError {
301    #[error("project source {location} is not a prefix: {source}")]
302    InvalidSourceRole {
303        location: Location,
304        #[source]
305        source: LocationRoleError,
306    },
307}
308
309/// One exact path-and-byte entry read below a project prefix.
310pub struct ProjectReadEntry {
311    relative_path: String,
312    bytes: Vec<u8>,
313}
314
315impl ProjectReadEntry {
316    /// The operation path relative to the requested prefix.
317    pub fn relative_path(&self) -> &str {
318        &self.relative_path
319    }
320
321    /// The exact bytes observed by the completed object read.
322    pub fn bytes(&self) -> &[u8] {
323        &self.bytes
324    }
325
326    /// The read byte length.
327    pub fn len(&self) -> u64 {
328        self.bytes.len() as u64
329    }
330
331    /// Whether this read object was empty.
332    pub fn is_empty(&self) -> bool {
333        self.bytes.is_empty()
334    }
335
336    /// Recovers the owned path and exact bytes for Project Snapshot assembly.
337    pub fn into_parts(self) -> (String, Vec<u8>) {
338        (self.relative_path, self.bytes)
339    }
340}
341
342impl fmt::Debug for ProjectReadEntry {
343    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
344        formatter
345            .debug_struct("ProjectReadEntry")
346            .field("relative_path", &self.relative_path)
347            .field("byte_length", &self.bytes.len())
348            .finish()
349    }
350}
351
352/// Exact entries read from one project prefix.
353pub struct ProjectRead {
354    source: Location,
355    entries: Vec<ProjectReadEntry>,
356}
357
358impl ProjectRead {
359    /// The normalized prefix from which entries were read.
360    pub fn source(&self) -> &Location {
361        &self.source
362    }
363
364    /// Read entries in relative operation-path order.
365    pub fn entries(&self) -> &[ProjectReadEntry] {
366        &self.entries
367    }
368
369    /// Recovers the source and owned entries for Project Snapshot assembly.
370    pub fn into_parts(self) -> (Location, Vec<ProjectReadEntry>) {
371        (self.source, self.entries)
372    }
373}
374
375impl fmt::Debug for ProjectRead {
376    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
377        formatter
378            .debug_struct("ProjectRead")
379            .field("source", &self.source)
380            .field("entries", &self.entries)
381            .finish()
382    }
383}
384
385/// An unsupported yielded OpenDAL entry kind.
386#[derive(Clone, Copy, Debug, Eq, PartialEq)]
387#[non_exhaustive]
388pub enum ProjectReadEntryKind {
389    Unknown,
390}
391
392/// One structural issue found while surveying a project prefix.
393#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
394#[non_exhaustive]
395pub enum ProjectReadIssue {
396    #[error("listed operation path {operation_path:?} is outside the project prefix")]
397    ListedPathOutsidePrefix { operation_path: String },
398    #[error("listed operation path {operation_path:?} is a prefix marker where a file is required")]
399    PrefixMarkerWhereFileRequired { operation_path: String },
400    #[error("listed operation path {operation_path:?} has an empty relative path")]
401    EmptyRelativeOperationPath { operation_path: String },
402    #[error("listed operation path {operation_path:?} is not a valid relative operation path")]
403    InvalidRelativeOperationPath { operation_path: String },
404    #[error("listed object {operation_path:?} was yielded more than once")]
405    DuplicateListedObject { operation_path: String },
406    #[error("listed operation path {operation_path:?} has unsupported kind {kind:?}")]
407    UnsupportedEntryKind {
408        operation_path: String,
409        kind: ProjectReadEntryKind,
410    },
411}
412
413/// The nonempty canonical set of structural project survey issues.
414#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
415#[error(
416    "{message}",
417    message = aggregate_issue_message(.issues.as_slice(), "project survey failed")
418)]
419pub struct ProjectReadSurveyError {
420    issues: Vec<ProjectReadIssue>,
421}
422
423impl ProjectReadSurveyError {
424    /// Every independently detectable issue in canonical order.
425    pub fn issues(&self) -> &[ProjectReadIssue] {
426        &self.issues
427    }
428}
429
430/// Reads every file entry yielded below one project prefix.
431///
432/// Directory markers are ignored. `.typkignore` is an ordinary file; Project
433/// Snapshot assembly remains authoritative for canonical project paths,
434/// collisions, `.typk` exclusion, entrypoint presence, bytes, and ordering.
435/// The listing is one observation, not a storage snapshot or coexistence claim.
436///
437/// ```no_run
438/// use typst::foundations::Dict;
439/// use typst_pack::{
440///     DiscoverySpecification, DocumentTime, FontCatalog, PackCreationInput,
441///     PackageReadFailures, PackageCatalog, ProjectSnapshotAssembly,
442///     TypstTarget, create,
443/// };
444/// use typst_pack::opendal::OperatorBindings;
445/// use typst_pack::opendal::pack_assembly::{
446///     ProjectReadEntry, ProjectReadRequest, read_project,
447/// };
448///
449/// async fn read_and_create(
450///     bindings: &OperatorBindings,
451///     request: &ProjectReadRequest,
452/// ) -> Result<(), Box<dyn std::error::Error>> {
453///     let (_, entries) = read_project(bindings, request).await?.into_parts();
454///     let project = ProjectSnapshotAssembly::new("main.typ").assemble(
455///         entries.into_iter().map(ProjectReadEntry::into_parts),
456///     )?;
457///     let packages = PackageCatalog::new();
458///     let fonts = FontCatalog::new();
459///     let package_failures = PackageReadFailures::new();
460///     let discovery = DiscoverySpecification::new(
461///         TypstTarget::Paged,
462///         Dict::new(),
463///         DocumentTime::Absent,
464///         [],
465///     )?;
466///     let _outcome = create(PackCreationInput {
467///         project: &project,
468///         packages: &packages,
469///         fonts: &fonts,
470///         package_failures: &package_failures,
471///         discovery: &discovery,
472///         metadata: None,
473///     })?;
474///     Ok(())
475/// }
476/// ```
477#[allow(clippy::result_large_err)]
478pub async fn read_project<R: OperatorResolver + ?Sized>(
479    resolver: &R,
480    request: &ProjectReadRequest,
481) -> Result<ProjectRead, ProjectReadError> {
482    let source = request.source().clone();
483    let entries = read_recursive_prefix(
484        resolver,
485        request.source(),
486        RecursiveReadSelection::AllFiles,
487        request.limits().into(),
488        &ProjectReadOperation {
489            source_location: request.source(),
490        },
491    )
492    .await?
493    .into_iter()
494    .map(|object| ProjectReadEntry {
495        relative_path: object.relative_path,
496        bytes: object.bytes,
497    })
498    .collect();
499
500    Ok(ProjectRead { source, entries })
501}
502
503/// A failure while reading a project through OpenDAL.
504///
505/// This error's own `Display` and `Debug` omit native resolver and OpenDAL
506/// messages. Rendering its complete source chain may disclose backend context.
507#[derive(Debug, thiserror::Error)]
508#[error(
509    "Project Read failed for binding {binding} at prefix operation path {operation_path:?}{failed_path}: {cause}",
510    binding = .source_location.binding(),
511    operation_path = .source_location.operation_path(),
512    failed_path = failed_path_context(.failed_path.as_deref()),
513)]
514pub struct ProjectReadError {
515    source_location: Location,
516    failed_path: Option<String>,
517    #[source]
518    cause: RedactedError<ProjectReadErrorCause>,
519}
520
521impl ProjectReadError {
522    /// The normalized project prefix whose read failed.
523    pub fn source_location(&self) -> &Location {
524        &self.source_location
525    }
526
527    /// The selected object's operation path when one object read failed.
528    pub fn failed_path(&self) -> Option<&str> {
529        self.failed_path.as_deref()
530    }
531
532    /// The typed cause of this failure.
533    pub fn cause(&self) -> &ProjectReadErrorCause {
534        self.cause.inner()
535    }
536
537    fn new(
538        source_location: &Location,
539        failed_path: Option<String>,
540        cause: ProjectReadErrorCause,
541    ) -> Self {
542        Self {
543            source_location: source_location.clone(),
544            failed_path,
545            cause: RedactedError::new(cause),
546        }
547    }
548}
549
550/// The typed cause of an OpenDAL Project Read failure.
551#[derive(Debug, thiserror::Error)]
552#[non_exhaustive]
553pub enum ProjectReadErrorCause {
554    #[error("operator resolution failed")]
555    ResolveOperator(#[source] BoxError),
556    #[error("required listing or read capability is unsupported")]
557    UnsupportedCapabilities {
558        list: bool,
559        list_with_recursive: bool,
560        read: bool,
561    },
562    #[error("the recursive listing failed")]
563    List(#[source] ::opendal::Error),
564    #[error("a listed object read failed")]
565    Read(#[source] ::opendal::Error),
566    #[error("a listed object was absent when read")]
567    ListedObjectAbsent(#[source] ::opendal::Error),
568    #[error("the completed listing had structural issues")]
569    Structural(#[source] ProjectReadSurveyError),
570    #[error("a Project Read limit failed")]
571    Limit(#[source] ProjectReadLimitError),
572}
573
574struct ProjectReadOperation<'a> {
575    source_location: &'a Location,
576}
577
578impl RecursiveReadOperation for ProjectReadOperation<'_> {
579    type Error = ProjectReadError;
580
581    fn invalid_location_role(&self, _: usize, _: LocationRoleError) -> ProjectReadError {
582        unreachable!("ProjectReadRequest validates the prefix role")
583    }
584
585    fn resolve_operator(&self, _: usize, source: BoxError) -> ProjectReadError {
586        ProjectReadError::new(
587            self.source_location,
588            None,
589            ProjectReadErrorCause::ResolveOperator(source),
590        )
591    }
592
593    fn unsupported_capabilities(
594        &self,
595        _: usize,
596        list: bool,
597        list_with_recursive: bool,
598        read: bool,
599    ) -> ProjectReadError {
600        ProjectReadError::new(
601            self.source_location,
602            None,
603            ProjectReadErrorCause::UnsupportedCapabilities {
604                list,
605                list_with_recursive,
606                read,
607            },
608        )
609    }
610
611    fn list(&self, _: usize, source: ::opendal::Error) -> ProjectReadError {
612        ProjectReadError::new(
613            self.source_location,
614            None,
615            ProjectReadErrorCause::List(source),
616        )
617    }
618
619    fn read(&self, _: usize, operation_path: String, source: ::opendal::Error) -> ProjectReadError {
620        ProjectReadError::new(
621            self.source_location,
622            Some(operation_path),
623            ProjectReadErrorCause::Read(source),
624        )
625    }
626
627    fn listed_object_absent(
628        &self,
629        _: usize,
630        operation_path: String,
631        source: ::opendal::Error,
632    ) -> ProjectReadError {
633        ProjectReadError::new(
634            self.source_location,
635            Some(operation_path),
636            ProjectReadErrorCause::ListedObjectAbsent(source),
637        )
638    }
639
640    fn structural(&self, _: usize, issues: Vec<RecursiveSurveyIssue>) -> ProjectReadError {
641        ProjectReadError::new(
642            self.source_location,
643            None,
644            ProjectReadErrorCause::Structural(ProjectReadSurveyError {
645                issues: issues.into_iter().map(map_issue).collect(),
646            }),
647        )
648    }
649
650    fn limit(
651        &self,
652        _: usize,
653        resource: RecursiveReadResource,
654        ceiling: u64,
655        _: u64,
656    ) -> ProjectReadError {
657        ProjectReadError::new(
658            self.source_location,
659            None,
660            ProjectReadErrorCause::Limit(ProjectReadLimitError::exceeded(
661                map_resource(resource),
662                ceiling,
663            )),
664        )
665    }
666
667    fn accounting_overflow(&self, _: usize, resource: RecursiveReadResource) -> ProjectReadError {
668        ProjectReadError::new(
669            self.source_location,
670            None,
671            ProjectReadErrorCause::Limit(ProjectReadLimitError::AccountingOverflow {
672                resource: map_resource(resource),
673            }),
674        )
675    }
676}
677
678impl From<ProjectReadLimits> for RecursiveReadLimits {
679    fn from(limits: ProjectReadLimits) -> Self {
680        Self::new(
681            limits.listed_entries(),
682            limits.listed_path_bytes(),
683            limits.total_listed_path_bytes(),
684            limits.selected_files(),
685            limits.object_bytes(),
686            limits.total_bytes(),
687        )
688    }
689}
690
691fn map_resource(resource: RecursiveReadResource) -> ProjectReadResource {
692    match resource {
693        RecursiveReadResource::ListedEntries => ProjectReadResource::ListedEntries,
694        RecursiveReadResource::ListedPathBytes => ProjectReadResource::ListedPathBytes,
695        RecursiveReadResource::TotalListedPathBytes => ProjectReadResource::TotalListedPathBytes,
696        RecursiveReadResource::SelectedObjects => ProjectReadResource::SelectedFiles,
697        RecursiveReadResource::ObjectBytes => ProjectReadResource::ObjectBytes,
698        RecursiveReadResource::TotalBytes => ProjectReadResource::TotalBytes,
699        _ => unreachable!("unknown recursive read resource"),
700    }
701}
702
703fn map_issue(issue: RecursiveSurveyIssue) -> ProjectReadIssue {
704    let operation_path = issue.operation_path;
705    match issue.kind {
706        RecursiveSurveyIssueKind::ListedPathOutsidePrefix => {
707            ProjectReadIssue::ListedPathOutsidePrefix { operation_path }
708        }
709        RecursiveSurveyIssueKind::PrefixMarkerWhereFileRequired => {
710            ProjectReadIssue::PrefixMarkerWhereFileRequired { operation_path }
711        }
712        RecursiveSurveyIssueKind::EmptyRelativeOperationPath => {
713            ProjectReadIssue::EmptyRelativeOperationPath { operation_path }
714        }
715        RecursiveSurveyIssueKind::InvalidRelativeOperationPath => {
716            ProjectReadIssue::InvalidRelativeOperationPath { operation_path }
717        }
718        RecursiveSurveyIssueKind::DuplicateListedObject => {
719            ProjectReadIssue::DuplicateListedObject { operation_path }
720        }
721        RecursiveSurveyIssueKind::UnsupportedEntryKind => ProjectReadIssue::UnsupportedEntryKind {
722            operation_path,
723            kind: ProjectReadEntryKind::Unknown,
724        },
725    }
726}
727
728/// Named finite ceilings for one OpenDAL Font Read.
729#[derive(Clone, Copy, Debug, Eq, PartialEq)]
730pub struct FontReadCeilings {
731    pub listed_entries: u64,
732    pub listed_path_bytes: u64,
733    pub total_listed_path_bytes: u64,
734    pub selected_containers: u64,
735    pub container_bytes: u64,
736    pub total_bytes: u64,
737}
738
739impl FontReadCeilings {
740    /// The first-party version-1 Font Read profile.
741    pub const fn reference_v1() -> Self {
742        Self {
743            listed_entries: 100_000,
744            listed_path_bytes: 64 * 1024,
745            total_listed_path_bytes: 64 * 1024 * 1024,
746            selected_containers: 16_384,
747            container_bytes: 256 * 1024 * 1024,
748            total_bytes: 2 * 1024 * 1024 * 1024,
749        }
750    }
751}
752
753/// A resource bounded across one OpenDAL Font Read.
754pub type FontReadResource = ResourceKind<10>;
755
756#[allow(non_upper_case_globals)]
757impl ResourceKind<10> {
758    pub const ListedEntries: Self = Self::new(0);
759    pub const ListedPathBytes: Self = Self::new(1);
760    pub const TotalListedPathBytes: Self = Self::new(2);
761    pub const SelectedContainers: Self = Self::new(3);
762    pub const ContainerBytes: Self = Self::new(4);
763    pub const TotalBytes: Self = Self::new(5);
764}
765
766/// Mandatory finite limits for OpenDAL Font Read.
767pub type FontReadLimits = Limits<FontReadResource>;
768
769impl Limits<FontReadResource> {
770    /// Validates all named read ceilings.
771    #[track_caller]
772    pub fn new(ceilings: FontReadCeilings) -> Self {
773        let limits = Self::from_ceilings([
774            ceilings.listed_entries,
775            ceilings.listed_path_bytes,
776            ceilings.total_listed_path_bytes,
777            ceilings.selected_containers,
778            ceilings.container_bytes,
779            ceilings.total_bytes,
780            0,
781        ])
782        .assert_probe_resources([
783            FontReadResource::ListedEntries,
784            FontReadResource::ListedPathBytes,
785            FontReadResource::TotalListedPathBytes,
786            FontReadResource::SelectedContainers,
787            FontReadResource::ContainerBytes,
788            FontReadResource::TotalBytes,
789        ]);
790        assert!(
791            ceilings.container_bytes <= ceilings.total_bytes,
792            "the ContainerBytes ceiling {} exceeds the TotalBytes ceiling {}",
793            ceilings.container_bytes,
794            ceilings.total_bytes
795        );
796        limits
797    }
798
799    /// The validated first-party version-1 Font Read limits.
800    pub const fn reference_v1() -> Self {
801        Self::from_ceilings([
802            100_000,
803            64 * 1024,
804            64 * 1024 * 1024,
805            16_384,
806            256 * 1024 * 1024,
807            2 * 1024 * 1024 * 1024,
808            0,
809        ])
810    }
811
812    pub const fn listed_entries(&self) -> u64 {
813        self.ceilings[0]
814    }
815
816    pub const fn listed_path_bytes(&self) -> u64 {
817        self.ceilings[1]
818    }
819
820    pub const fn total_listed_path_bytes(&self) -> u64 {
821        self.ceilings[2]
822    }
823
824    pub const fn selected_containers(&self) -> u64 {
825        self.ceilings[3]
826    }
827
828    pub const fn container_bytes(&self) -> u64 {
829        self.ceilings[4]
830    }
831
832    pub const fn total_bytes(&self) -> u64 {
833        self.ceilings[5]
834    }
835}
836
837/// Font Read exceeded or could not account for a mandatory limit.
838pub type FontReadLimitError = LimitError<FontReadResource>;
839
840/// One explicitly configured OpenDAL prefix of Font Containers.
841#[derive(Clone, Debug, Eq, PartialEq)]
842pub struct FontSource {
843    source: Location,
844    disposition: FontDisposition,
845}
846
847impl FontSource {
848    /// Associates one prefix with the disposition of every selected container.
849    pub fn new(source: Location, disposition: FontDisposition) -> Self {
850        Self {
851            source,
852            disposition,
853        }
854    }
855
856    /// The normalized Font Container prefix.
857    pub fn source(&self) -> &Location {
858        &self.source
859    }
860
861    /// The disposition every selected container from this source carries.
862    pub const fn disposition(&self) -> FontDisposition {
863        self.disposition
864    }
865}
866
867/// A validated request to read caller-ordered OpenDAL font prefixes.
868#[derive(Clone, Debug)]
869pub struct FontReadRequest {
870    sources: Vec<FontSource>,
871    limits: FontReadLimits,
872}
873
874impl FontReadRequest {
875    /// Validates every source role before accepting the request.
876    pub fn new(
877        sources: impl IntoIterator<Item = FontSource>,
878        limits: FontReadLimits,
879    ) -> Result<Self, FontReadRequestRejection> {
880        let sources = sources.into_iter().collect::<Vec<_>>();
881        let issues = sources
882            .iter()
883            .enumerate()
884            .filter_map(|(source_index, configured)| {
885                configured.source.require_prefix().err().map(|source| {
886                    FontReadRequestIssue::InvalidSourceRole {
887                        source_index,
888                        location: configured.source.clone(),
889                        source,
890                    }
891                })
892            })
893            .collect::<Vec<_>>();
894        if !issues.is_empty() {
895            return Err(FontReadRequestRejection { issues });
896        }
897        Ok(Self { sources, limits })
898    }
899
900    /// Font sources in caller order.
901    pub fn sources(&self) -> &[FontSource] {
902        &self.sources
903    }
904
905    /// The mandatory finite limits shared across every configured source.
906    pub const fn limits(&self) -> FontReadLimits {
907        self.limits
908    }
909}
910
911/// Every invalid source role in a rejected Font Read request.
912#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
913#[error(
914    "{message}",
915    message = aggregate_issue_message(.issues.as_slice(), "Font Read request rejected")
916)]
917pub struct FontReadRequestRejection {
918    issues: Vec<FontReadRequestIssue>,
919}
920
921impl FontReadRequestRejection {
922    /// Invalid source roles in caller source order.
923    pub fn issues(&self) -> &[FontReadRequestIssue] {
924        &self.issues
925    }
926}
927
928/// One invalid source role in a Font Read request.
929#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
930#[non_exhaustive]
931pub enum FontReadRequestIssue {
932    #[error("font source {source_index} at {location} is not a prefix: {source}")]
933    InvalidSourceRole {
934        source_index: usize,
935        location: Location,
936        #[source]
937        source: LocationRoleError,
938    },
939}
940
941/// One exact Font Container selected and read from a configured source.
942pub struct FontReadEntry {
943    source_index: usize,
944    source: Location,
945    relative_path: String,
946    disposition: FontDisposition,
947    bytes: Vec<u8>,
948}
949
950impl FontReadEntry {
951    /// The configured source's caller-order index.
952    pub fn source_index(&self) -> usize {
953        self.source_index
954    }
955
956    /// The normalized prefix from which this entry was read.
957    pub fn source(&self) -> &Location {
958        &self.source
959    }
960
961    /// The selected operation path relative to its source prefix.
962    pub fn relative_path(&self) -> &str {
963        &self.relative_path
964    }
965
966    /// The explicit disposition inherited from the configured source.
967    pub const fn disposition(&self) -> FontDisposition {
968        self.disposition
969    }
970
971    /// The exact bytes observed by the completed object read.
972    pub fn bytes(&self) -> &[u8] {
973        &self.bytes
974    }
975
976    /// The read byte length.
977    pub fn len(&self) -> u64 {
978        self.bytes.len() as u64
979    }
980
981    /// Whether this read container is empty.
982    pub fn is_empty(&self) -> bool {
983        self.bytes.is_empty()
984    }
985
986    /// Recovers all owned source evidence and exact bytes.
987    pub fn into_parts(self) -> (usize, Location, String, FontDisposition, Vec<u8>) {
988        (
989            self.source_index,
990            self.source,
991            self.relative_path,
992            self.disposition,
993            self.bytes,
994        )
995    }
996}
997
998impl fmt::Debug for FontReadEntry {
999    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1000        formatter
1001            .debug_struct("FontReadEntry")
1002            .field("source_index", &self.source_index)
1003            .field("source", &self.source)
1004            .field("relative_path", &self.relative_path)
1005            .field("disposition", &self.disposition)
1006            .field("byte_length", &self.bytes.len())
1007            .finish()
1008    }
1009}
1010
1011/// Exact Font Containers read from caller-ordered sources.
1012pub struct FontRead {
1013    sources: Vec<FontSource>,
1014    entries: Vec<FontReadEntry>,
1015}
1016
1017impl FontRead {
1018    /// Configured font sources in caller order.
1019    pub fn sources(&self) -> &[FontSource] {
1020        &self.sources
1021    }
1022
1023    /// Read entries in source order, then relative operation-path order.
1024    pub fn entries(&self) -> &[FontReadEntry] {
1025        &self.entries
1026    }
1027
1028    /// Recovers the configured sources and exact read entries.
1029    pub fn into_parts(self) -> (Vec<FontSource>, Vec<FontReadEntry>) {
1030        (self.sources, self.entries)
1031    }
1032}
1033
1034impl fmt::Debug for FontRead {
1035    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1036        formatter
1037            .debug_struct("FontRead")
1038            .field("sources", &self.sources)
1039            .field("entries", &self.entries)
1040            .finish()
1041    }
1042}
1043
1044/// An unsupported yielded OpenDAL entry kind.
1045#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1046#[non_exhaustive]
1047pub enum FontReadEntryKind {
1048    Unknown,
1049}
1050
1051/// One structural issue found while surveying configured font prefixes.
1052#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
1053#[non_exhaustive]
1054pub enum FontReadIssue {
1055    #[error(
1056        "font source {source_index} listed operation path {operation_path:?} outside its prefix"
1057    )]
1058    ListedPathOutsidePrefix {
1059        source_index: usize,
1060        operation_path: String,
1061    },
1062    #[error(
1063        "font source {source_index} listed operation path {operation_path:?} as a prefix marker where a file is required"
1064    )]
1065    PrefixMarkerWhereFileRequired {
1066        source_index: usize,
1067        operation_path: String,
1068    },
1069    #[error(
1070        "font source {source_index} listed operation path {operation_path:?} with an empty relative path"
1071    )]
1072    EmptyRelativeOperationPath {
1073        source_index: usize,
1074        operation_path: String,
1075    },
1076    #[error("font source {source_index} listed invalid relative operation path {operation_path:?}")]
1077    InvalidRelativeOperationPath {
1078        source_index: usize,
1079        operation_path: String,
1080    },
1081    #[error("font source {source_index} listed object {operation_path:?} more than once")]
1082    DuplicateListedObject {
1083        source_index: usize,
1084        operation_path: String,
1085    },
1086    #[error(
1087        "font source {source_index} listed operation path {operation_path:?} with unsupported kind {kind:?}"
1088    )]
1089    UnsupportedEntryKind {
1090        source_index: usize,
1091        operation_path: String,
1092        kind: FontReadEntryKind,
1093    },
1094}
1095
1096/// The nonempty canonical set of structural font survey issues.
1097#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
1098#[error(
1099    "{message}",
1100    message = aggregate_issue_message(.issues.as_slice(), "font survey failed")
1101)]
1102pub struct FontReadSurveyError {
1103    issues: Vec<FontReadIssue>,
1104}
1105
1106impl FontReadSurveyError {
1107    /// Every independently detectable issue in source and path order.
1108    pub fn issues(&self) -> &[FontReadIssue] {
1109        &self.issues
1110    }
1111}
1112
1113/// Reads suffix-selected Font Containers from caller-ordered prefixes.
1114///
1115/// `.ttf`, `.ttc`, `.otf`, and `.otc` suffixes are matched
1116/// case-insensitively. Directory markers and non-font entries are ignored. All
1117/// selected entries come only from completed listing observations; those
1118/// observations make no storage snapshot or coexistence claim.
1119///
1120/// ```no_run
1121/// use typst_pack::{
1122///     DiscoverySpecification, FontCatalog, FontCatalogEntry, FontContainer,
1123///     PackCreationInput, PackageReadFailures, PackageCatalog,
1124///     ProjectSnapshot, create,
1125/// };
1126/// use typst_pack::opendal::OperatorBindings;
1127/// use typst_pack::opendal::pack_assembly::{
1128///     FontReadRequest, read_fonts,
1129/// };
1130///
1131/// async fn read_fonts_and_create(
1132///     bindings: &OperatorBindings,
1133///     request: &FontReadRequest,
1134///     project: &ProjectSnapshot,
1135///     packages: &PackageCatalog,
1136///     package_failures: &PackageReadFailures,
1137///     discovery: &DiscoverySpecification,
1138/// ) -> Result<(), Box<dyn std::error::Error>> {
1139///     let (_, read) = read_fonts(bindings, request).await?.into_parts();
1140///     let mut fonts = FontCatalog::new();
1141///     for entry in read {
1142///         let (_, _, _, disposition, bytes) = entry.into_parts();
1143///         let container = FontContainer::new(bytes)?;
1144///         fonts.push(FontCatalogEntry::new(container, disposition));
1145///     }
1146///     let _outcome = create(PackCreationInput {
1147///         project,
1148///         packages,
1149///         fonts: &fonts,
1150///         package_failures,
1151///         discovery,
1152///         metadata: None,
1153///     })?;
1154///     Ok(())
1155/// }
1156/// ```
1157#[allow(clippy::result_large_err)]
1158pub async fn read_fonts<R: OperatorResolver + ?Sized>(
1159    resolver: &R,
1160    request: &FontReadRequest,
1161) -> Result<FontRead, FontReadError> {
1162    let locations = request
1163        .sources()
1164        .iter()
1165        .map(FontSource::source)
1166        .collect::<Vec<_>>();
1167    let read = read_recursive_prefixes(
1168        resolver,
1169        &locations,
1170        RecursiveReadSelection::FontContainers,
1171        request.limits().into(),
1172        &FontReadOperation {
1173            sources: request.sources(),
1174        },
1175    )
1176    .await?;
1177
1178    let sources = request.sources().to_vec();
1179    let entries = read
1180        .into_iter()
1181        .enumerate()
1182        .flat_map(|(source_index, objects)| {
1183            let source = sources[source_index].clone();
1184            objects.into_iter().map(move |object| FontReadEntry {
1185                source_index,
1186                source: source.source.clone(),
1187                relative_path: object.relative_path,
1188                disposition: source.disposition,
1189                bytes: object.bytes,
1190            })
1191        })
1192        .collect();
1193
1194    Ok(FontRead { sources, entries })
1195}
1196
1197/// A failure while reading Font Containers through OpenDAL.
1198///
1199/// This error's own `Display` and `Debug` omit native resolver and OpenDAL
1200/// messages. Rendering its complete source chain may disclose backend context.
1201#[derive(Debug, thiserror::Error)]
1202#[error(
1203    "Font Read failed at source {source_index} for binding {binding} at prefix operation path {operation_path:?}{failed_path}: {cause}",
1204    binding = .source_location.binding(),
1205    operation_path = .source_location.operation_path(),
1206    failed_path = failed_path_context(.failed_path.as_deref()),
1207)]
1208pub struct FontReadError {
1209    source_index: usize,
1210    source_location: Location,
1211    failed_path: Option<String>,
1212    #[source]
1213    cause: RedactedError<FontReadErrorCause>,
1214}
1215
1216impl FontReadError {
1217    /// The caller-order index of the source at which read failed.
1218    pub fn source_index(&self) -> usize {
1219        self.source_index
1220    }
1221
1222    /// The normalized font prefix at which read failed.
1223    pub fn source_location(&self) -> &Location {
1224        &self.source_location
1225    }
1226
1227    /// The selected object's operation path when one object read failed.
1228    pub fn failed_path(&self) -> Option<&str> {
1229        self.failed_path.as_deref()
1230    }
1231
1232    /// The typed cause of this failure.
1233    pub fn cause(&self) -> &FontReadErrorCause {
1234        self.cause.inner()
1235    }
1236
1237    fn new(
1238        source_index: usize,
1239        source_location: &Location,
1240        failed_path: Option<String>,
1241        cause: FontReadErrorCause,
1242    ) -> Self {
1243        Self {
1244            source_index,
1245            source_location: source_location.clone(),
1246            failed_path,
1247            cause: RedactedError::new(cause),
1248        }
1249    }
1250}
1251
1252/// The typed cause of an OpenDAL Font Read failure.
1253#[derive(Debug, thiserror::Error)]
1254#[non_exhaustive]
1255pub enum FontReadErrorCause {
1256    #[error("operator resolution failed")]
1257    ResolveOperator(#[source] BoxError),
1258    #[error("required listing or read capability is unsupported")]
1259    UnsupportedCapabilities {
1260        list: bool,
1261        list_with_recursive: bool,
1262        read: bool,
1263    },
1264    #[error("a recursive listing failed")]
1265    List(#[source] ::opendal::Error),
1266    #[error("a listed Font Container read failed")]
1267    Read(#[source] ::opendal::Error),
1268    #[error("a listed Font Container was absent when read")]
1269    ListedObjectAbsent(#[source] ::opendal::Error),
1270    #[error("the completed listings had structural issues")]
1271    Structural(#[source] FontReadSurveyError),
1272    #[error("a Font Read limit failed")]
1273    Limit(#[source] FontReadLimitError),
1274}
1275
1276struct FontReadOperation<'a> {
1277    sources: &'a [FontSource],
1278}
1279
1280impl FontReadOperation<'_> {
1281    fn error(
1282        &self,
1283        source_index: usize,
1284        failed_path: Option<String>,
1285        cause: FontReadErrorCause,
1286    ) -> FontReadError {
1287        FontReadError::new(
1288            source_index,
1289            self.sources[source_index].source(),
1290            failed_path,
1291            cause,
1292        )
1293    }
1294}
1295
1296impl RecursiveReadOperation for FontReadOperation<'_> {
1297    type Error = FontReadError;
1298
1299    fn invalid_location_role(&self, _: usize, _: LocationRoleError) -> FontReadError {
1300        unreachable!("FontReadRequest validates every prefix role")
1301    }
1302
1303    fn resolve_operator(&self, source_index: usize, source: BoxError) -> FontReadError {
1304        self.error(
1305            source_index,
1306            None,
1307            FontReadErrorCause::ResolveOperator(source),
1308        )
1309    }
1310
1311    fn unsupported_capabilities(
1312        &self,
1313        source_index: usize,
1314        list: bool,
1315        list_with_recursive: bool,
1316        read: bool,
1317    ) -> FontReadError {
1318        self.error(
1319            source_index,
1320            None,
1321            FontReadErrorCause::UnsupportedCapabilities {
1322                list,
1323                list_with_recursive,
1324                read,
1325            },
1326        )
1327    }
1328
1329    fn list(&self, source_index: usize, source: ::opendal::Error) -> FontReadError {
1330        self.error(source_index, None, FontReadErrorCause::List(source))
1331    }
1332
1333    fn read(
1334        &self,
1335        source_index: usize,
1336        operation_path: String,
1337        source: ::opendal::Error,
1338    ) -> FontReadError {
1339        self.error(
1340            source_index,
1341            Some(operation_path),
1342            FontReadErrorCause::Read(source),
1343        )
1344    }
1345
1346    fn listed_object_absent(
1347        &self,
1348        source_index: usize,
1349        operation_path: String,
1350        source: ::opendal::Error,
1351    ) -> FontReadError {
1352        self.error(
1353            source_index,
1354            Some(operation_path),
1355            FontReadErrorCause::ListedObjectAbsent(source),
1356        )
1357    }
1358
1359    fn structural(&self, source_index: usize, issues: Vec<RecursiveSurveyIssue>) -> FontReadError {
1360        self.error(
1361            source_index,
1362            None,
1363            FontReadErrorCause::Structural(FontReadSurveyError {
1364                issues: issues.into_iter().map(map_font_issue).collect(),
1365            }),
1366        )
1367    }
1368
1369    fn limit(
1370        &self,
1371        source_index: usize,
1372        resource: RecursiveReadResource,
1373        ceiling: u64,
1374        _: u64,
1375    ) -> FontReadError {
1376        self.error(
1377            source_index,
1378            None,
1379            FontReadErrorCause::Limit(FontReadLimitError::exceeded(
1380                map_font_resource(resource),
1381                ceiling,
1382            )),
1383        )
1384    }
1385
1386    fn accounting_overflow(
1387        &self,
1388        source_index: usize,
1389        resource: RecursiveReadResource,
1390    ) -> FontReadError {
1391        self.error(
1392            source_index,
1393            None,
1394            FontReadErrorCause::Limit(FontReadLimitError::AccountingOverflow {
1395                resource: map_font_resource(resource),
1396            }),
1397        )
1398    }
1399}
1400
1401impl From<FontReadLimits> for RecursiveReadLimits {
1402    fn from(limits: FontReadLimits) -> Self {
1403        Self::new(
1404            limits.listed_entries(),
1405            limits.listed_path_bytes(),
1406            limits.total_listed_path_bytes(),
1407            limits.selected_containers(),
1408            limits.container_bytes(),
1409            limits.total_bytes(),
1410        )
1411    }
1412}
1413
1414fn map_font_resource(resource: RecursiveReadResource) -> FontReadResource {
1415    match resource {
1416        RecursiveReadResource::ListedEntries => FontReadResource::ListedEntries,
1417        RecursiveReadResource::ListedPathBytes => FontReadResource::ListedPathBytes,
1418        RecursiveReadResource::TotalListedPathBytes => FontReadResource::TotalListedPathBytes,
1419        RecursiveReadResource::SelectedObjects => FontReadResource::SelectedContainers,
1420        RecursiveReadResource::ObjectBytes => FontReadResource::ContainerBytes,
1421        RecursiveReadResource::TotalBytes => FontReadResource::TotalBytes,
1422        _ => unreachable!("unknown recursive read resource"),
1423    }
1424}
1425
1426fn map_font_issue(issue: RecursiveSurveyIssue) -> FontReadIssue {
1427    let source_index = issue.source_index;
1428    let operation_path = issue.operation_path;
1429    match issue.kind {
1430        RecursiveSurveyIssueKind::ListedPathOutsidePrefix => {
1431            FontReadIssue::ListedPathOutsidePrefix {
1432                source_index,
1433                operation_path,
1434            }
1435        }
1436        RecursiveSurveyIssueKind::PrefixMarkerWhereFileRequired => {
1437            FontReadIssue::PrefixMarkerWhereFileRequired {
1438                source_index,
1439                operation_path,
1440            }
1441        }
1442        RecursiveSurveyIssueKind::EmptyRelativeOperationPath => {
1443            FontReadIssue::EmptyRelativeOperationPath {
1444                source_index,
1445                operation_path,
1446            }
1447        }
1448        RecursiveSurveyIssueKind::InvalidRelativeOperationPath => {
1449            FontReadIssue::InvalidRelativeOperationPath {
1450                source_index,
1451                operation_path,
1452            }
1453        }
1454        RecursiveSurveyIssueKind::DuplicateListedObject => FontReadIssue::DuplicateListedObject {
1455            source_index,
1456            operation_path,
1457        },
1458        RecursiveSurveyIssueKind::UnsupportedEntryKind => FontReadIssue::UnsupportedEntryKind {
1459            source_index,
1460            operation_path,
1461            kind: FontReadEntryKind::Unknown,
1462        },
1463    }
1464}