Skip to main content

typst_pack/
fs_assembly.rs

1//! The reference filesystem Pack Assembler.
2//!
3//! The adapter reads and the core transforms. It lists and reads the
4//! project, composes the Font Catalog out of the font sources the
5//! host offers, obtains the Package Trees the core reports as
6//! missing, and resolves Document Time; Pack Creation itself runs in
7//! the core over those bytes.
8//!
9//! Each read value records the exact bytes observed by its source adapter;
10//! the project reader does not reread the source solely to establish that all
11//! values coexisted at one instant.
12
13#![cfg(feature = "fs")]
14
15use std::collections::HashSet;
16use std::fmt;
17use std::path::Path;
18use std::path::PathBuf;
19use std::sync::Arc;
20
21use ecow::EcoVec;
22use typst::diag::{FileError, FileResult, SourceDiagnostic};
23use typst::foundations::{Bytes, Datetime, Dict, Duration};
24use typst::syntax::package::PackageSpec;
25use typst::syntax::{FileId, RootedPath, Source, VirtualPath, VirtualRoot};
26use typst::text::{Font, FontBook};
27use typst::utils::LazyHash;
28use typst::{Feature, Library, LibraryExt, World};
29use typst_kit::files::{FileLoader, FileStore};
30use typst_kit::fonts::FontStore;
31
32use crate::creation::{
33    DiscoverySpecification, PackCreationError, PackCreationInput, PackCreationOutcome, create,
34};
35use crate::domain::{DocumentTime, TypstTarget};
36use crate::font_catalog::FontDisposition;
37use crate::fs_fonts::{FilesystemFontLimits, FilesystemFontSource, read_filesystem_fonts};
38use crate::fs_packages::{
39    FilesystemPackageAuthority, FilesystemPackageAuthorityReadError, FilesystemPackageLimits,
40    ReadPackages,
41};
42use crate::fs_project;
43use crate::manifest::PackMetadata;
44use crate::pack::Pack;
45use crate::package_catalog::{PackageCatalog, PackageCatalogError, PackageDisposition};
46use crate::package_failure::PackageReadFailures;
47use crate::project_snapshot::ProjectSnapshot;
48
49/// Named finite resource policy for one filesystem Pack Assembly run.
50#[derive(Clone, Copy, Debug, Eq, PartialEq)]
51pub struct FilesystemPackAssemblyProfile {
52    project: fs_project::FilesystemProjectLimits,
53    packages: FilesystemPackageLimits,
54    fonts: FilesystemFontLimits,
55    #[cfg(feature = "egress")]
56    package_expansion: crate::PackageExpansionLimits,
57}
58
59impl FilesystemPackAssemblyProfile {
60    /// The first-party finite profile used by ordinary filesystem workflows.
61    pub const fn reference_v1() -> Self {
62        Self {
63            project: fs_project::FilesystemProjectLimits::reference_v1(),
64            packages: FilesystemPackageLimits::reference_v1(),
65            fonts: FilesystemFontLimits::reference_v1(),
66            #[cfg(feature = "egress")]
67            package_expansion: crate::PackageExpansionLimits::reference_v1(),
68        }
69    }
70
71    pub const fn project(&self) -> fs_project::FilesystemProjectLimits {
72        self.project
73    }
74
75    pub const fn packages(&self) -> FilesystemPackageLimits {
76        self.packages
77    }
78
79    pub const fn fonts(&self) -> FilesystemFontLimits {
80        self.fonts
81    }
82
83    #[cfg(feature = "egress")]
84    pub const fn package_expansion(&self) -> crate::PackageExpansionLimits {
85        self.package_expansion
86    }
87}
88
89/// Clock policy used when a run does not supply an exact Document Time.
90#[derive(Clone, Copy, Debug, Default, PartialEq)]
91#[non_exhaustive]
92pub enum FilesystemPackAssemblyClock {
93    /// Resolve the current UNIX timestamp from the host clock for each run.
94    #[default]
95    System,
96    /// Reuse one exact configured Document Time for every run.
97    Fixed(DocumentTime),
98}
99
100/// Reusable host policy for the reference filesystem Pack Assembler.
101#[derive(Debug)]
102pub struct FilesystemPackAssemblerConfig {
103    font_paths: Vec<PathBuf>,
104    system_fonts: bool,
105    typst_embedded_fonts: bool,
106    package_path: Option<PathBuf>,
107    package_cache_path: Option<PathBuf>,
108    offline: bool,
109    #[cfg(feature = "egress")]
110    certificate: Option<PathBuf>,
111    clock: FilesystemPackAssemblyClock,
112    profile: FilesystemPackAssemblyProfile,
113}
114
115impl FilesystemPackAssemblerConfig {
116    /// Starts with ordinary first-party host policy and the reference-v1
117    /// finite profile.
118    pub fn new() -> Self {
119        Self {
120            font_paths: Vec::new(),
121            system_fonts: true,
122            typst_embedded_fonts: true,
123            package_path: None,
124            package_cache_path: None,
125            offline: false,
126            #[cfg(feature = "egress")]
127            certificate: None,
128            clock: FilesystemPackAssemblyClock::System,
129            profile: FilesystemPackAssemblyProfile::reference_v1(),
130        }
131    }
132
133    /// Selects the finite resource policy applied to every run.
134    pub fn profile(mut self, profile: FilesystemPackAssemblyProfile) -> Self {
135        self.profile = profile;
136        self
137    }
138
139    /// Selects how omitted per-run Document Time is resolved.
140    pub fn clock(mut self, clock: FilesystemPackAssemblyClock) -> Self {
141        self.clock = clock;
142        self
143    }
144
145    /// Adds a directory to the configured Font Authority.
146    pub fn font_path(mut self, path: impl Into<PathBuf>) -> Self {
147        self.font_paths.push(path.into());
148        self
149    }
150
151    /// Whether the configured Font Authority scans host system fonts.
152    pub fn system_fonts(mut self, system: bool) -> Self {
153        self.system_fonts = system;
154        self
155    }
156
157    /// Whether the configured Font Authority offers Typst's embedded fonts.
158    pub fn typst_embedded_fonts(mut self, include: bool) -> Self {
159        self.typst_embedded_fonts = include;
160        self
161    }
162
163    /// Overrides the directory in which locally installed packages are
164    /// searched (namespace/name/version layout).
165    pub fn package_path(mut self, path: impl Into<PathBuf>) -> Self {
166        self.package_path = Some(path.into());
167        self
168    }
169
170    /// Overrides the directory in which downloaded packages are cached.
171    ///
172    /// This configures both cache reads and the destination of successful
173    /// downloads. A build without egress can still read the selected cache.
174    pub fn package_cache_path(mut self, path: impl Into<PathBuf>) -> Self {
175        self.package_cache_path = Some(path.into());
176        self
177    }
178
179    /// Disallows network access during creation. Defaults to
180    /// `false`.
181    ///
182    /// When enabled, package dependencies must already exist in the local
183    /// package directories or package cache; anything that would need to be
184    /// downloaded fails the compile as not found. A build without the `egress`
185    /// feature behaves this way regardless, having no transport to reach the
186    /// network with, so setting this stays available either way.
187    pub fn offline(mut self, offline: bool) -> Self {
188        self.offline = offline;
189        self
190    }
191
192    /// Configures a custom CA certificate for package downloads.
193    ///
194    /// Only a download presents a certificate to verify, so this needs the
195    /// `egress` feature.
196    #[cfg(feature = "egress")]
197    pub fn certificate(mut self, path: Option<PathBuf>) -> Self {
198        self.certificate = path;
199        self
200    }
201}
202
203impl Default for FilesystemPackAssemblerConfig {
204    fn default() -> Self {
205        Self::new()
206    }
207}
208
209/// Per-run roots, Discovery Specification controls, embedding choices, and
210/// Pack metadata.
211pub struct FilesystemPackAssemblyRequest<'a> {
212    root: &'a Path,
213    entrypoint: &'a Path,
214    vendor_packages: bool,
215    embed_fonts: bool,
216    include_typst_embedded_fonts: bool,
217    inputs: Dict,
218    features: Vec<Feature>,
219    target: TypstTarget,
220    document_time: Option<DocumentTime>,
221    timings: Option<PathBuf>,
222    metadata: Option<PackMetadata>,
223}
224
225impl<'a> FilesystemPackAssemblyRequest<'a> {
226    /// Starts one run for an entrypoint that is absolute or relative to `root`.
227    pub fn new(root: &'a Path, entrypoint: &'a Path) -> Self {
228        Self {
229            root,
230            entrypoint,
231            vendor_packages: true,
232            embed_fonts: false,
233            include_typst_embedded_fonts: false,
234            inputs: Dict::new(),
235            features: Vec::new(),
236            target: TypstTarget::Paged,
237            document_time: None,
238            timings: None,
239            metadata: None,
240        }
241    }
242
243    /// Whether selected Package Trees are embedded in the Pack.
244    pub fn vendor_packages(mut self, vendor: bool) -> Self {
245        self.vendor_packages = vendor;
246        self
247    }
248
249    /// Whether selected scanned and system Font Containers are embedded.
250    pub fn embed_fonts(mut self, embed: bool) -> Self {
251        self.embed_fonts = embed;
252        self
253    }
254
255    /// Whether embedding includes selected Typst-embedded Font Containers.
256    pub fn include_typst_embedded_fonts(mut self, include: bool) -> Self {
257        self.include_typst_embedded_fonts = include;
258        self
259    }
260
261    /// Values made available to document code as `sys.inputs` during discovery.
262    pub fn inputs(mut self, inputs: Dict) -> Self {
263        self.inputs = inputs;
264        self
265    }
266
267    /// Enables one Typst engine feature during discovery.
268    pub fn feature(mut self, feature: Feature) -> Self {
269        self.features.push(feature);
270        self
271    }
272
273    /// Selects the Typst Target for discovery.
274    pub fn target(mut self, target: TypstTarget) -> Self {
275        self.target = target;
276        self
277    }
278
279    /// Supplies an exact Document Time instead of resolving the host clock.
280    pub fn document_time(mut self, document_time: DocumentTime) -> Self {
281        self.document_time = Some(document_time);
282        self
283    }
284
285    /// Writes creation performance timings to a Perfetto-compatible JSON file.
286    pub fn timings(mut self, path: Option<PathBuf>) -> Self {
287        self.timings = path;
288        self
289    }
290
291    /// Sets descriptive metadata recorded in the pack manifest.
292    pub fn metadata(mut self, metadata: PackMetadata) -> Self {
293        self.metadata = Some(metadata);
294        self
295    }
296}
297
298/// Reusable filesystem Pack Assembly over configured concrete authorities.
299///
300/// One assembler holds the host policy — font paths, package directories, the
301/// clock, and the finite profile — and serves many requests. It runs Dependency
302/// Discovery and repeats [`create`](crate::create) until it produces a Pack.
303///
304/// ```no_run
305/// use std::path::Path;
306///
307/// use typst_pack::pack_archive::encode;
308/// use typst_pack::{
309///     FilesystemPackAssembler, FilesystemPackAssemblerConfig, FilesystemPackAssemblyRequest,
310/// };
311///
312/// # fn run() -> Result<(), Box<dyn std::error::Error>> {
313/// let assembler = FilesystemPackAssembler::new(FilesystemPackAssemblerConfig::new());
314/// let report = assembler.assemble(
315///     FilesystemPackAssemblyRequest::new(Path::new("path/to/project"), Path::new("main.typ"))
316///         .embed_fonts(true),
317/// )?;
318///
319/// for warning in report.warnings() {
320///     eprintln!("discovery warning: {}", warning.message);
321/// }
322///
323/// let archive = encode(report.pack())?;
324/// std::fs::write("project.typk", archive.as_slice())?;
325/// # Ok(())
326/// # }
327/// ```
328pub struct FilesystemPackAssembler {
329    authority: FilesystemPackageAuthority,
330    font_paths: Vec<PathBuf>,
331    system_fonts: bool,
332    typst_embedded_fonts: bool,
333    clock: FilesystemPackAssemblyClock,
334    profile: FilesystemPackAssemblyProfile,
335    #[cfg(test)]
336    after_creation_hook: Option<Box<dyn Fn()>>,
337}
338
339impl FilesystemPackAssembler {
340    /// Configures the concrete project, package, font, clock, and finite-profile
341    /// policy reused by each assembly request.
342    pub fn new(config: FilesystemPackAssemblerConfig) -> Self {
343        let authority = FilesystemPackageAuthority::with_limits(
344            config.package_path.as_deref(),
345            config.package_cache_path.as_deref(),
346            config.offline,
347            config.profile.packages,
348            #[cfg(feature = "egress")]
349            config.profile.package_expansion,
350        );
351        #[cfg(feature = "egress")]
352        let authority = authority.certificate(config.certificate);
353        Self {
354            authority,
355            font_paths: config.font_paths,
356            system_fonts: config.system_fonts,
357            typst_embedded_fonts: config.typst_embedded_fonts,
358            clock: config.clock,
359            profile: config.profile,
360            #[cfg(test)]
361            after_creation_hook: None,
362        }
363    }
364
365    #[cfg(test)]
366    pub(crate) fn after_creation_hook(mut self, hook: impl Fn() + 'static) -> Self {
367        self.after_creation_hook = Some(Box::new(hook));
368        self
369    }
370
371    /// Reads one Project Snapshot and Font Catalog, then resolves exactly the
372    /// packages reported between stateless Pack Creation invocations.
373    pub fn assemble(
374        &self,
375        request: FilesystemPackAssemblyRequest<'_>,
376    ) -> Result<PackAssemblyReport, FilesystemPackAssemblyError> {
377        let (result, timing_error) = self.assemble_with_timing(request);
378        timing_error.map_or(result, Err)
379    }
380
381    #[doc(hidden)]
382    pub fn assemble_with_timing(
383        &self,
384        request: FilesystemPackAssemblyRequest<'_>,
385    ) -> (
386        Result<PackAssemblyReport, FilesystemPackAssemblyError>,
387        Option<FilesystemPackAssemblyError>,
388    ) {
389        let mut timing_error = None;
390        let result = self.assemble_inner(request, &mut timing_error);
391        (result, timing_error)
392    }
393
394    fn assemble_inner(
395        &self,
396        request: FilesystemPackAssemblyRequest<'_>,
397        timing_error: &mut Option<FilesystemPackAssemblyError>,
398    ) -> Result<PackAssemblyReport, FilesystemPackAssemblyError> {
399        let root = request.root.canonicalize().map_err(|err| {
400            FilesystemPackAssemblyError::io("failed to resolve project root", err)
401        })?;
402        let entrypoint_abs = if request.entrypoint.is_absolute() {
403            request.entrypoint.to_owned()
404        } else {
405            root.join(request.entrypoint)
406        };
407        let entrypoint_abs = entrypoint_abs
408            .canonicalize()
409            .map_err(|err| FilesystemPackAssemblyError::io("failed to resolve entrypoint", err))?;
410        let entrypoint = VirtualPath::virtualize(&root, &entrypoint_abs)
411            .map_err(|_| FilesystemPackAssemblyError::OutsideRoot(entrypoint_abs.clone()))?;
412        let snapshot = Arc::new(fs_project::read_filesystem_project(
413            &root,
414            entrypoint.get_without_slash(),
415            self.profile.project,
416        )?);
417
418        let packages = Arc::new(ReadPackages::new());
419
420        let scanned_disposition = FontDisposition::embedded_if(request.embed_fonts);
421        let mut font_sources = Vec::new();
422        if self.system_fonts {
423            font_sources.push(FilesystemFontSource::system(scanned_disposition));
424        }
425        #[cfg(feature = "embedded-fonts")]
426        if self.typst_embedded_fonts {
427            font_sources.push(FilesystemFontSource::typst_embedded(
428                FontDisposition::embedded_if(
429                    request.embed_fonts && request.include_typst_embedded_fonts,
430                ),
431            ));
432        }
433        #[cfg(not(feature = "embedded-fonts"))]
434        let _ = (
435            self.typst_embedded_fonts,
436            request.include_typst_embedded_fonts,
437        );
438        font_sources.extend(
439            self.font_paths
440                .iter()
441                .map(|path| FilesystemFontSource::directory(path, scanned_disposition)),
442        );
443        let font_catalog = read_filesystem_fonts(font_sources, self.profile.fonts)?;
444
445        // The core consults no wall clock, so the adapter resolves the
446        // representative request's Document Time from the host's.
447        let document_time = request
448            .document_time
449            .unwrap_or_else(|| self.clock.document_time());
450
451        let discovery = DiscoverySpecification::new(
452            request.target,
453            request.inputs,
454            document_time,
455            request.features,
456        )
457        .map_err(|source| {
458            FilesystemPackAssemblyError::DiscoverySpecification(
459                FilesystemPackAssemblyDiscoveryError { source },
460            )
461        })?;
462
463        let mut world = ReadWorld {
464            root: root.clone(),
465            #[cfg(feature = "diagnostics")]
466            workdir: std::env::current_dir()
467                .ok()
468                .map(|path| path.canonicalize().unwrap_or(path)),
469            library: LazyHash::new(Library::builder().build()),
470            main: RootedPath::new(VirtualRoot::Project, entrypoint).intern(),
471            files: FileStore::new(ReadLoader {
472                project: Arc::clone(&snapshot),
473                packages: Arc::clone(&packages),
474            }),
475            fonts: FontStore::new(),
476        };
477
478        let disposition = if request.vendor_packages {
479            PackageDisposition::Embedded
480        } else {
481            PackageDisposition::External
482        };
483        let mut timer = typst_kit::timer::Timer::new_or_placeholder(request.timings);
484        let mut creation = None;
485        let timings = timer.record(&mut world, |_| {
486            creation = Some(resolve_and_create(
487                &snapshot,
488                &font_catalog,
489                &discovery,
490                request.metadata.as_ref(),
491                &self.authority,
492                &packages,
493                disposition,
494            ));
495        });
496        let Some(creation) = creation else {
497            return Err(FilesystemPackAssemblyError::Timings(
498                timings
499                    .expect_err("timer did not execute creation")
500                    .to_string(),
501            ));
502        };
503        *timing_error = timings
504            .err()
505            .map(|error| FilesystemPackAssemblyError::Timings(error.to_string()));
506        let (pack, warnings) = match creation {
507            Ok(created) => created,
508            Err(error) => return Err(error.into_assembly_error(world)),
509        };
510
511        #[cfg(test)]
512        if let Some(hook) = &self.after_creation_hook {
513            hook();
514        }
515
516        Ok(PackAssemblyReport {
517            pack,
518            warnings,
519            #[cfg(feature = "diagnostics")]
520            world,
521        })
522    }
523}
524
525impl FilesystemPackAssemblyClock {
526    fn document_time(self) -> DocumentTime {
527        match self {
528            Self::System => {
529                let timestamp = std::time::SystemTime::now()
530                    .duration_since(std::time::UNIX_EPOCH)
531                    .map_or(0, |duration| duration.as_secs() as i64);
532                DocumentTime::UnixTimestamp(timestamp)
533            }
534            Self::Fixed(document_time) => document_time,
535        }
536    }
537}
538
539/// Runs creation over the read inputs, resolving what it reports as
540/// missing, until it issues a Pack.
541///
542/// Package requirements can only be discovered by compiling, so each round
543/// reports the exact specifications no supplied tree covers, the adapter
544/// obtains them through the Package Authority, and creation runs again over
545/// the larger set. Every round therefore either issues a Pack, adds a tree the
546/// request did not have, declares one the Package Authority could not resolve,
547/// or fails.
548///
549/// A specification the authority cannot resolve is declared rather than
550/// returned. The next round's representative request then fails at the import
551/// that needed it, carrying the authority's own reason, so an unresolvable
552/// package is reported where the document asked for it exactly as it was
553/// before package resolution moved out of the representative compile.
554fn resolve_and_create(
555    project: &ProjectSnapshot,
556    fonts: &crate::font_catalog::FontCatalog,
557    discovery: &DiscoverySpecification,
558    metadata: Option<&PackMetadata>,
559    authority: &FilesystemPackageAuthority,
560    packages: &ReadPackages,
561    disposition: PackageDisposition,
562) -> Result<(Pack, EcoVec<SourceDiagnostic>), CreationFailure> {
563    let mut attempted_specs: HashSet<String> = HashSet::new();
564    let mut package_failures = Vec::new();
565    let mut read_failures = PackageReadFailures::new();
566    let mut catalog = PackageCatalog::new();
567    loop {
568        let outcome = create(PackCreationInput {
569            project,
570            packages: &catalog,
571            fonts,
572            package_failures: &read_failures,
573            discovery,
574            metadata,
575        })
576        .map_err(|error| CreationFailure::Core {
577            error,
578            package_failures: std::mem::take(&mut package_failures),
579        })?;
580        match outcome {
581            PackCreationOutcome::Created { pack, warnings } => return Ok((pack, warnings)),
582            PackCreationOutcome::MissingPackageSpecifications(missing) => {
583                for spec in missing {
584                    if !attempted_specs.insert(spec.to_string()) {
585                        // Creation reports neither what a supplied tree covers
586                        // nor what was declared unresolvable, so this cannot
587                        // repeat; failing keeps that a diagnosis rather than a
588                        // loop that never progresses.
589                        return Err(CreationFailure::Adapter(
590                            FilesystemPackAssemblyError::Package {
591                                message: "the representative creation compile did not accept the \
592                                      resolved package tree"
593                                    .to_owned(),
594                                spec,
595                            },
596                        ));
597                    }
598                    match authority.read(&spec) {
599                        Ok(read) => {
600                            let (tree, _) = read.into_parts();
601                            packages.record(spec.clone(), tree.clone());
602                            read_failures.remove(&spec);
603                            catalog
604                                .insert(spec.clone(), tree, disposition)
605                                .map_err(FilesystemPackAssemblyError::InvalidPackageCatalog)?;
606                        }
607                        Err(error) => {
608                            read_failures.insert(error.failure().clone());
609                            package_failures.push(error);
610                        }
611                    }
612                }
613            }
614        }
615    }
616}
617
618/// A failure that ended one creation loop, before the adapter dressed it in
619/// its own vocabulary.
620enum CreationFailure {
621    /// The core issued no Pack.
622    Core {
623        error: PackCreationError,
624        package_failures: Vec<FilesystemPackageAuthorityReadError>,
625    },
626    /// The adapter failed to read what the core reported as missing.
627    Adapter(FilesystemPackAssemblyError),
628}
629
630impl CreationFailure {
631    /// Reports the failure in the filesystem adapter's vocabulary, handing a
632    /// failed representative compile the sources that render its diagnostics.
633    fn into_assembly_error(self, world: ReadWorld) -> FilesystemPackAssemblyError {
634        match self {
635            Self::Adapter(error) => error,
636            Self::Core {
637                error,
638                package_failures,
639            } => FilesystemPackAssemblyError::Creation(FilesystemPackAssemblyCreationError {
640                context: Box::new(PackAssemblyDiagnosticContext { world }),
641                error,
642                package_failures,
643            }),
644        }
645    }
646}
647
648impl From<FilesystemPackAssemblyError> for CreationFailure {
649    fn from(error: FilesystemPackAssemblyError) -> Self {
650        Self::Adapter(error)
651    }
652}
653
654/// The terminal report of a successful filesystem Pack Assembly run.
655pub struct PackAssemblyReport {
656    pack: Pack,
657    warnings: EcoVec<SourceDiagnostic>,
658    #[cfg(feature = "diagnostics")]
659    pub(crate) world: ReadWorld,
660}
661
662impl PackAssemblyReport {
663    /// The assembled, authoritatively validated Pack.
664    pub fn pack(&self) -> &Pack {
665        &self.pack
666    }
667
668    /// Warnings emitted by the successful Dependency Discovery run.
669    pub fn warnings(&self) -> &[SourceDiagnostic] {
670        &self.warnings
671    }
672
673    /// Recovers the Pack and discovery warnings.
674    pub fn into_parts(self) -> (Pack, EcoVec<SourceDiagnostic>) {
675        (self.pack, self.warnings)
676    }
677}
678
679/// Opaque source context retained for first-party creation diagnostics.
680///
681/// This value intentionally does not implement Typst's [`World`] interface.
682#[derive(Debug)]
683pub struct PackAssemblyDiagnosticContext {
684    #[cfg_attr(not(feature = "diagnostics"), allow(dead_code))]
685    pub(crate) world: ReadWorld,
686}
687
688/// A Pack Creation failure retained by the filesystem Pack Assembler.
689#[derive(Debug, thiserror::Error)]
690#[error("{error}")]
691pub struct FilesystemPackAssemblyCreationError {
692    context: Box<PackAssemblyDiagnosticContext>,
693    #[source]
694    error: PackCreationError,
695    package_failures: Vec<FilesystemPackageAuthorityReadError>,
696}
697
698impl FilesystemPackAssemblyCreationError {
699    /// Opaque source context for first-party diagnostic rendering.
700    pub fn context(&self) -> &PackAssemblyDiagnosticContext {
701        &self.context
702    }
703
704    /// The unchanged core Pack Creation failure.
705    pub fn error(&self) -> &PackCreationError {
706        &self.error
707    }
708
709    /// Package Authority failures from the same assembly attempt.
710    pub fn package_failures(&self) -> &[FilesystemPackageAuthorityReadError] {
711        &self.package_failures
712    }
713
714    /// Recovers the diagnostic context, core error, and authority failures.
715    pub fn into_parts(
716        self,
717    ) -> (
718        Box<PackAssemblyDiagnosticContext>,
719        PackCreationError,
720        Vec<FilesystemPackageAuthorityReadError>,
721    ) {
722        (self.context, self.error, self.package_failures)
723    }
724}
725
726/// An invalid Discovery Specification retained by filesystem Pack Assembly.
727#[derive(Debug, thiserror::Error)]
728#[error("invalid Discovery Specification: {source}")]
729pub struct FilesystemPackAssemblyDiscoveryError {
730    #[source]
731    source: crate::creation::DiscoverySpecificationError,
732}
733
734impl FilesystemPackAssemblyDiscoveryError {
735    /// The unchanged Discovery Specification construction failure.
736    pub fn source_error(&self) -> &crate::creation::DiscoverySpecificationError {
737        &self.source
738    }
739
740    /// Recovers the Discovery Specification construction failure.
741    pub fn into_source(self) -> crate::creation::DiscoverySpecificationError {
742        self.source
743    }
744}
745
746/// A failure while packing a project directory.
747#[derive(Debug, thiserror::Error)]
748#[non_exhaustive]
749pub enum FilesystemPackAssemblyError {
750    #[error("{message}: {source}")]
751    Io {
752        message: String,
753        #[source]
754        source: std::io::Error,
755    },
756    #[error("`{0}` is outside the project root and cannot be packed")]
757    OutsideRoot(PathBuf),
758    #[error(transparent)]
759    Creation(FilesystemPackAssemblyCreationError),
760    #[error(transparent)]
761    DiscoverySpecification(FilesystemPackAssemblyDiscoveryError),
762    #[error("failed to write creation timings: {0}")]
763    Timings(String),
764    #[error("failed to load package {spec}: {message}")]
765    Package { spec: PackageSpec, message: String },
766    /// The read Package Trees do not form a valid Package Catalog.
767    #[error(transparent)]
768    InvalidPackageCatalog(PackageCatalogError),
769    #[error(transparent)]
770    ProjectRead(#[from] fs_project::FilesystemProjectReadError),
771    #[error(transparent)]
772    FontRead(#[from] crate::fs_fonts::FilesystemFontReadError),
773}
774
775impl FilesystemPackAssemblyError {
776    pub(crate) fn io(message: &str, source: std::io::Error) -> Self {
777        Self::Io {
778            message: message.to_owned(),
779            source,
780        }
781    }
782}
783
784/// The bytes one creation read, as a world.
785///
786/// It compiles nothing: the representative request runs in the core, over the
787/// same bytes. This world exists so that what the adapter read can still be
788/// presented afterwards — creation diagnostics render their source context and
789/// timing spans resolve their file and line from here, without reading the
790/// project or a package tree a second time.
791pub(crate) struct ReadWorld {
792    root: PathBuf,
793    #[cfg(feature = "diagnostics")]
794    workdir: Option<PathBuf>,
795    library: LazyHash<Library>,
796    main: FileId,
797    files: FileStore<ReadLoader>,
798    fonts: FontStore,
799}
800
801impl ReadWorld {
802    /// The canonicalized project root.
803    #[cfg(feature = "diagnostics")]
804    fn root(&self) -> &Path {
805        &self.root
806    }
807
808    #[cfg(feature = "diagnostics")]
809    fn workdir(&self) -> Option<&Path> {
810        self.workdir.as_deref()
811    }
812}
813
814impl fmt::Debug for ReadWorld {
815    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
816        f.debug_struct("ReadWorld")
817            .field("root", &self.root)
818            .finish_non_exhaustive()
819    }
820}
821
822impl World for ReadWorld {
823    fn library(&self) -> &LazyHash<Library> {
824        &self.library
825    }
826
827    /// No face at all: presentation resolves file requests, never fonts.
828    fn book(&self) -> &LazyHash<FontBook> {
829        self.fonts.book()
830    }
831
832    fn main(&self) -> FileId {
833        self.main
834    }
835
836    fn source(&self, id: FileId) -> FileResult<Source> {
837        self.files.source(id)
838    }
839
840    fn file(&self, id: FileId) -> FileResult<Bytes> {
841        self.files.file(id)
842    }
843
844    fn font(&self, _index: usize) -> Option<Font> {
845        None
846    }
847
848    /// Absent: the representative request's Document Time is the core's, and
849    /// presentation evaluates no document code.
850    fn today(&self, _offset: Option<Duration>) -> Option<Datetime> {
851        None
852    }
853}
854
855#[cfg(feature = "diagnostics")]
856impl typst_kit::diagnostics::DiagnosticWorld for ReadWorld {
857    fn name(&self, id: FileId) -> String {
858        match id.root() {
859            VirtualRoot::Project => id
860                .vpath()
861                .realize(self.root())
862                .ok()
863                .and_then(|path| relative_path(&path, self.workdir()?))
864                .map(|path| path.to_string_lossy().into_owned())
865                .unwrap_or_else(|| display_file_id(id)),
866            VirtualRoot::Package(_) => display_file_id(id),
867        }
868    }
869}
870
871#[cfg(feature = "diagnostics")]
872fn display_file_id(id: FileId) -> String {
873    match id.root() {
874        VirtualRoot::Project => id.vpath().get_without_slash().to_owned(),
875        VirtualRoot::Package(spec) => format!("{spec}{}", id.vpath().get_with_slash()),
876    }
877}
878
879#[cfg(feature = "diagnostics")]
880fn relative_path(path: &Path, base: &Path) -> Option<PathBuf> {
881    if path.is_absolute() != base.is_absolute() {
882        return path.is_absolute().then(|| path.to_path_buf());
883    }
884
885    let mut path_components = path.components();
886    let mut base_components = base.components();
887    let mut relative = Vec::new();
888    loop {
889        match (path_components.next(), base_components.next()) {
890            (None, None) => break,
891            (Some(component), None) => {
892                relative.push(component);
893                relative.extend(path_components.by_ref());
894                break;
895            }
896            (None, Some(_)) => relative.push(std::path::Component::ParentDir),
897            (Some(path), Some(base)) if relative.is_empty() && path == base => {}
898            (Some(path), Some(std::path::Component::CurDir)) => relative.push(path),
899            (Some(_), Some(std::path::Component::ParentDir)) => return None,
900            (Some(std::path::Component::Prefix(_) | std::path::Component::RootDir), Some(_))
901            | (Some(_), Some(std::path::Component::Prefix(_) | std::path::Component::RootDir)) => {
902                return path.is_absolute().then(|| path.to_path_buf());
903            }
904            (Some(path), Some(_)) => {
905                relative.push(std::path::Component::ParentDir);
906                relative.extend(base_components.map(|_| std::path::Component::ParentDir));
907                relative.push(path);
908                relative.extend(path_components.by_ref());
909                break;
910            }
911        }
912    }
913
914    Some(relative.iter().map(|part| part.as_os_str()).collect())
915}
916
917/// Serves file requests from the bytes the adapter read, and from nothing
918/// else: no request reaches the filesystem a second time.
919struct ReadLoader {
920    project: Arc<ProjectSnapshot>,
921    packages: Arc<ReadPackages>,
922}
923
924impl FileLoader for ReadLoader {
925    fn load(&self, id: FileId) -> FileResult<Bytes> {
926        let path = id.vpath().get_without_slash();
927        match id.root() {
928            VirtualRoot::Project => self.project.shared_file(path).map(|data| data.to_typst()),
929            VirtualRoot::Package(spec) => self.packages.file(spec, path),
930        }
931        .ok_or_else(|| FileError::NotFound(PathBuf::from(path)))
932    }
933}