Skip to main content

typst_pack/
pack.rs

1//! The validated in-memory Pack model.
2
3use std::collections::{BTreeMap, BTreeSet};
4use std::str::FromStr;
5
6use typst::syntax::package::PackageSpec;
7use typst::text::{Font, FontInfo};
8
9use crate::manifest::PackMetadata;
10use crate::paths::{
11    CanonicalPath, canonical_relative_path, path_tree_conflicts as shared_path_tree_conflicts,
12};
13use crate::payload::SharedBytes;
14use crate::{CanonicalIdentity, CanonicalIdentityRole, FontContainer, PackageTree};
15
16/// The conventional file extension for packs.
17pub const FILE_EXTENSION: &str = "typk";
18
19/// Whether any segment of a root-relative path names a Pack.
20pub(crate) fn names_pack_path(path: &str) -> bool {
21    path.split('/').any(|segment| {
22        segment.strip_prefix('.') == Some(FILE_EXTENSION)
23            || std::path::Path::new(segment)
24                .extension()
25                .is_some_and(|extension| extension == FILE_EXTENSION)
26    })
27}
28
29/// A portable pack of a Typst project.
30///
31/// A pack holds project files (sources, images, and data files), optionally
32/// package files and fonts. Every project path has contained bytes.
33/// Its archive form is a Zip file with a `typst-pack.toml`
34/// manifest, conventionally named `*.typk`.
35#[derive(Debug, Clone)]
36pub struct Pack {
37    identity: CanonicalIdentity,
38    entrypoint: CanonicalPath,
39    metadata: Option<PackMetadata>,
40    files: BTreeMap<CanonicalPath, SharedBytes>,
41    /// Vendored packages, keyed by spec string for deterministic order.
42    packages: BTreeMap<String, PackageFiles>,
43    package_requirements: Vec<PackageRequirement>,
44    fonts: Vec<PackFont>,
45    font_catalog: Vec<PackFontCatalogFace>,
46    font_requirements: Vec<FontRequirement>,
47}
48
49#[derive(Debug, Clone)]
50pub(crate) struct PackageFiles {
51    pub(crate) spec: PackageSpec,
52    files: BTreeMap<CanonicalPath, SharedBytes>,
53}
54
55impl PackageFiles {
56    pub(crate) fn file(&self, path: &str) -> Option<&SharedBytes> {
57        self.files.get(path)
58    }
59
60    fn from_validated_tree(spec: PackageSpec, tree: PackageTree) -> Self {
61        Self {
62            spec,
63            files: tree
64                .into_shared_files()
65                .into_iter()
66                .map(|(path, data)| (CanonicalPath::from_canonical(path), data))
67                .collect(),
68        }
69    }
70}
71
72/// Exact verified dependencies accepted by the synchronous Compilation Kernel.
73pub(crate) struct CompilationDependencySnapshot {
74    pack_identity: CanonicalIdentity,
75    packages: BTreeMap<String, PackageFiles>,
76    font_catalog: Vec<Font>,
77}
78
79impl CompilationDependencySnapshot {
80    pub(crate) fn pack_identity(&self) -> CanonicalIdentity {
81        self.pack_identity
82    }
83
84    pub(crate) fn into_parts(self) -> (BTreeMap<String, PackageFiles>, Vec<Font>) {
85        (self.packages, self.font_catalog)
86    }
87}
88
89/// One exact package specification and Package Tree identity.
90#[derive(Debug, Clone, PartialEq, Eq)]
91pub struct PackageRequirement {
92    spec: PackageSpec,
93    tree: CanonicalIdentity,
94    file_count: u64,
95    byte_length: u64,
96    embedded: bool,
97}
98
99impl PackageRequirement {
100    pub fn spec(&self) -> &PackageSpec {
101        &self.spec
102    }
103    pub fn tree_identity(&self) -> CanonicalIdentity {
104        self.tree
105    }
106    pub fn file_count(&self) -> u64 {
107        self.file_count
108    }
109    pub fn byte_length(&self) -> u64 {
110        self.byte_length
111    }
112    pub fn is_embedded(&self) -> bool {
113        self.embedded
114    }
115}
116
117/// A font embedded in a pack.
118#[derive(Debug, Clone)]
119pub struct PackFont {
120    identity: FontFaceIdentity,
121    data: SharedBytes,
122    font: Font,
123}
124
125pub(crate) fn font_container_identity(data: &[u8]) -> CanonicalIdentity {
126    CanonicalIdentity::for_font_container_bytes(data)
127}
128
129pub(crate) fn font_container_path(identity: CanonicalIdentity, data: Option<&[u8]>) -> String {
130    let extension = match data.and_then(|data| data.get(..4)) {
131        Some(b"OTTO") => "otf",
132        Some(b"ttcf") => "ttc",
133        Some(_) => "ttf",
134        None => "font",
135    };
136    format!("fonts/{}.{extension}", identity.encode())
137}
138
139/// The exact identity of one face within a Font Container.
140#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
141pub struct FontFaceIdentity {
142    container: CanonicalIdentity,
143    index: u32,
144}
145
146impl FontFaceIdentity {
147    /// The face at a container-local index within the given container.
148    pub(crate) fn new(container: CanonicalIdentity, index: u32) -> Self {
149        Self { container, index }
150    }
151
152    /// The containing font file or collection.
153    pub fn container(self) -> CanonicalIdentity {
154        self.container
155    }
156
157    /// The face's container-local index.
158    pub fn index(self) -> u32 {
159        self.index
160    }
161}
162
163/// One ordered face in the exact Pack Font Catalog.
164#[derive(Debug, Clone, PartialEq, Eq)]
165pub struct PackFontCatalogFace {
166    identity: FontFaceIdentity,
167    embedded: bool,
168}
169
170impl PackFontCatalogFace {
171    /// The exact container and face index.
172    pub fn identity(&self) -> FontFaceIdentity {
173        self.identity
174    }
175
176    /// Whether the Font Container bytes are stored in the Pack.
177    pub fn is_embedded(&self) -> bool {
178        self.embedded
179    }
180}
181
182/// One exact Font Container and the faces required from it.
183#[derive(Debug, Clone, PartialEq, Eq)]
184pub struct FontRequirement {
185    container: CanonicalIdentity,
186    length: u64,
187    face_indices: Vec<u32>,
188    embedded: bool,
189}
190
191impl FontRequirement {
192    pub fn container_identity(&self) -> CanonicalIdentity {
193        self.container
194    }
195
196    pub fn container_length(&self) -> u64 {
197        self.length
198    }
199
200    pub fn face_indices(&self) -> &[u32] {
201        &self.face_indices
202    }
203
204    pub fn is_embedded(&self) -> bool {
205        self.embedded
206    }
207}
208
209impl PackFont {
210    /// The exact container and container-local face index.
211    pub fn identity(&self) -> FontFaceIdentity {
212        self.identity
213    }
214
215    /// The contained font bytes.
216    pub fn data(&self) -> &[u8] {
217        self.data.as_slice()
218    }
219
220    pub(crate) fn shared_data(&self) -> &SharedBytes {
221        &self.data
222    }
223
224    /// Official selection metadata derived from the verified container bytes.
225    pub fn info(&self) -> &FontInfo {
226        self.font.info()
227    }
228}
229
230#[derive(Debug, Clone)]
231pub(crate) struct PackFontInput {
232    pub(crate) source: PackFontSourceInput,
233    pub(crate) index: u32,
234    pub(crate) embedded: bool,
235}
236
237#[derive(Debug, Clone)]
238pub(crate) enum PackFontSourceInput {
239    ExactBytes(SharedBytes),
240    Declared {
241        label: String,
242        identity: DeclaredFontContainerIdentity,
243        length: Option<u64>,
244        data: Option<SharedBytes>,
245    },
246}
247
248#[derive(Debug, Clone, Copy)]
249pub(crate) enum DeclaredFontContainerIdentity {
250    Absent,
251    Partial(Option<CanonicalIdentity>),
252    Valid(CanonicalIdentity),
253    Invalid,
254}
255
256#[derive(Debug)]
257pub(crate) struct ProjectFileInput {
258    pub(crate) path: String,
259    pub(crate) data: SharedBytes,
260}
261
262#[derive(Debug)]
263pub(crate) struct PackageFileInput {
264    pub(crate) spec: PackageSpec,
265    pub(crate) path: String,
266    pub(crate) data: SharedBytes,
267    pub(crate) embedded: bool,
268}
269
270#[derive(Debug)]
271pub(crate) struct PackageRequirementInput {
272    pub(crate) spec: Result<PackageSpec, InvalidPackageSpecInput>,
273    pub(crate) tree: Option<CanonicalIdentity>,
274    pub(crate) file_count: u64,
275    pub(crate) byte_length: u64,
276    pub(crate) embedded: bool,
277}
278
279#[derive(Debug)]
280pub(crate) struct InvalidPackageSpecInput {
281    pub(crate) spec: String,
282    pub(crate) message: String,
283}
284
285#[derive(Debug)]
286pub(crate) enum PackageRequirementsInput {
287    Inferred,
288    Declared(Vec<PackageRequirementInput>),
289}
290
291#[derive(Debug)]
292pub(crate) struct PackConstructionInput {
293    pub(crate) entrypoint: String,
294    pub(crate) metadata: Option<PackMetadata>,
295    pub(crate) files: Vec<ProjectFileInput>,
296    pub(crate) package_files: Vec<PackageFileInput>,
297    pub(crate) package_requirements: PackageRequirementsInput,
298    pub(crate) fonts: Vec<PackFontInput>,
299}
300
301impl Pack {
302    /// Starts building a pack from in-memory data.
303    ///
304    /// `entrypoint` is the root-relative path of the main file, e.g.
305    /// `main.typ`.
306    ///
307    /// Building directly does not run Dependency Discovery: the pack contains
308    /// exactly the files added here and declares no package or font
309    /// requirements. Use [`create`](crate::create) when the library should
310    /// discover requirements from values the caller already holds.
311    ///
312    /// ```
313    /// use typst_pack::Pack;
314    /// use typst_pack::pack_archive::encode;
315    ///
316    /// let pack = Pack::builder("main.typ")
317    ///     .file("main.typ", b"= Report\n".to_vec())?
318    ///     .file("data/figures.csv", b"quarter,revenue\nQ1,120\n".to_vec())?
319    ///     .build()?;
320    ///
321    /// assert_eq!(pack.entrypoint(), "main.typ");
322    ///
323    /// let archive = encode(&pack)?;
324    /// assert!(!archive.as_slice().is_empty());
325    /// # Ok::<(), Box<dyn std::error::Error>>(())
326    /// ```
327    pub fn builder(entrypoint: impl Into<String>) -> PackBuilder {
328        PackBuilder::new(entrypoint)
329    }
330
331    pub(crate) fn construct(input: PackConstructionInput) -> Result<Self, PackInvariantError> {
332        let mut issues = Vec::new();
333
334        let entrypoint = match canonical_path(PackPathRole::Entrypoint, &input.entrypoint) {
335            Ok(path) => Some(path),
336            Err(issue) => {
337                issues.push(issue);
338                None
339            }
340        };
341
342        let mut canonical_files = BTreeMap::new();
343        let mut duplicate_project_paths = BTreeSet::new();
344        for file in input.files {
345            match canonical_path(PackPathRole::ProjectFile, &file.path) {
346                Ok(path) => {
347                    if canonical_files.insert(path.clone(), file.data).is_some() {
348                        duplicate_project_paths.insert(path);
349                    }
350                }
351                Err(issue) => issues.push(issue),
352            }
353        }
354        issues.extend(duplicate_project_paths.into_iter().map(|path| {
355            PackInvariantIssue::DuplicateProjectPath {
356                path: path.into_string(),
357            }
358        }));
359        issues.extend(path_tree_conflicts(
360            canonical_files.keys(),
361            PackPathRole::ProjectFile,
362        ));
363
364        let mut package_groups = BTreeMap::<(String, bool), PackageFiles>::new();
365        let mut invalid_package_groups = BTreeSet::new();
366        let mut duplicate_package_paths = BTreeSet::new();
367        for file in input.package_files {
368            let key = file.spec.to_string();
369            if let Err(issue) = validate_package_spec(&file.spec) {
370                issues.push(issue);
371            }
372            let path = match canonical_path(PackPathRole::PackageFile, &file.path) {
373                Ok(path) => path,
374                Err(issue) => {
375                    issues.push(issue);
376                    continue;
377                }
378            };
379            let package = package_groups
380                .entry((key.clone(), file.embedded))
381                .or_insert_with(|| PackageFiles {
382                    spec: file.spec.clone(),
383                    files: BTreeMap::new(),
384                });
385            if package.files.insert(path.clone(), file.data).is_some() {
386                duplicate_package_paths.insert((key.clone(), file.embedded, path));
387                invalid_package_groups.insert((key, file.embedded));
388            }
389        }
390        for (package, _, path) in duplicate_package_paths {
391            if let Some(spec) = package_groups
392                .values()
393                .find(|entry| entry.spec.to_string() == package)
394                .map(|entry| entry.spec.clone())
395            {
396                issues.push(PackInvariantIssue::DuplicatePackagePath {
397                    package: spec,
398                    path: path.into_string(),
399                });
400            }
401        }
402        for ((package, embedded), files) in &package_groups {
403            let conflicts = shared_path_tree_conflicts(
404                files
405                    .files
406                    .keys()
407                    .map(|path| (path, PackPathRole::PackageFile)),
408            );
409            if !conflicts.is_empty() {
410                invalid_package_groups.insert((package.clone(), *embedded));
411            }
412            for conflict in conflicts {
413                issues.push(PackInvariantIssue::PackagePathTreeConflict {
414                    package: package.clone(),
415                    ancestor: conflict.ancestor.to_string(),
416                    ancestor_role: conflict.ancestor_role,
417                    descendant: conflict.descendant.to_string(),
418                    descendant_role: conflict.descendant_role,
419                });
420            }
421        }
422
423        let declared_inputs = match input.package_requirements {
424            PackageRequirementsInput::Inferred => None,
425            PackageRequirementsInput::Declared(entries) => Some(entries),
426        };
427        let declarations_are_explicit = declared_inputs.is_some();
428        let mut declared_requirements = BTreeMap::<(String, bool), Vec<PackageRequirement>>::new();
429        let mut declared_requirement_roles = BTreeSet::new();
430        let mut duplicate_requirements = BTreeSet::new();
431        for declaration in declared_inputs.unwrap_or_default() {
432            let spec = match declaration.spec {
433                Ok(spec) => spec,
434                Err(error) => {
435                    issues.push(PackInvariantIssue::InvalidPackageSpec {
436                        spec: error.spec,
437                        message: error.message,
438                    });
439                    continue;
440                }
441            };
442            let role = (spec.to_string(), declaration.embedded);
443            if !declared_requirement_roles.insert(role.clone()) {
444                duplicate_requirements.insert(role.clone());
445            }
446            let Some(tree) = declaration.tree.filter(|_| declaration.file_count > 0) else {
447                issues.push(PackInvariantIssue::InvalidPackageRequirement {
448                    spec: spec.to_string(),
449                });
450                continue;
451            };
452            declared_requirements
453                .entry(role)
454                .or_default()
455                .push(PackageRequirement {
456                    spec,
457                    tree,
458                    file_count: declaration.file_count,
459                    byte_length: declaration.byte_length,
460                    embedded: declaration.embedded,
461                });
462        }
463        issues.extend(duplicate_requirements.into_iter().map(|(spec, embedded)| {
464            PackInvariantIssue::DuplicatePackageRequirement { spec, embedded }
465        }));
466
467        let requirement_specs = package_groups
468            .keys()
469            .map(|(spec, _)| spec.clone())
470            .chain(
471                declared_requirement_roles
472                    .iter()
473                    .map(|(spec, _)| spec.clone()),
474            )
475            .collect::<BTreeSet<_>>();
476        for spec in &requirement_specs {
477            let embedded = package_groups.contains_key(&(spec.clone(), true))
478                || declared_requirement_roles.contains(&(spec.clone(), true));
479            let external = package_groups.contains_key(&(spec.clone(), false))
480                || declared_requirement_roles.contains(&(spec.clone(), false));
481            if embedded && external {
482                issues.push(PackInvariantIssue::PackageRoleConflict { spec: spec.clone() });
483            }
484        }
485
486        let mut canonical_packages = BTreeMap::new();
487        let mut package_requirements = Vec::new();
488        if declarations_are_explicit {
489            for ((spec, embedded), declarations) in &declared_requirements {
490                let declared = &declarations[0];
491                if *embedded && let Some(package) = package_groups.get(&(spec.clone(), true)) {
492                    let (tree, file_count, byte_length) = package_tree_identity(&package.files);
493                    if !invalid_package_groups.contains(&(spec.clone(), true))
494                        && declarations.iter().any(|declared| {
495                            declared.tree != tree
496                                || declared.file_count != file_count
497                                || declared.byte_length != byte_length
498                        })
499                    {
500                        issues.push(PackInvariantIssue::MismatchedEmbeddedPackageIdentity {
501                            spec: spec.clone(),
502                        });
503                    }
504                }
505                package_requirements.push(declared.clone());
506            }
507            for (spec, embedded) in &declared_requirement_roles {
508                if *embedded && !package_groups.contains_key(&(spec.clone(), true)) {
509                    issues.push(PackInvariantIssue::MissingVendoredPackageData {
510                        spec: spec.clone(),
511                    });
512                }
513            }
514            for ((spec, embedded), package) in &package_groups {
515                if *embedded && !declared_requirement_roles.contains(&(spec.clone(), true)) {
516                    issues.push(PackInvariantIssue::UndeclaredPackageData { spec: spec.clone() });
517                }
518                if *embedded {
519                    canonical_packages.insert(spec.clone(), package.clone());
520                }
521            }
522        } else {
523            for ((spec, embedded), package) in &package_groups {
524                let (tree, file_count, byte_length) = package_tree_identity(&package.files);
525                package_requirements.push(PackageRequirement {
526                    spec: package.spec.clone(),
527                    tree,
528                    file_count,
529                    byte_length,
530                    embedded: *embedded,
531                });
532                if *embedded {
533                    canonical_packages.insert(spec.clone(), package.clone());
534                }
535            }
536        }
537        package_requirements.sort_by_key(|requirement| requirement.spec.to_string());
538        let mut canonical_fonts = Vec::new();
539        let mut font_catalog = Vec::new();
540        let mut font_requirements = Vec::<FontRequirement>::new();
541        let mut font_faces = BTreeSet::new();
542        let mut declared_font_paths = BTreeSet::new();
543        for (position, entry) in input.fonts.into_iter().enumerate() {
544            let (path, data, declared_identity, declared_length, exact_bytes) = match entry.source {
545                PackFontSourceInput::ExactBytes(data) => (
546                    format!("font input {position}"),
547                    Some(data),
548                    DeclaredFontContainerIdentity::Absent,
549                    None,
550                    true,
551                ),
552                PackFontSourceInput::Declared {
553                    label,
554                    identity,
555                    length,
556                    data,
557                } => {
558                    match canonical_path(PackPathRole::FontData, &label) {
559                        Ok(path) => {
560                            declared_font_paths.insert(path);
561                        }
562                        Err(issue) => issues.push(issue),
563                    }
564                    (label, data, identity, length, false)
565                }
566            };
567            let index = entry.index;
568            let embedded = entry.embedded;
569            let parsed_data = data.as_ref().and_then(|data| {
570                Font::new(data.to_typst(), index).map(|font| {
571                    let container = font_container_identity(data.as_slice());
572                    (data.clone(), font, container, data.len() as u64)
573                })
574            });
575            let (data, parsed, container, length) = if embedded {
576                let Some((data, parsed, container, length)) = parsed_data else {
577                    issues.push(if data.is_some() {
578                        PackInvariantIssue::InvalidFontData { path, index }
579                    } else {
580                        PackInvariantIssue::MissingFontData { path }
581                    });
582                    continue;
583                };
584                if matches!(declared_identity, DeclaredFontContainerIdentity::Invalid)
585                    || matches!(
586                        declared_identity,
587                        DeclaredFontContainerIdentity::Valid(declared)
588                            | DeclaredFontContainerIdentity::Partial(Some(declared))
589                            if declared != container
590                    )
591                    || declared_length.is_some_and(|declared| declared != length)
592                {
593                    issues.push(PackInvariantIssue::MismatchedEmbeddedFontIdentity {
594                        path: path.clone(),
595                    });
596                }
597                (Some(data), Some(parsed), container, length)
598            } else if exact_bytes {
599                let Some((_, _, container, length)) = parsed_data else {
600                    issues.push(PackInvariantIssue::InvalidFontData { path, index });
601                    continue;
602                };
603                (None, None, container, length)
604            } else {
605                if data.is_some() {
606                    issues.push(PackInvariantIssue::ExternalFontHasContainedData {
607                        path: path.clone(),
608                    });
609                }
610                let DeclaredFontContainerIdentity::Valid(container) = declared_identity else {
611                    issues.push(PackInvariantIssue::InvalidExternalFontIdentity { path });
612                    continue;
613                };
614                let Some(length) = declared_length.filter(|length| *length > 0) else {
615                    issues.push(PackInvariantIssue::InvalidExternalFontIdentity { path });
616                    continue;
617                };
618                (None, None, container, length)
619            };
620
621            if !font_faces.insert((container, index)) {
622                issues.push(PackInvariantIssue::DuplicateFontFace {
623                    path: path.clone(),
624                    index,
625                });
626            }
627            font_catalog.push(PackFontCatalogFace {
628                identity: FontFaceIdentity::new(container, index),
629                embedded,
630            });
631            match font_requirements
632                .iter_mut()
633                .find(|requirement| requirement.container == container)
634            {
635                Some(requirement)
636                    if requirement.length != length || requirement.embedded != embedded =>
637                {
638                    issues.push(PackInvariantIssue::InconsistentFontContainer { path });
639                }
640                Some(requirement) => requirement.face_indices.push(index),
641                None => font_requirements.push(FontRequirement {
642                    container,
643                    length,
644                    face_indices: vec![index],
645                    embedded,
646                }),
647            }
648            if let (Some(data), Some(font)) = (data, parsed) {
649                canonical_fonts.push(PackFont {
650                    identity: FontFaceIdentity::new(container, index),
651                    data,
652                    font,
653                });
654            }
655        }
656        issues.extend(path_tree_conflicts(
657            &declared_font_paths,
658            PackPathRole::FontData,
659        ));
660        if let Some(entrypoint) = &entrypoint
661            && !canonical_files.contains_key(entrypoint)
662        {
663            issues.push(PackInvariantIssue::MissingEntrypoint {
664                path: entrypoint.to_string(),
665            });
666        }
667
668        issues.sort_by_key(PackInvariantIssue::sort_key);
669        if !issues.is_empty() {
670            return Err(PackInvariantError { issues });
671        }
672
673        let entrypoint = entrypoint.expect("a valid Pack has a canonical entrypoint");
674        let identity = pack_identity(
675            &entrypoint,
676            &canonical_files,
677            &package_requirements,
678            &font_catalog,
679        );
680        Ok(Self {
681            identity,
682            entrypoint,
683            metadata: input.metadata,
684            files: canonical_files,
685            packages: canonical_packages,
686            package_requirements,
687            fonts: canonical_fonts,
688            font_catalog,
689            font_requirements,
690        })
691    }
692
693    /// Derives the Pack's identity-bearing semantic projection.
694    pub fn identity(&self) -> CanonicalIdentity {
695        self.identity
696    }
697
698    /// The root-relative path of the entrypoint file.
699    pub fn entrypoint(&self) -> &str {
700        self.entrypoint.as_str()
701    }
702
703    /// Optional descriptive metadata, excluded from Pack Identity.
704    pub fn metadata(&self) -> Option<&PackMetadata> {
705        self.metadata.as_ref()
706    }
707
708    /// The project files, keyed by root-relative path.
709    pub fn files(&self) -> impl Iterator<Item = (&str, &[u8])> {
710        self.files
711            .iter()
712            .map(|(path, data)| (path.as_str(), data.as_slice()))
713    }
714
715    /// Looks up a project file by root-relative path.
716    pub fn file(&self, path: &str) -> Option<&[u8]> {
717        self.files.get(path).map(SharedBytes::as_slice)
718    }
719
720    pub(crate) fn shared_file(&self, path: &str) -> Option<&SharedBytes> {
721        self.files.get(path)
722    }
723
724    pub(crate) fn canonical_project_path(path: &str) -> Result<String, String> {
725        canonical_path(PackPathRole::ProjectFile, path)
726            .map(CanonicalPath::into_string)
727            .map_err(|error| error.to_string())
728    }
729
730    /// Canonicalizes a supplied package-relative path, so that a tree is
731    /// looked up and contained under the same path.
732    pub(crate) fn canonical_package_path(path: &str) -> Result<String, String> {
733        canonical_path(PackPathRole::PackageFile, path)
734            .map(CanonicalPath::into_string)
735            .map_err(|error| error.to_string())
736    }
737
738    /// The vendored packages and their files.
739    pub fn packages(
740        &self,
741    ) -> impl Iterator<Item = (&PackageSpec, impl Iterator<Item = (&str, &[u8])>)> {
742        self.packages.values().map(|package| {
743            (
744                &package.spec,
745                package
746                    .files
747                    .iter()
748                    .map(|(path, data)| (path.as_str(), data.as_slice())),
749            )
750        })
751    }
752
753    /// Looks up a vendored package file.
754    pub fn package_file(&self, spec: &PackageSpec, path: &str) -> Option<&[u8]> {
755        self.packages
756            .get(&spec.to_string())?
757            .files
758            .get(path)
759            .map(SharedBytes::as_slice)
760    }
761
762    pub(crate) fn shared_package_file(
763        &self,
764        spec: &PackageSpec,
765        path: &str,
766    ) -> Option<&SharedBytes> {
767        self.packages.get(&spec.to_string())?.files.get(path)
768    }
769
770    /// Whether the pack vendors the given package.
771    pub fn has_package(&self, spec: &PackageSpec) -> bool {
772        self.packages.contains_key(&spec.to_string())
773    }
774
775    /// The Pack's exact Package Requirements in canonical specification order.
776    pub fn package_requirements(&self) -> &[PackageRequirement] {
777        &self.package_requirements
778    }
779
780    /// The fonts embedded in the pack.
781    pub fn fonts(&self) -> &[PackFont] {
782        &self.fonts
783    }
784
785    /// The exact Pack Font Catalog faces exposed to official Typst, in stable order.
786    pub fn font_catalog(&self) -> &[PackFontCatalogFace] {
787        &self.font_catalog
788    }
789
790    /// The exact Font Containers required by this Pack.
791    pub fn font_requirements(&self) -> &[FontRequirement] {
792        &self.font_requirements
793    }
794
795    pub(crate) fn materialize_compilation_dependency_snapshot(
796        &self,
797        mut package_fulfillments: BTreeMap<String, PackageTree>,
798        font_fulfillments: BTreeMap<CanonicalIdentity, FontContainer>,
799    ) -> CompilationDependencySnapshot {
800        let mut packages = self.packages.clone();
801        for requirement in self
802            .package_requirements
803            .iter()
804            .filter(|requirement| !requirement.embedded)
805        {
806            let key = requirement.spec.to_string();
807            let tree = package_fulfillments
808                .remove(&key)
809                .expect("exact fulfillment verification supplies every external Package Tree");
810            packages.insert(
811                key,
812                PackageFiles::from_validated_tree(requirement.spec.clone(), tree),
813            );
814        }
815        let font_catalog = self
816            .font_catalog
817            .iter()
818            .map(|face| {
819                let identity = face.identity;
820                if face.embedded {
821                    self.fonts
822                        .iter()
823                        .find(|font| font.identity == identity)
824                        .expect("Pack Font Catalog embedded face invariant violated")
825                        .font
826                        .clone()
827                } else {
828                    font_fulfillments[&identity.container]
829                        .font(identity.index)
830                        .expect("exact validated Font Container holds every required face")
831                }
832            })
833            .collect();
834        CompilationDependencySnapshot {
835            pack_identity: self.identity(),
836            packages,
837            font_catalog,
838        }
839    }
840}
841
842fn pack_identity(
843    entrypoint: &CanonicalPath,
844    files: &BTreeMap<CanonicalPath, SharedBytes>,
845    package_requirements: &[PackageRequirement],
846    font_catalog: &[PackFontCatalogFace],
847) -> CanonicalIdentity {
848    let project_files = files
849        .iter()
850        .map(|(path, data)| (path.as_str(), typst::utils::hash128(data)))
851        .collect::<Vec<_>>();
852    let packages = package_requirements
853        .iter()
854        .map(|requirement| {
855            (
856                requirement.spec.to_string(),
857                requirement.tree,
858                requirement.file_count,
859                requirement.byte_length,
860                requirement.embedded,
861            )
862        })
863        .collect::<Vec<_>>();
864    let fonts = font_catalog
865        .iter()
866        .map(|face| (face.identity.container, face.identity.index, face.embedded))
867        .collect::<Vec<_>>();
868    CanonicalIdentity::from_digest(
869        CanonicalIdentityRole::Pack,
870        typst::utils::hash128(&(
871            "typst-pack-identity-v1",
872            entrypoint.as_str(),
873            project_files,
874            packages,
875            fonts,
876        )),
877    )
878}
879
880fn package_tree_identity(
881    files: &BTreeMap<CanonicalPath, SharedBytes>,
882) -> (CanonicalIdentity, u64, u64) {
883    crate::package_catalog::derive_package_tree_identity(
884        files.iter().map(|(path, data)| (path.as_str(), data)),
885    )
886}
887
888/// Builds a [`Pack`] from in-memory data.
889///
890/// This is the constructor to use when the project does not live on a file
891/// system, for example in a web editor. For packing a project directory, use
892/// `FilesystemPackAssembler` instead (requires the `fs` feature).
893#[derive(Debug)]
894pub struct PackBuilder {
895    entrypoint: String,
896    files: Vec<ProjectFileInput>,
897    package_files: Vec<PackageFileInput>,
898    fonts: Vec<PackFontInput>,
899    metadata: Option<PackMetadata>,
900}
901
902impl PackBuilder {
903    /// Creates a builder for a pack with the given entrypoint path.
904    pub fn new(entrypoint: impl Into<String>) -> Self {
905        Self {
906            entrypoint: entrypoint.into(),
907            files: Vec::new(),
908            package_files: Vec::new(),
909            fonts: Vec::new(),
910            metadata: None,
911        }
912    }
913
914    /// Adds a project file under a root-relative path.
915    pub fn file(
916        self,
917        path: impl AsRef<str>,
918        data: impl Into<Vec<u8>>,
919    ) -> Result<Self, PackBuildError> {
920        self.shared_file(path, SharedBytes::new(data.into()))
921    }
922
923    pub(crate) fn shared_file(
924        mut self,
925        path: impl AsRef<str>,
926        data: SharedBytes,
927    ) -> Result<Self, PackBuildError> {
928        self.files.push(ProjectFileInput {
929            path: path.as_ref().to_owned(),
930            data,
931        });
932        Ok(self)
933    }
934
935    /// Adds a file of a vendored package.
936    pub fn package_file(
937        self,
938        spec: PackageSpec,
939        path: impl AsRef<str>,
940        data: impl Into<Vec<u8>>,
941    ) -> Result<Self, PackBuildError> {
942        self.shared_package_file(spec, path, SharedBytes::new(data.into()))
943    }
944
945    pub(crate) fn shared_package_file(
946        mut self,
947        spec: PackageSpec,
948        path: impl AsRef<str>,
949        data: SharedBytes,
950    ) -> Result<Self, PackBuildError> {
951        self.package_files.push(PackageFileInput {
952            spec,
953            path: path.as_ref().to_owned(),
954            data,
955            embedded: true,
956        });
957        Ok(self)
958    }
959
960    /// Adds a file to an exact Package Tree fulfilled outside the Pack.
961    pub fn external_package_file(
962        self,
963        spec: PackageSpec,
964        path: impl AsRef<str>,
965        data: impl Into<Vec<u8>>,
966    ) -> Result<Self, PackBuildError> {
967        self.shared_external_package_file(spec, path, SharedBytes::new(data.into()))
968    }
969
970    pub(crate) fn shared_external_package_file(
971        mut self,
972        spec: PackageSpec,
973        path: impl AsRef<str>,
974        data: SharedBytes,
975    ) -> Result<Self, PackBuildError> {
976        self.package_files.push(PackageFileInput {
977            spec,
978            path: path.as_ref().to_owned(),
979            data,
980            embedded: false,
981        });
982        Ok(self)
983    }
984
985    /// Embeds a font file.
986    ///
987    /// `index` is the face index for font collections and zero otherwise. The
988    /// entry name and family list are derived from the font data.
989    pub fn font(self, data: impl Into<Vec<u8>>, index: u32) -> Result<Self, PackBuildError> {
990        self.shared_font(SharedBytes::new(data.into()), index)
991    }
992
993    pub(crate) fn shared_font(
994        mut self,
995        data: SharedBytes,
996        index: u32,
997    ) -> Result<Self, PackBuildError> {
998        self.fonts.push(PackFontInput {
999            source: PackFontSourceInput::ExactBytes(data),
1000            index,
1001            embedded: true,
1002        });
1003        Ok(self)
1004    }
1005
1006    /// Records an exact font dependency without storing its container bytes.
1007    pub fn external_font(
1008        self,
1009        data: impl Into<Vec<u8>>,
1010        index: u32,
1011    ) -> Result<Self, PackBuildError> {
1012        self.shared_external_font(SharedBytes::new(data.into()), index)
1013    }
1014
1015    pub(crate) fn shared_external_font(
1016        mut self,
1017        data: SharedBytes,
1018        index: u32,
1019    ) -> Result<Self, PackBuildError> {
1020        self.fonts.push(PackFontInput {
1021            source: PackFontSourceInput::ExactBytes(data),
1022            index,
1023            embedded: false,
1024        });
1025        Ok(self)
1026    }
1027
1028    /// Sets descriptive metadata.
1029    pub fn metadata(mut self, metadata: PackMetadata) -> Self {
1030        self.metadata = Some(metadata);
1031        self
1032    }
1033
1034    /// Finishes the pack.
1035    pub fn build(self) -> Result<Pack, PackBuildError> {
1036        Ok(Pack::construct(PackConstructionInput {
1037            entrypoint: self.entrypoint,
1038            metadata: self.metadata,
1039            files: self.files,
1040            package_files: self.package_files,
1041            package_requirements: PackageRequirementsInput::Inferred,
1042            fonts: self.fonts,
1043        })?)
1044    }
1045}
1046
1047fn canonical_path(role: PackPathRole, path: &str) -> Result<CanonicalPath, PackInvariantIssue> {
1048    let canonical = canonical_path_without_membership(role, path)?;
1049    // No route into a Pack can name a Pack as a project file.
1050    if matches!(role, PackPathRole::ProjectFile | PackPathRole::Entrypoint)
1051        && names_pack_path(canonical.as_str())
1052    {
1053        return Err(PackInvariantIssue::InvalidPath {
1054            role,
1055            path: path.to_owned(),
1056            message: format!("`.{FILE_EXTENSION}` paths are excluded from project membership"),
1057        });
1058    }
1059    Ok(canonical)
1060}
1061
1062/// Canonicalizes a path for its role without deciding project membership.
1063fn canonical_path_without_membership(
1064    role: PackPathRole,
1065    path: &str,
1066) -> Result<CanonicalPath, PackInvariantIssue> {
1067    let invalid = |message: String| PackInvariantIssue::InvalidPath {
1068        role,
1069        path: path.to_owned(),
1070        message,
1071    };
1072    canonical_relative_path(path).map_err(|error| invalid(error.to_string()))
1073}
1074
1075fn validate_package_spec(spec: &PackageSpec) -> Result<(), PackInvariantIssue> {
1076    let serialized = spec.to_string();
1077    let parsed = PackageSpec::from_str(&serialized).map_err(|message| {
1078        PackInvariantIssue::InvalidPackageSpec {
1079            spec: serialized.clone(),
1080            message: message.to_string(),
1081        }
1082    })?;
1083    if parsed != *spec {
1084        return Err(PackInvariantIssue::InvalidPackageSpec {
1085            spec: serialized,
1086            message: "package specification does not round-trip canonically".to_owned(),
1087        });
1088    }
1089    Ok(())
1090}
1091
1092fn path_tree_conflicts<'a>(
1093    paths: impl IntoIterator<Item = &'a CanonicalPath>,
1094    role: PackPathRole,
1095) -> Vec<PackInvariantIssue> {
1096    shared_path_tree_conflicts(paths.into_iter().map(|path| (path, role)))
1097        .into_iter()
1098        .map(|conflict| PackInvariantIssue::PathTreeConflict {
1099            ancestor: conflict.ancestor.to_string(),
1100            ancestor_role: conflict.ancestor_role,
1101            descendant: conflict.descendant.to_string(),
1102            descendant_role: conflict.descendant_role,
1103        })
1104        .collect()
1105}
1106
1107/// A failure while building a pack in memory.
1108#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
1109#[non_exhaustive]
1110pub enum PackBuildError {
1111    #[error(transparent)]
1112    Invariant(#[from] PackInvariantError),
1113}
1114
1115/// One independently detectable violation of a whole-Pack invariant.
1116#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
1117#[non_exhaustive]
1118pub enum PackInvariantIssue {
1119    /// A path cannot identify a canonical file for its declared role.
1120    #[error("invalid {role} path {path:?}: {message:?}")]
1121    InvalidPath {
1122        role: PackPathRole,
1123        path: String,
1124        message: String,
1125    },
1126    /// A package value cannot be represented as a canonical package specification.
1127    #[error("invalid package spec {spec:?}: {message:?}")]
1128    InvalidPackageSpec { spec: String, message: String },
1129    /// A Package Requirement has a malformed or unsupported tree identity.
1130    #[error("package requirement {spec:?} has an invalid Package Tree identity")]
1131    InvalidPackageRequirement { spec: String },
1132    /// Embedded package bytes disagree with their declared tree identity.
1133    #[error("embedded package {spec:?} does not match its declared Package Tree identity")]
1134    MismatchedEmbeddedPackageIdentity { spec: String },
1135    /// Two project entries identify one canonical path.
1136    #[error("project path {path:?} is supplied more than once")]
1137    DuplicateProjectPath { path: String },
1138    /// Two package entries identify one canonical path.
1139    #[error("package {package} path {path:?} is supplied more than once")]
1140    DuplicatePackagePath { package: PackageSpec, path: String },
1141    /// One exact Package Requirement is declared more than once in one role.
1142    #[error("package requirement {spec:?} is declared more than once")]
1143    DuplicatePackageRequirement { spec: String, embedded: bool },
1144    /// One file path is an ancestor of another file path in the same tree.
1145    #[error(
1146        "{ancestor_role} path {ancestor:?} conflicts with {descendant_role} descendant {descendant:?}"
1147    )]
1148    PathTreeConflict {
1149        ancestor: String,
1150        ancestor_role: PackPathRole,
1151        descendant: String,
1152        descendant_role: PackPathRole,
1153    },
1154    /// One package file path is an ancestor of another file path in that package.
1155    #[error(
1156        "package {package:?} {ancestor_role} path {ancestor:?} conflicts with {descendant_role} descendant {descendant:?}"
1157    )]
1158    PackagePathTreeConflict {
1159        package: String,
1160        ancestor: String,
1161        ancestor_role: PackPathRole,
1162        descendant: String,
1163        descendant_role: PackPathRole,
1164    },
1165    /// A package was declared both vendored and unvendored.
1166    #[error("package {spec:?} cannot be both vendored and unvendored")]
1167    PackageRoleConflict { spec: String },
1168    /// Package bytes exist without a matching vendored declaration.
1169    #[error("package {spec:?} has contained data but is not declared vendored")]
1170    UndeclaredPackageData { spec: String },
1171    /// A vendored package declaration has no contained bytes.
1172    #[error("vendored package {spec:?} has no contained data")]
1173    MissingVendoredPackageData { spec: String },
1174    /// A font declaration has no contained bytes.
1175    #[error("font data {path:?} is missing")]
1176    MissingFontData { path: String },
1177    /// Contained font bytes do not contain the declared face.
1178    #[error("font data {path:?} does not contain a valid face at index {index}")]
1179    InvalidFontData { path: String, index: u32 },
1180    /// An external font declaration has no valid exact container identity.
1181    #[error("external font {path:?} has an invalid container identity or length")]
1182    InvalidExternalFontIdentity { path: String },
1183    /// Embedded font bytes disagree with their declared exact identity.
1184    #[error("embedded font {path:?} does not match its declared container identity")]
1185    MismatchedEmbeddedFontIdentity { path: String },
1186    /// Faces from one exact container disagree about its length or fulfillment role.
1187    #[error("font {path:?} conflicts with another declaration for the same container")]
1188    InconsistentFontContainer { path: String },
1189    /// An externally fulfilled declaration also has bytes in the Pack.
1190    #[error("external font {path:?} cannot also have contained data")]
1191    ExternalFontHasContainedData { path: String },
1192    /// The same contained font face was declared more than once.
1193    #[error("font {path:?} declares face index {index} more than once")]
1194    DuplicateFontFace { path: String, index: u32 },
1195    /// The declared entrypoint is not present among the packed project files.
1196    #[error("entrypoint {path:?} is not a contained project file")]
1197    MissingEntrypoint { path: String },
1198}
1199
1200impl PackInvariantIssue {
1201    fn sort_key(&self) -> (u8, String, u8, u64, String) {
1202        match self {
1203            Self::InvalidPath {
1204                role: PackPathRole::Entrypoint,
1205                path,
1206                ..
1207            } => (3, path.clone(), 0, 0, String::new()),
1208            Self::InvalidPath { role, path, .. } => {
1209                (role_sort_rank(*role), path.clone(), 0, 0, String::new())
1210            }
1211            Self::DuplicateProjectPath { path } => (0, path.clone(), 1, 0, String::new()),
1212            Self::PathTreeConflict {
1213                ancestor,
1214                ancestor_role,
1215                descendant,
1216                ..
1217            } => (
1218                role_sort_rank(*ancestor_role),
1219                ancestor.clone(),
1220                2,
1221                0,
1222                descendant.clone(),
1223            ),
1224            Self::InvalidPackageSpec { spec, .. } => (1, spec.clone(), 0, 0, String::new()),
1225            Self::DuplicatePackagePath { package, path } => {
1226                (1, package.to_string(), 1, 0, path.clone())
1227            }
1228            Self::PackagePathTreeConflict {
1229                package,
1230                ancestor,
1231                descendant,
1232                ..
1233            } => (
1234                1,
1235                package.clone(),
1236                2,
1237                0,
1238                format!("{ancestor}\0{descendant}"),
1239            ),
1240            Self::DuplicatePackageRequirement { spec, embedded } => {
1241                (1, spec.clone(), 3, u64::from(*embedded), String::new())
1242            }
1243            Self::InvalidPackageRequirement { spec } => (1, spec.clone(), 4, 0, String::new()),
1244            Self::MismatchedEmbeddedPackageIdentity { spec } => {
1245                (1, spec.clone(), 5, 0, String::new())
1246            }
1247            Self::PackageRoleConflict { spec } => (1, spec.clone(), 6, 0, String::new()),
1248            Self::UndeclaredPackageData { spec } => (1, spec.clone(), 7, 0, String::new()),
1249            Self::MissingVendoredPackageData { spec } => (1, spec.clone(), 8, 0, String::new()),
1250            Self::MissingFontData { path } => (2, path.clone(), 0, 0, String::new()),
1251            Self::InvalidFontData { path, index } => {
1252                (2, path.clone(), 1, u64::from(*index), String::new())
1253            }
1254            Self::InvalidExternalFontIdentity { path } => (2, path.clone(), 2, 0, String::new()),
1255            Self::MismatchedEmbeddedFontIdentity { path } => (2, path.clone(), 3, 0, String::new()),
1256            Self::InconsistentFontContainer { path } => (2, path.clone(), 4, 0, String::new()),
1257            Self::ExternalFontHasContainedData { path } => (2, path.clone(), 5, 0, String::new()),
1258            Self::DuplicateFontFace { path, index } => {
1259                (2, path.clone(), 6, u64::from(*index), String::new())
1260            }
1261            Self::MissingEntrypoint { path } => (3, path.clone(), 1, 0, String::new()),
1262        }
1263    }
1264}
1265
1266fn role_sort_rank(role: PackPathRole) -> u8 {
1267    match role {
1268        PackPathRole::Entrypoint => 3,
1269        PackPathRole::ProjectFile => 0,
1270        PackPathRole::PackageFile => 1,
1271        PackPathRole::FontData => 2,
1272    }
1273}
1274
1275/// A violation of the invariants shared by every [`Pack`] construction path.
1276#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
1277#[error("Pack construction failed with {} issue(s)", .issues.len())]
1278pub struct PackInvariantError {
1279    issues: Vec<PackInvariantIssue>,
1280}
1281
1282impl PackInvariantError {
1283    /// Every independently detectable issue in canonical domain order.
1284    pub fn issues(&self) -> &[PackInvariantIssue] {
1285        &self.issues
1286    }
1287}
1288
1289/// The role a path plays in a Pack invariant.
1290#[derive(Debug, Clone, Copy, Eq, PartialEq)]
1291pub enum PackPathRole {
1292    Entrypoint,
1293    ProjectFile,
1294    PackageFile,
1295    FontData,
1296}
1297
1298impl std::fmt::Display for PackPathRole {
1299    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1300        formatter.write_str(match self {
1301            Self::Entrypoint => "entrypoint",
1302            Self::ProjectFile => "project file",
1303            Self::PackageFile => "package file",
1304            Self::FontData => "font data",
1305        })
1306    }
1307}