Skip to main content

typst_pack/
pack.rs

1//! The in-memory pack model and its archive serialization.
2
3use std::borrow::Borrow;
4use std::collections::{BTreeMap, BTreeSet};
5use std::io::{Cursor, Read, Seek, SeekFrom, Write};
6use std::str::FromStr;
7
8use typst::foundations::Bytes;
9use typst::syntax::VirtualPath;
10use typst::syntax::package::PackageSpec;
11use typst::text::{Font, FontInfo};
12use zip::write::SimpleFileOptions;
13use zip::{ZipArchive, ZipWriter};
14
15use crate::manifest::{
16    FontManifest, MANIFEST_PATH, PackManifest, PackManifestError, PackMetadata, PackageManifest,
17};
18
19/// The conventional file extension for packs.
20pub const FILE_EXTENSION: &str = "typk";
21
22const PROJECT_PREFIX: &str = "project/";
23const PACKAGES_PREFIX: &str = "packages/";
24const MAX_ZIP_ENTRY_NAME_LEN: usize = u16::MAX as usize;
25pub(crate) const PACKAGE_TREE_IDENTITY_KIND: &str = "complete-package-tree";
26pub(crate) const PACKAGE_TREE_IDENTITY_SCHEMA: &str = "typst-pack-complete-package-tree-v1";
27pub(crate) const PACKAGE_TREE_IDENTITY_ALGORITHM: &str = "typst-hash128-0.15";
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    manifest: PackManifest,
38    files: BTreeMap<CanonicalPath, Bytes>,
39    /// Vendored packages, keyed by spec string for deterministic order.
40    packages: BTreeMap<String, PackageFiles>,
41    package_requirements: Vec<PackageRequirement>,
42    fonts: Vec<PackFont>,
43    font_catalog: Vec<PackFontCatalogFace>,
44    font_requirements: Vec<FontRequirement>,
45}
46
47/// The canonical semantic identity of a [`Pack`].
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49pub struct PackIdentity(u128);
50
51impl PackIdentity {
52    pub fn kind(self) -> &'static str {
53        "pack"
54    }
55
56    pub fn schema(self) -> &'static str {
57        "typst-pack-identity-v1"
58    }
59
60    pub fn algorithm(self) -> &'static str {
61        "typst-hash128-0.15"
62    }
63
64    pub fn digest(self) -> [u8; 16] {
65        self.0.to_be_bytes()
66    }
67}
68
69#[derive(Debug, Clone)]
70pub(crate) struct PackageFiles {
71    pub(crate) spec: PackageSpec,
72    files: BTreeMap<CanonicalPath, Bytes>,
73}
74
75impl PackageFiles {
76    pub(crate) fn file(&self, path: &str) -> Option<&Bytes> {
77        self.files.get(path)
78    }
79}
80
81/// Exact verified dependencies accepted by the synchronous Compilation Kernel.
82pub(crate) struct CompilationDependencySnapshot {
83    pack_identity: PackIdentity,
84    packages: BTreeMap<String, PackageFiles>,
85    font_catalog: Vec<Font>,
86}
87
88impl CompilationDependencySnapshot {
89    pub(crate) fn pack_identity(&self) -> PackIdentity {
90        self.pack_identity
91    }
92
93    pub(crate) fn into_parts(self) -> (BTreeMap<String, PackageFiles>, Vec<Font>) {
94        (self.packages, self.font_catalog)
95    }
96}
97
98/// The canonical content identity of one Complete Package Tree.
99#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
100pub struct PackageTreeIdentity(u128);
101
102impl PackageTreeIdentity {
103    pub fn digest(self) -> [u8; 16] {
104        self.0.to_be_bytes()
105    }
106    pub fn kind(self) -> &'static str {
107        PACKAGE_TREE_IDENTITY_KIND
108    }
109    pub fn schema(self) -> &'static str {
110        PACKAGE_TREE_IDENTITY_SCHEMA
111    }
112    pub fn algorithm(self) -> &'static str {
113        PACKAGE_TREE_IDENTITY_ALGORITHM
114    }
115    fn encode(self) -> String {
116        format!("{:032x}", self.0)
117    }
118    fn decode(value: &str) -> Option<Self> {
119        (value.len() == 32)
120            .then(|| u128::from_str_radix(value, 16).ok().map(Self))
121            .flatten()
122    }
123}
124
125/// One exact package specification and Complete Package Tree identity.
126#[derive(Debug, Clone, PartialEq, Eq)]
127pub struct PackageRequirement {
128    spec: PackageSpec,
129    tree: PackageTreeIdentity,
130    file_count: u64,
131    byte_length: u64,
132    embedded: bool,
133}
134
135impl PackageRequirement {
136    pub fn spec(&self) -> &PackageSpec {
137        &self.spec
138    }
139    pub fn tree_identity(&self) -> PackageTreeIdentity {
140        self.tree
141    }
142    pub fn file_count(&self) -> u64 {
143        self.file_count
144    }
145    pub fn byte_length(&self) -> u64 {
146        self.byte_length
147    }
148    pub fn is_embedded(&self) -> bool {
149        self.embedded
150    }
151}
152
153#[derive(Debug, Clone, Eq, Ord, PartialEq, PartialOrd)]
154struct CanonicalPath(String);
155
156#[derive(Debug)]
157struct PathTreeConflict {
158    ancestor: CanonicalPath,
159    ancestor_role: PackPathRole,
160    descendant: CanonicalPath,
161    descendant_role: PackPathRole,
162}
163
164impl CanonicalPath {
165    fn as_str(&self) -> &str {
166        &self.0
167    }
168
169    fn into_string(self) -> String {
170        self.0
171    }
172}
173
174impl Borrow<str> for CanonicalPath {
175    fn borrow(&self) -> &str {
176        self.as_str()
177    }
178}
179
180impl std::fmt::Display for CanonicalPath {
181    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182        formatter.write_str(self.as_str())
183    }
184}
185
186/// A font embedded in a pack.
187#[derive(Debug, Clone)]
188pub struct PackFont {
189    /// The manifest entry describing this font.
190    entry: FontManifest,
191    /// The raw font file data.
192    data: Bytes,
193    font: Font,
194}
195
196/// The canonical content identity of one exact Font Container.
197#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
198pub struct FontContainerIdentity(u128);
199
200impl FontContainerIdentity {
201    /// Derives the identity from exact container bytes.
202    pub fn from_bytes(data: &[u8]) -> Self {
203        Self(typst::utils::hash128(&data))
204    }
205
206    /// The identity digest in big-endian order.
207    pub fn digest(self) -> [u8; 16] {
208        self.0.to_be_bytes()
209    }
210
211    pub fn kind(self) -> &'static str {
212        "font-container"
213    }
214
215    pub fn schema(self) -> &'static str {
216        "typst-pack-font-container-identity-v1"
217    }
218
219    pub fn algorithm(self) -> &'static str {
220        "typst-hash128-0.15"
221    }
222
223    fn encode(self) -> String {
224        format!("{:032x}", self.0)
225    }
226
227    fn decode(value: &str) -> Option<Self> {
228        u128::from_str_radix(value, 16).ok().map(Self)
229    }
230}
231
232/// The exact identity of one face within a Font Container.
233#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
234pub struct FontFaceIdentity {
235    container: FontContainerIdentity,
236    index: u32,
237}
238
239impl FontFaceIdentity {
240    /// The containing font file or collection.
241    pub fn container(self) -> FontContainerIdentity {
242        self.container
243    }
244
245    /// The face's container-local index.
246    pub fn index(self) -> u32 {
247        self.index
248    }
249}
250
251/// One ordered face in the exact Pack Font Catalog.
252#[derive(Debug, Clone, PartialEq, Eq)]
253pub struct PackFontCatalogFace {
254    identity: FontFaceIdentity,
255    embedded: bool,
256}
257
258impl PackFontCatalogFace {
259    /// The exact container and face index.
260    pub fn identity(&self) -> FontFaceIdentity {
261        self.identity
262    }
263
264    /// Whether the Font Container bytes are stored in the Pack.
265    pub fn is_embedded(&self) -> bool {
266        self.embedded
267    }
268}
269
270/// One exact Font Container and the faces required from it.
271#[derive(Debug, Clone, PartialEq, Eq)]
272pub struct FontRequirement {
273    container: FontContainerIdentity,
274    length: u64,
275    face_indices: Vec<u32>,
276    embedded: bool,
277}
278
279impl FontRequirement {
280    pub fn container_identity(&self) -> FontContainerIdentity {
281        self.container
282    }
283
284    pub fn container_length(&self) -> u64 {
285        self.length
286    }
287
288    pub fn face_indices(&self) -> &[u32] {
289        &self.face_indices
290    }
291
292    pub fn is_embedded(&self) -> bool {
293        self.embedded
294    }
295}
296
297impl PackFont {
298    /// The declaration describing this font face.
299    pub fn manifest(&self) -> &FontManifest {
300        &self.entry
301    }
302
303    /// The contained font bytes.
304    pub fn data(&self) -> &Bytes {
305        &self.data
306    }
307
308    /// Official selection metadata derived from the verified container bytes.
309    pub fn info(&self) -> &FontInfo {
310        self.font.info()
311    }
312}
313
314#[derive(Debug, Clone)]
315struct PackFontInput {
316    entry: FontManifest,
317    data: Bytes,
318    embedded: bool,
319}
320
321impl Pack {
322    /// Starts building a pack from in-memory data.
323    ///
324    /// `entrypoint` is the root-relative path of the main file, e.g.
325    /// `main.typ`.
326    pub fn builder(entrypoint: impl Into<String>) -> PackBuilder {
327        PackBuilder::new(entrypoint)
328    }
329
330    fn construct(
331        manifest: PackManifest,
332        files: BTreeMap<CanonicalPath, Bytes>,
333        packages: BTreeMap<String, PackageFiles>,
334        font_data: BTreeMap<CanonicalPath, Bytes>,
335    ) -> Result<Self, PackInvariantError> {
336        let entrypoint = canonical_path(PackPathRole::Entrypoint, manifest.project().entrypoint())?;
337        let canonical_files = files;
338        let font_entries = manifest
339            .fonts()
340            .iter()
341            .cloned()
342            .map(|entry| Ok((canonical_path(PackPathRole::FontData, entry.path())?, entry)))
343            .collect::<Result<Vec<_>, PackInvariantError>>()?;
344
345        let vendored_packages = manifest
346            .packages()
347            .vendored()
348            .iter()
349            .map(|entry| package_manifest_requirement(entry, true))
350            .collect::<Result<BTreeMap<_, _>, _>>()?;
351        let unvendored_packages = manifest
352            .packages()
353            .unvendored()
354            .iter()
355            .map(|entry| package_manifest_requirement(entry, false))
356            .collect::<Result<BTreeMap<_, _>, _>>()?;
357        for path in canonical_files.keys() {
358            validate_archive_entry_name(
359                PackPathRole::ProjectFile,
360                path,
361                PROJECT_PREFIX.len() + path.as_str().len(),
362            )?;
363        }
364        for package in packages.values() {
365            let spec = &package.spec;
366            let version = spec.version.to_string();
367            let package_prefix_len =
368                PACKAGES_PREFIX.len() + spec.namespace.len() + spec.name.len() + version.len() + 3;
369            for path in package.files.keys() {
370                validate_archive_entry_name(
371                    PackPathRole::PackageFile,
372                    path,
373                    package_prefix_len + path.as_str().len(),
374                )?;
375            }
376        }
377        for (path, _) in &font_entries {
378            validate_archive_entry_name(PackPathRole::FontData, path, path.as_str().len())?;
379        }
380
381        validate_project_declarations(canonical_files.keys().cloned())?;
382
383        for package in packages.values() {
384            let paths = package
385                .files
386                .keys()
387                .cloned()
388                .map(|path| (path, PackPathRole::PackageFile))
389                .collect();
390            if let Some(conflict) = find_path_tree_conflict(paths) {
391                return Err(PackInvariantError::PackagePathTreeConflict {
392                    package: package.spec.to_string(),
393                    ancestor: conflict.ancestor.to_string(),
394                    ancestor_role: conflict.ancestor_role,
395                    descendant: conflict.descendant.to_string(),
396                    descendant_role: conflict.descendant_role,
397                });
398            }
399        }
400
401        for (path, _) in &font_entries {
402            if let Some(conflicting_role) = reserved_font_path_role(path) {
403                return Err(PackInvariantError::ReservedFontPath {
404                    path: path.to_string(),
405                    conflicting_role,
406                });
407            }
408        }
409        let font_paths = font_entries
410            .iter()
411            .map(|(path, _)| path.clone())
412            .collect::<BTreeSet<_>>()
413            .into_iter()
414            .map(|path| (path, PackPathRole::FontData))
415            .collect();
416        if let Some(conflict) = find_path_tree_conflict(font_paths) {
417            return Err(PackInvariantError::PathTreeConflict {
418                ancestor: conflict.ancestor.to_string(),
419                ancestor_role: conflict.ancestor_role,
420                descendant: conflict.descendant.to_string(),
421                descendant_role: conflict.descendant_role,
422            });
423        }
424        if let Some(spec) = vendored_packages
425            .keys()
426            .find(|spec| unvendored_packages.contains_key(*spec))
427        {
428            return Err(PackInvariantError::PackageRoleConflict(spec.clone()));
429        }
430
431        if !canonical_files.contains_key(&entrypoint) {
432            return Err(PackInvariantError::MissingEntrypoint(
433                entrypoint.to_string(),
434            ));
435        }
436
437        for requirement in vendored_packages
438            .values()
439            .chain(unvendored_packages.values())
440        {
441            validate_package_spec(&requirement.spec)?;
442        }
443        for package in packages.values() {
444            validate_package_spec(&package.spec)?;
445        }
446
447        let mut canonical_packages = BTreeMap::new();
448        let mut package_requirements = Vec::new();
449        for (_, package) in packages {
450            let key = package.spec.to_string();
451            let Some(declared) = vendored_packages.get(&key) else {
452                return Err(PackInvariantError::UndeclaredPackageData(key));
453            };
454            let package_files = package.files;
455            let (tree, file_count, byte_length) = package_tree_identity(&package_files);
456            if declared.tree != tree
457                || declared.file_count != file_count
458                || declared.byte_length != byte_length
459            {
460                return Err(PackInvariantError::MismatchedEmbeddedPackageIdentity(key));
461            }
462            package_requirements.push(PackageRequirement {
463                spec: package.spec.clone(),
464                tree,
465                file_count,
466                byte_length,
467                embedded: true,
468            });
469            canonical_packages.insert(
470                key,
471                PackageFiles {
472                    spec: package.spec,
473                    files: package_files,
474                },
475            );
476        }
477        if let Some(spec) = vendored_packages
478            .keys()
479            .find(|spec| !canonical_packages.contains_key(*spec))
480        {
481            return Err(PackInvariantError::MissingVendoredPackageData(spec.clone()));
482        }
483        package_requirements.extend(unvendored_packages.values().cloned());
484        package_requirements.sort_by_key(|requirement| requirement.spec.to_string());
485
486        let mut canonical_fonts = Vec::new();
487        let mut canonical_font_entries = Vec::new();
488        let mut font_catalog = Vec::new();
489        let mut font_requirements = Vec::<FontRequirement>::new();
490        let mut font_faces = BTreeSet::new();
491        for (path, entry) in font_entries {
492            let index = entry.index();
493            let (data, parsed, container, length) = if entry.is_external() {
494                if font_data.contains_key(&path) {
495                    return Err(PackInvariantError::ExternalFontHasContainedData {
496                        path: path.to_string(),
497                    });
498                }
499                if entry.container_identity_kind() != Some("font-container")
500                    || entry.container_identity_schema()
501                        != Some("typst-pack-font-container-identity-v1")
502                    || entry.container_identity_algorithm() != Some("typst-hash128-0.15")
503                {
504                    return Err(PackInvariantError::InvalidExternalFontIdentity {
505                        path: path.to_string(),
506                    });
507                }
508                let container = entry
509                    .container_digest()
510                    .and_then(FontContainerIdentity::decode)
511                    .ok_or_else(|| PackInvariantError::InvalidExternalFontIdentity {
512                        path: path.to_string(),
513                    })?;
514                let length = entry
515                    .container_length()
516                    .filter(|length| *length > 0)
517                    .ok_or_else(|| PackInvariantError::InvalidExternalFontIdentity {
518                        path: path.to_string(),
519                    })?;
520                (None, None, container, length)
521            } else {
522                let data = font_data
523                    .get(&path)
524                    .cloned()
525                    .ok_or_else(|| PackInvariantError::MissingFontData(path.to_string()))?;
526                let parsed = Font::new(data.clone(), index).ok_or_else(|| {
527                    PackInvariantError::InvalidFontData {
528                        path: path.to_string(),
529                        index,
530                    }
531                })?;
532                let container = FontContainerIdentity::from_bytes(data.as_slice());
533                let length = data.len() as u64;
534                if entry
535                    .container_digest()
536                    .is_some_and(|digest| FontContainerIdentity::decode(digest) != Some(container))
537                    || entry
538                        .container_length()
539                        .is_some_and(|declared| declared != length)
540                    || entry
541                        .container_identity_kind()
542                        .is_some_and(|kind| kind != container.kind())
543                    || entry
544                        .container_identity_schema()
545                        .is_some_and(|schema| schema != container.schema())
546                    || entry
547                        .container_identity_algorithm()
548                        .is_some_and(|algorithm| algorithm != container.algorithm())
549                {
550                    return Err(PackInvariantError::MismatchedEmbeddedFontIdentity {
551                        path: path.to_string(),
552                    });
553                }
554                (Some(data), Some(parsed), container, length)
555            };
556            if !font_faces.insert((container, index)) {
557                return Err(PackInvariantError::DuplicateFontFace {
558                    path: path.to_string(),
559                    index,
560                });
561            }
562            let embedded = !entry.is_external();
563            font_catalog.push(PackFontCatalogFace {
564                identity: FontFaceIdentity { container, index },
565                embedded,
566            });
567            match font_requirements
568                .iter_mut()
569                .find(|requirement| requirement.container == container)
570            {
571                Some(requirement)
572                    if requirement.length != length || requirement.embedded != embedded =>
573                {
574                    return Err(PackInvariantError::InconsistentFontContainer {
575                        path: path.to_string(),
576                    });
577                }
578                Some(requirement) => requirement.face_indices.push(index),
579                None => font_requirements.push(FontRequirement {
580                    container,
581                    length,
582                    face_indices: vec![index],
583                    embedded,
584                }),
585            }
586            let canonical_entry = FontManifest::new(
587                path.into_string(),
588                index,
589                entry.families().to_vec(),
590                !embedded,
591                container.encode(),
592                length,
593            );
594            canonical_font_entries.push(canonical_entry.clone());
595            if let (Some(data), Some(font)) = (data, parsed) {
596                canonical_fonts.push(PackFont {
597                    entry: canonical_entry,
598                    data,
599                    font,
600                });
601            }
602        }
603
604        let manifest = PackManifest::new(
605            entrypoint.into_string(),
606            package_requirements
607                .iter()
608                .filter(|requirement| requirement.embedded)
609                .map(package_requirement_manifest)
610                .collect(),
611            package_requirements
612                .iter()
613                .filter(|requirement| !requirement.embedded)
614                .map(package_requirement_manifest)
615                .collect(),
616            canonical_font_entries,
617            manifest.metadata().cloned(),
618        );
619
620        let pack = Self {
621            manifest,
622            files: canonical_files,
623            packages: canonical_packages,
624            package_requirements,
625            fonts: canonical_fonts,
626            font_catalog,
627            font_requirements,
628        };
629        Ok(pack)
630    }
631
632    /// The pack manifest.
633    pub fn manifest(&self) -> &PackManifest {
634        &self.manifest
635    }
636
637    /// Derives the Pack's identity-bearing semantic projection.
638    pub fn identity(&self) -> PackIdentity {
639        let project_files = self
640            .files()
641            .map(|(path, data)| (path, typst::utils::hash128(data)))
642            .collect::<Vec<_>>();
643        let packages = self
644            .package_requirements()
645            .iter()
646            .map(|requirement| {
647                (
648                    requirement.spec.to_string(),
649                    requirement.tree.0,
650                    requirement.file_count,
651                    requirement.byte_length,
652                    requirement.embedded,
653                )
654            })
655            .collect::<Vec<_>>();
656        let fonts = self
657            .font_catalog()
658            .iter()
659            .map(|face| {
660                (
661                    face.identity.container.0,
662                    face.identity.index,
663                    face.embedded,
664                )
665            })
666            .collect::<Vec<_>>();
667        PackIdentity(typst::utils::hash128(&(
668            "typst-pack-identity-v1",
669            self.entrypoint(),
670            project_files,
671            packages,
672            fonts,
673        )))
674    }
675
676    /// The root-relative path of the entrypoint file.
677    pub fn entrypoint(&self) -> &str {
678        self.manifest.project().entrypoint()
679    }
680
681    /// The project files, keyed by root-relative path.
682    pub fn files(&self) -> impl Iterator<Item = (&str, &Bytes)> {
683        self.files.iter().map(|(path, data)| (path.as_str(), data))
684    }
685
686    /// Looks up a project file by root-relative path.
687    pub fn file(&self, path: &str) -> Option<&Bytes> {
688        self.files.get(path)
689    }
690
691    pub(crate) fn canonical_project_path(path: &str) -> Result<String, String> {
692        canonical_path(PackPathRole::ProjectFile, path)
693            .map(CanonicalPath::into_string)
694            .map_err(|error| error.to_string())
695    }
696
697    /// The vendored packages and their files.
698    pub fn packages(
699        &self,
700    ) -> impl Iterator<Item = (&PackageSpec, impl Iterator<Item = (&str, &Bytes)>)> {
701        self.packages.values().map(|package| {
702            (
703                &package.spec,
704                package
705                    .files
706                    .iter()
707                    .map(|(path, data)| (path.as_str(), data)),
708            )
709        })
710    }
711
712    /// Looks up a vendored package file.
713    pub fn package_file(&self, spec: &PackageSpec, path: &str) -> Option<&Bytes> {
714        self.packages.get(&spec.to_string())?.files.get(path)
715    }
716
717    /// Whether the pack vendors the given package.
718    pub fn has_package(&self, spec: &PackageSpec) -> bool {
719        self.packages.contains_key(&spec.to_string())
720    }
721
722    /// The Pack's exact Package Requirements in canonical specification order.
723    pub fn package_requirements(&self) -> &[PackageRequirement] {
724        &self.package_requirements
725    }
726
727    pub(crate) fn materialize_package_trees(
728        &self,
729        fulfillments: BTreeMap<String, Vec<(String, Bytes)>>,
730    ) -> Result<BTreeMap<String, PackageFiles>, PackageTreeError> {
731        let missing = self
732            .package_requirements
733            .iter()
734            .filter(|requirement| !requirement.embedded)
735            .filter(|requirement| !fulfillments.contains_key(&requirement.spec.to_string()))
736            .map(|requirement| requirement.spec.clone())
737            .collect::<Vec<_>>();
738        if !missing.is_empty() {
739            return Err(PackageTreeError::Missing { packages: missing });
740        }
741
742        let mut materialized = self.packages.clone();
743        for requirement in self
744            .package_requirements
745            .iter()
746            .filter(|requirement| !requirement.embedded)
747        {
748            let key = requirement.spec.to_string();
749            let mut files = BTreeMap::new();
750            for (path, data) in &fulfillments[&key] {
751                let canonical =
752                    canonical_path(PackPathRole::PackageFile, path).map_err(|error| {
753                        PackageTreeError::Malformed {
754                            spec: requirement.spec.clone(),
755                            path: path.clone(),
756                            message: error.to_string(),
757                        }
758                    })?;
759                if files.insert(canonical, data.clone()).is_some() {
760                    return Err(PackageTreeError::Malformed {
761                        spec: requirement.spec.clone(),
762                        path: path.clone(),
763                        message: "duplicate package file path".to_owned(),
764                    });
765                }
766            }
767            let paths = files
768                .keys()
769                .cloned()
770                .map(|path| (path, PackPathRole::PackageFile))
771                .collect();
772            if let Some(conflict) = find_path_tree_conflict(paths) {
773                return Err(PackageTreeError::Malformed {
774                    spec: requirement.spec.clone(),
775                    path: conflict.descendant.to_string(),
776                    message: format!("file path has file ancestor `{}`", conflict.ancestor),
777                });
778            }
779            let (actual, actual_file_count, actual_byte_length) = package_tree_identity(&files);
780            if actual != requirement.tree
781                || actual_file_count != requirement.file_count
782                || actual_byte_length != requirement.byte_length
783            {
784                return Err(PackageTreeError::Mismatched {
785                    spec: requirement.spec.clone(),
786                    expected: requirement.tree,
787                    actual,
788                    expected_file_count: requirement.file_count,
789                    actual_file_count,
790                    expected_byte_length: requirement.byte_length,
791                    actual_byte_length,
792                });
793            }
794            materialized.insert(
795                key,
796                PackageFiles {
797                    spec: requirement.spec.clone(),
798                    files,
799                },
800            );
801        }
802        Ok(materialized)
803    }
804
805    /// The fonts embedded in the pack.
806    pub fn fonts(&self) -> &[PackFont] {
807        &self.fonts
808    }
809
810    /// The exact candidate faces exposed to official Typst, in stable order.
811    pub fn font_catalog(&self) -> &[PackFontCatalogFace] {
812        &self.font_catalog
813    }
814
815    /// The exact Font Containers required by this Pack.
816    pub fn font_requirements(&self) -> &[FontRequirement] {
817        &self.font_requirements
818    }
819
820    pub(crate) fn materialize_font_catalog(
821        &self,
822        fulfillments: &BTreeMap<FontContainerIdentity, Bytes>,
823    ) -> Result<Vec<Font>, FontCatalogError> {
824        let missing = self
825            .font_requirements
826            .iter()
827            .filter(|requirement| !requirement.embedded)
828            .map(|requirement| requirement.container)
829            .filter(|container| !fulfillments.contains_key(container))
830            .collect::<Vec<_>>();
831        if !missing.is_empty() {
832            return Err(FontCatalogError::Missing {
833                containers: missing,
834            });
835        }
836        self.font_catalog
837            .iter()
838            .map(|face| {
839                let identity = face.identity;
840                if face.embedded {
841                    return Ok(self
842                        .fonts
843                        .iter()
844                        .find(|font| {
845                            FontContainerIdentity::from_bytes(font.data.as_slice())
846                                == identity.container
847                                && font.entry.index() == identity.index
848                        })
849                        .expect("Pack Font Catalog embedded face invariant violated")
850                        .font
851                        .clone());
852                }
853                let data = &fulfillments[&identity.container];
854                let actual = FontContainerIdentity::from_bytes(data.as_slice());
855                let actual_length = data.len() as u64;
856                let expected_length = self
857                    .font_requirements
858                    .iter()
859                    .find(|requirement| requirement.container == identity.container)
860                    .expect("Pack Font Catalog requirement invariant violated")
861                    .length;
862                if actual != identity.container || actual_length != expected_length {
863                    return Err(FontCatalogError::Mismatched {
864                        expected: identity.container,
865                        actual,
866                        expected_length,
867                        actual_length,
868                    });
869                }
870                Font::new(data.clone(), identity.index).ok_or(FontCatalogError::Malformed {
871                    container: identity.container,
872                    index: identity.index,
873                })
874            })
875            .collect()
876    }
877
878    pub(crate) fn materialize_compilation_dependency_snapshot(
879        &self,
880        package_fulfillments: BTreeMap<String, Vec<(String, Bytes)>>,
881        font_fulfillments: &BTreeMap<FontContainerIdentity, Bytes>,
882    ) -> Result<CompilationDependencySnapshot, CompilationDependencySnapshotError> {
883        let packages = self
884            .materialize_package_trees(package_fulfillments)
885            .map_err(|error| CompilationDependencySnapshotError::Package(Box::new(error)))?;
886        let font_catalog = self
887            .materialize_font_catalog(font_fulfillments)
888            .map_err(CompilationDependencySnapshotError::Font)?;
889        Ok(CompilationDependencySnapshot {
890            pack_identity: self.identity(),
891            packages,
892            font_catalog,
893        })
894    }
895
896    /// Reads a pack from a seekable reader.
897    pub fn read<R: Read + Seek>(reader: R) -> Result<Self, PackReadError> {
898        let archive = ZipArchive::new(reader)?;
899        let retained_entry_count = archive.len();
900        let central_directory_start = archive.central_directory_start();
901        let mut reader = archive.into_inner();
902        let raw_entries = raw_central_entries(&mut reader, central_directory_start)?;
903        let mut archive = ZipArchive::new(reader)?;
904        const FILE_TYPE_MASK: u32 = 0o170000;
905        const REGULAR_FILE: u32 = 0o100000;
906
907        let mut manifest_entry = None;
908        for index in 0..archive.len() {
909            let entry = archive.by_index_raw(index)?;
910            let prefix_normalized_name = strip_current_directory_prefix(entry.name());
911            let canonical_manifest_alias = !prefix_normalized_name.starts_with(PROJECT_PREFIX)
912                && !prefix_normalized_name.starts_with(PACKAGES_PREFIX)
913                && canonical_archive_name(entry.name()).is_ok_and(|name| name == MANIFEST_PATH);
914            if prefix_normalized_name == MANIFEST_PATH || canonical_manifest_alias {
915                let regular_file = entry.is_file()
916                    && entry
917                        .unix_mode()
918                        .is_none_or(|mode| matches!(mode & FILE_TYPE_MASK, 0 | REGULAR_FILE));
919                manifest_entry = Some((index, regular_file));
920                break;
921            }
922        }
923        let (manifest_index, manifest_is_file) =
924            manifest_entry.ok_or(PackReadError::MissingManifest)?;
925        if !manifest_is_file {
926            return Err(PackReadError::ManifestNotFile);
927        }
928        let manifest_value = {
929            let mut entry = archive.by_index(manifest_index)?;
930            let mut bytes = Vec::new();
931            entry
932                .read_to_end(&mut bytes)
933                .map_err(PackReadError::ManifestUnreadable)?;
934            let text = std::str::from_utf8(&bytes).map_err(PackReadError::ManifestNotUtf8)?;
935            toml::from_str::<toml::Value>(text).map_err(PackManifestError::from)?
936        };
937
938        let mut raw_names = BTreeSet::new();
939        for entry in &raw_entries {
940            if !raw_names.insert(entry.name.clone()) {
941                if entry.name == MANIFEST_PATH.as_bytes() {
942                    return Err(PackReadError::DuplicateManifest);
943                }
944                return Err(PackReadError::DuplicateArchiveEntry(entry.name.clone()));
945            }
946        }
947        if raw_entries.len() != retained_entry_count {
948            return Err(PackReadError::AmbiguousArchiveEntries);
949        }
950        let manifest = PackManifest::from_toml_value(manifest_value)?;
951
952        struct ProjectEntry {
953            index: usize,
954            path: CanonicalPath,
955        }
956        struct PackageEntry {
957            index: usize,
958            spec: PackageSpec,
959            path: CanonicalPath,
960        }
961        struct UnknownEntry {
962            index: usize,
963            archive_name: String,
964            raw_name: Vec<u8>,
965            canonical_name: String,
966            regular_file: bool,
967        }
968
969        let mut project_entries = Vec::new();
970        let mut package_entries = Vec::new();
971        let mut unknown_entries = Vec::new();
972        let mut canonical_archive_entries = BTreeMap::new();
973        for (index, raw_entry) in raw_entries.iter().enumerate() {
974            let entry = archive.by_index_raw(index)?;
975            let archive_name = entry.name().to_owned();
976            let raw_name = raw_entry.name.clone();
977            let prefix_normalized_name = strip_current_directory_prefix(&archive_name);
978            let canonical_name = canonical_archive_name(&archive_name)?;
979            register_archive_identity(
980                &mut canonical_archive_entries,
981                canonical_name.clone(),
982                &raw_name,
983            )?;
984            if entry.is_dir() {
985                continue;
986            }
987            let regular_file = entry.is_file()
988                && entry
989                    .unix_mode()
990                    .is_none_or(|mode| matches!(mode & FILE_TYPE_MASK, 0 | REGULAR_FILE));
991            let role_name = if prefix_normalized_name == MANIFEST_PATH
992                || prefix_normalized_name.starts_with(PROJECT_PREFIX)
993                || prefix_normalized_name.starts_with(PACKAGES_PREFIX)
994            {
995                prefix_normalized_name
996            } else {
997                canonical_name.as_str()
998            };
999
1000            if role_name == MANIFEST_PATH {
1001                register_archive_identity(
1002                    &mut canonical_archive_entries,
1003                    MANIFEST_PATH.to_owned(),
1004                    &raw_name,
1005                )?;
1006            } else if let Some(path) = role_name.strip_prefix(PROJECT_PREFIX) {
1007                if !regular_file {
1008                    return Err(PackReadError::UnsupportedEntryType(archive_name));
1009                }
1010                let path = canonical_path(PackPathRole::ProjectFile, path.trim_start_matches('/'))?;
1011                register_archive_identity(
1012                    &mut canonical_archive_entries,
1013                    format!("{PROJECT_PREFIX}{path}"),
1014                    &raw_name,
1015                )?;
1016                project_entries.push(ProjectEntry { index, path });
1017            } else if let Some(rest) = role_name.strip_prefix(PACKAGES_PREFIX) {
1018                if !regular_file {
1019                    return Err(PackReadError::UnsupportedEntryType(archive_name));
1020                }
1021                let (spec, path) = split_package_entry(rest, &archive_name)?;
1022                register_archive_identity(
1023                    &mut canonical_archive_entries,
1024                    format!(
1025                        "{PACKAGES_PREFIX}{}/{}/{}/{path}",
1026                        spec.namespace, spec.name, spec.version
1027                    ),
1028                    &raw_name,
1029                )?;
1030                package_entries.push(PackageEntry { index, spec, path });
1031            } else {
1032                unknown_entries.push(UnknownEntry {
1033                    index,
1034                    archive_name,
1035                    raw_name,
1036                    canonical_name,
1037                    regular_file,
1038                });
1039            }
1040        }
1041
1042        let font_paths = manifest
1043            .fonts()
1044            .iter()
1045            .filter_map(|font| canonical_path(PackPathRole::FontData, font.path()).ok())
1046            .collect::<BTreeSet<_>>();
1047        let mut font_entries = Vec::new();
1048        for entry in unknown_entries {
1049            if let Some(path) = font_paths.get(entry.canonical_name.as_str()) {
1050                if !entry.regular_file {
1051                    return Err(PackReadError::UnsupportedEntryType(entry.archive_name));
1052                }
1053                register_archive_identity(
1054                    &mut canonical_archive_entries,
1055                    path.to_string(),
1056                    &entry.raw_name,
1057                )?;
1058                font_entries.push((entry.index, path.clone()));
1059            }
1060        }
1061
1062        let mut files = BTreeMap::new();
1063        for project in project_entries {
1064            let mut data = Vec::new();
1065            archive.by_index(project.index)?.read_to_end(&mut data)?;
1066            files.insert(project.path, Bytes::new(data));
1067        }
1068        let mut packages: BTreeMap<String, PackageFiles> = BTreeMap::new();
1069        for package in package_entries {
1070            let mut data = Vec::new();
1071            archive.by_index(package.index)?.read_to_end(&mut data)?;
1072            packages
1073                .entry(package.spec.to_string())
1074                .or_insert_with(|| PackageFiles {
1075                    spec: package.spec,
1076                    files: BTreeMap::new(),
1077                })
1078                .files
1079                .insert(package.path, Bytes::new(data));
1080        }
1081        let mut fonts_by_path = BTreeMap::new();
1082        for (index, path) in font_entries {
1083            let mut data = Vec::new();
1084            archive.by_index(index)?.read_to_end(&mut data)?;
1085            fonts_by_path.insert(path, Bytes::new(data));
1086        }
1087
1088        Ok(Self::construct(manifest, files, packages, fonts_by_path)?)
1089    }
1090
1091    /// Reads a pack from a byte buffer.
1092    pub fn from_bytes(bytes: impl Into<Vec<u8>>) -> Result<Self, PackReadError> {
1093        Self::read(Cursor::new(bytes.into()))
1094    }
1095
1096    /// Writes the pack archive to a seekable writer.
1097    pub fn write<W: Write + Seek>(&self, writer: W) -> Result<(), PackWriteError> {
1098        let mut zip = ZipWriter::new(writer);
1099        let manifest = self.manifest.to_toml();
1100
1101        zip.start_file(MANIFEST_PATH, zip_file_options(manifest.len()))?;
1102        zip.write_all(manifest.as_bytes())?;
1103
1104        for (path, data) in &self.files {
1105            zip.start_file(
1106                format!("{PROJECT_PREFIX}{path}"),
1107                zip_file_options(data.len()),
1108            )?;
1109            zip.write_all(data)?;
1110        }
1111
1112        for package in self.packages.values() {
1113            let spec = &package.spec;
1114            for (path, data) in &package.files {
1115                zip.start_file(
1116                    format!(
1117                        "{PACKAGES_PREFIX}{}/{}/{}/{path}",
1118                        spec.namespace, spec.name, spec.version
1119                    ),
1120                    zip_file_options(data.len()),
1121                )?;
1122                zip.write_all(data)?;
1123            }
1124        }
1125
1126        let mut written = std::collections::BTreeSet::new();
1127        for font in &self.fonts {
1128            if written.insert(font.manifest().path()) {
1129                zip.start_file(font.manifest().path(), zip_file_options(font.data().len()))?;
1130                zip.write_all(font.data())?;
1131            }
1132        }
1133
1134        zip.finish()?;
1135        Ok(())
1136    }
1137
1138    /// Serializes the pack archive to a byte buffer.
1139    pub fn to_bytes(&self) -> Result<Vec<u8>, PackWriteError> {
1140        let mut buffer = Cursor::new(Vec::new());
1141        self.write(&mut buffer)?;
1142        Ok(buffer.into_inner())
1143    }
1144}
1145
1146/// A Pack-owned failure to materialize its exact Font Catalog.
1147#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1148pub enum FontCatalogError {
1149    #[error("exact font containers {containers:?} are unavailable")]
1150    Missing {
1151        containers: Vec<FontContainerIdentity>,
1152    },
1153    #[error("font container fulfillment does not match {expected:?}")]
1154    Mismatched {
1155        expected: FontContainerIdentity,
1156        actual: FontContainerIdentity,
1157        expected_length: u64,
1158        actual_length: u64,
1159    },
1160    #[error("font container {container:?} has no valid face at index {index}")]
1161    Malformed {
1162        container: FontContainerIdentity,
1163        index: u32,
1164    },
1165}
1166
1167/// A Pack-owned failure to materialize exact Complete Package Trees.
1168#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1169pub enum PackageTreeError {
1170    #[error("exact package trees {packages:?} are unavailable")]
1171    Missing { packages: Vec<PackageSpec> },
1172    #[error("package fulfillment for {spec} does not match its Complete Package Tree identity")]
1173    Mismatched {
1174        spec: PackageSpec,
1175        expected: PackageTreeIdentity,
1176        actual: PackageTreeIdentity,
1177        expected_file_count: u64,
1178        actual_file_count: u64,
1179        expected_byte_length: u64,
1180        actual_byte_length: u64,
1181    },
1182    #[error("package fulfillment for {spec} has malformed path `{path}`: {message}")]
1183    Malformed {
1184        spec: PackageSpec,
1185        path: String,
1186        message: String,
1187    },
1188}
1189
1190/// A Pack-owned failure to construct a complete Compilation Dependency Snapshot.
1191#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1192pub(crate) enum CompilationDependencySnapshotError {
1193    #[error(transparent)]
1194    Package(Box<PackageTreeError>),
1195    #[error(transparent)]
1196    Font(FontCatalogError),
1197}
1198
1199fn package_tree_identity(
1200    files: &BTreeMap<CanonicalPath, Bytes>,
1201) -> (PackageTreeIdentity, u64, u64) {
1202    let file_count = files.len() as u64;
1203    let byte_length = files.values().map(|data| data.len() as u64).sum();
1204    let projection = files
1205        .iter()
1206        .map(|(path, data)| {
1207            (
1208                path.as_str(),
1209                data.len() as u64,
1210                typst::utils::hash128(data),
1211            )
1212        })
1213        .collect::<Vec<_>>();
1214    (
1215        PackageTreeIdentity(typst::utils::hash128(&(
1216            PACKAGE_TREE_IDENTITY_SCHEMA,
1217            file_count,
1218            byte_length,
1219            projection,
1220        ))),
1221        file_count,
1222        byte_length,
1223    )
1224}
1225
1226fn package_manifest_requirement(
1227    manifest: &PackageManifest,
1228    embedded: bool,
1229) -> Result<(String, PackageRequirement), PackInvariantError> {
1230    let spec = manifest.spec().map_err(|error| match error {
1231        PackManifestError::InvalidPackageSpec { spec, message } => {
1232            PackInvariantError::InvalidPackageSpec { spec, message }
1233        }
1234        error => PackInvariantError::InvalidPackageRequirement {
1235            spec: error.to_string(),
1236        },
1237    })?;
1238    if manifest.tree_identity_kind() != PACKAGE_TREE_IDENTITY_KIND
1239        || manifest.tree_identity_schema() != PACKAGE_TREE_IDENTITY_SCHEMA
1240        || manifest.tree_identity_algorithm() != PACKAGE_TREE_IDENTITY_ALGORITHM
1241        || manifest.file_count() == 0
1242    {
1243        return Err(PackInvariantError::InvalidPackageRequirement {
1244            spec: spec.to_string(),
1245        });
1246    }
1247    let tree = PackageTreeIdentity::decode(manifest.tree_digest()).ok_or_else(|| {
1248        PackInvariantError::InvalidPackageRequirement {
1249            spec: spec.to_string(),
1250        }
1251    })?;
1252    let key = spec.to_string();
1253    Ok((
1254        key,
1255        PackageRequirement {
1256            spec,
1257            tree,
1258            file_count: manifest.file_count(),
1259            byte_length: manifest.byte_length(),
1260            embedded,
1261        },
1262    ))
1263}
1264
1265fn package_requirement_manifest(requirement: &PackageRequirement) -> PackageManifest {
1266    PackageManifest::new(
1267        requirement.spec.clone(),
1268        requirement.tree.encode(),
1269        requirement.file_count,
1270        requirement.byte_length,
1271    )
1272}
1273
1274fn zip_file_options(size: usize) -> SimpleFileOptions {
1275    // Deflate may expand incompressible input. Nine bits per input byte plus
1276    // framing is a conservative bound for the configured encoder.
1277    let compressed_bound = size.saturating_add(size.div_ceil(8)).saturating_add(16);
1278    let compressed_bound = u64::try_from(compressed_bound).unwrap_or(u64::MAX);
1279    SimpleFileOptions::default()
1280        .compression_method(zip::CompressionMethod::Deflated)
1281        .large_file(compressed_bound > zip::ZIP64_BYTES_THR)
1282}
1283
1284struct RawCentralEntry {
1285    name: Vec<u8>,
1286}
1287
1288fn raw_central_entries<R: Read + Seek>(
1289    reader: &mut R,
1290    central_directory_start: u64,
1291) -> Result<Vec<RawCentralEntry>, PackReadError> {
1292    reader.seek(SeekFrom::Start(central_directory_start))?;
1293    let mut entries = Vec::new();
1294    loop {
1295        let header_start = reader.stream_position()?;
1296        let mut signature = [0; 4];
1297        reader.read_exact(&mut signature)?;
1298        if signature != *b"PK\x01\x02" {
1299            reader.seek(SeekFrom::Start(header_start))?;
1300            break;
1301        }
1302
1303        let mut fixed = [0; 42];
1304        reader.read_exact(&mut fixed)?;
1305        let name_len = u16::from_le_bytes([fixed[24], fixed[25]]) as usize;
1306        let extra_len = u16::from_le_bytes([fixed[26], fixed[27]]) as i64;
1307        let comment_len = u16::from_le_bytes([fixed[28], fixed[29]]) as i64;
1308        let mut name = vec![0; name_len];
1309        reader.read_exact(&mut name)?;
1310        reader.seek(SeekFrom::Current(extra_len + comment_len))?;
1311        entries.push(RawCentralEntry { name });
1312    }
1313    Ok(entries)
1314}
1315
1316/// Splits `namespace/name/version/rest...` into a package spec and file path.
1317fn split_package_entry(
1318    rest: &str,
1319    entry: &str,
1320) -> Result<(PackageSpec, CanonicalPath), PackReadError> {
1321    let mut parts = rest.splitn(4, '/');
1322    let (Some(namespace), Some(name), Some(version), Some(path)) =
1323        (parts.next(), parts.next(), parts.next(), parts.next())
1324    else {
1325        return Err(PackReadError::InvalidEntry {
1326            entry: entry.to_owned(),
1327            message: "expected packages/<namespace>/<name>/<version>/<path>".into(),
1328        });
1329    };
1330    let spec = PackageSpec::from_str(&format!("@{namespace}/{name}:{version}")).map_err(|err| {
1331        PackReadError::InvalidEntry {
1332            entry: entry.to_owned(),
1333            message: err.to_string(),
1334        }
1335    })?;
1336    let path = canonical_path(PackPathRole::PackageFile, path.trim_start_matches('/'))?;
1337    Ok((spec, path))
1338}
1339
1340/// A failure while reading a pack archive.
1341#[derive(Debug, thiserror::Error)]
1342pub enum PackReadError {
1343    #[error("failed to read archive: {0}")]
1344    Zip(#[from] zip::result::ZipError),
1345    #[error("i/o error while reading archive: {0}")]
1346    Io(#[from] std::io::Error),
1347    #[error("the archive contains no {MANIFEST_PATH} manifest (is this a Typst pack?)")]
1348    MissingManifest,
1349    #[error("the archive contains more than one {MANIFEST_PATH} manifest")]
1350    DuplicateManifest,
1351    #[error("the archive contains a duplicate entry named {0:?}")]
1352    DuplicateArchiveEntry(Vec<u8>),
1353    #[error("the archive contains entries with ambiguous effective names")]
1354    AmbiguousArchiveEntries,
1355    #[error("the {MANIFEST_PATH} manifest is not a regular file")]
1356    ManifestNotFile,
1357    #[error("the {MANIFEST_PATH} manifest could not be read: {0}")]
1358    ManifestUnreadable(#[source] std::io::Error),
1359    #[error("the {MANIFEST_PATH} manifest is not valid UTF-8: {0}")]
1360    ManifestNotUtf8(#[source] std::str::Utf8Error),
1361    #[error(transparent)]
1362    Manifest(#[from] PackManifestError),
1363    #[error("archive entry `{0}` has an unsafe path")]
1364    UnsafeEntry(String),
1365    #[error("invalid archive entry `{entry}`: {message}")]
1366    InvalidEntry { entry: String, message: String },
1367    #[error("archive entry `{0}` is not a regular file")]
1368    UnsupportedEntryType(String),
1369    #[error(transparent)]
1370    Invariant(#[from] PackInvariantError),
1371}
1372
1373/// A failure while writing a pack archive.
1374#[derive(Debug, thiserror::Error)]
1375pub enum PackWriteError {
1376    #[error("failed to write archive: {0}")]
1377    Zip(#[from] zip::result::ZipError),
1378    #[error("i/o error while writing archive: {0}")]
1379    Io(#[from] std::io::Error),
1380}
1381
1382/// Builds a [`Pack`] from in-memory data.
1383///
1384/// This is the constructor to use when the project does not live on a file
1385/// system, for example in a web editor. For packing a project directory, use
1386/// `Packer` instead (requires the `fs` feature).
1387#[derive(Debug)]
1388pub struct PackBuilder {
1389    entrypoint: String,
1390    files: BTreeMap<CanonicalPath, Bytes>,
1391    packages: BTreeMap<String, PackageFiles>,
1392    external_packages: BTreeMap<String, PackageFiles>,
1393    fonts: Vec<PackFontInput>,
1394    metadata: Option<PackMetadata>,
1395}
1396
1397impl PackBuilder {
1398    /// Creates a builder for a pack with the given entrypoint path.
1399    pub fn new(entrypoint: impl Into<String>) -> Self {
1400        Self {
1401            entrypoint: entrypoint.into(),
1402            files: BTreeMap::new(),
1403            packages: BTreeMap::new(),
1404            external_packages: BTreeMap::new(),
1405            fonts: Vec::new(),
1406            metadata: None,
1407        }
1408    }
1409
1410    /// Adds a project file under a root-relative path.
1411    pub fn file(
1412        mut self,
1413        path: impl AsRef<str>,
1414        data: impl Into<Vec<u8>>,
1415    ) -> Result<Self, PackBuildError> {
1416        let path = canonical_path(PackPathRole::ProjectFile, path.as_ref())?;
1417        self.files.insert(path, Bytes::new(data.into()));
1418        Ok(self)
1419    }
1420
1421    /// Adds a file of a vendored package.
1422    pub fn package_file(
1423        mut self,
1424        spec: PackageSpec,
1425        path: impl AsRef<str>,
1426        data: impl Into<Vec<u8>>,
1427    ) -> Result<Self, PackBuildError> {
1428        let path = canonical_path(PackPathRole::PackageFile, path.as_ref())?;
1429        self.packages
1430            .entry(spec.to_string())
1431            .or_insert_with(|| PackageFiles {
1432                spec,
1433                files: BTreeMap::new(),
1434            })
1435            .files
1436            .insert(path, Bytes::new(data.into()));
1437        Ok(self)
1438    }
1439
1440    /// Adds a file to an exact Complete Package Tree fulfilled outside the Pack.
1441    pub fn external_package_file(
1442        mut self,
1443        spec: PackageSpec,
1444        path: impl AsRef<str>,
1445        data: impl Into<Vec<u8>>,
1446    ) -> Result<Self, PackBuildError> {
1447        let path = canonical_path(PackPathRole::PackageFile, path.as_ref())?;
1448        self.external_packages
1449            .entry(spec.to_string())
1450            .or_insert_with(|| PackageFiles {
1451                spec,
1452                files: BTreeMap::new(),
1453            })
1454            .files
1455            .insert(path, Bytes::new(data.into()));
1456        Ok(self)
1457    }
1458
1459    /// Embeds a font file.
1460    ///
1461    /// `index` is the face index for font collections and zero otherwise. The
1462    /// entry name and family list are derived from the font data.
1463    pub fn font(mut self, data: impl Into<Vec<u8>>, index: u32) -> Result<Self, PackBuildError> {
1464        let data = data.into();
1465        let info = FontInfo::new(&data, index).ok_or(PackBuildError::InvalidFontInput { index })?;
1466        let family = info.family.to_string();
1467        let path = self.font_path(&family, &data);
1468        self.fonts.push(PackFontInput {
1469            entry: FontManifest::new(
1470                path,
1471                index,
1472                vec![family],
1473                false,
1474                FontContainerIdentity::from_bytes(&data).encode(),
1475                data.len() as u64,
1476            ),
1477            data: Bytes::new(data),
1478            embedded: true,
1479        });
1480        Ok(self)
1481    }
1482
1483    /// Records an exact font dependency without storing its container bytes.
1484    pub fn external_font(
1485        mut self,
1486        data: impl Into<Vec<u8>>,
1487        index: u32,
1488    ) -> Result<Self, PackBuildError> {
1489        let data = data.into();
1490        let info = FontInfo::new(&data, index).ok_or(PackBuildError::InvalidFontInput { index })?;
1491        let family = info.family.to_string();
1492        let path = self.font_path(&family, &data);
1493        self.fonts.push(PackFontInput {
1494            entry: FontManifest::new(
1495                path,
1496                index,
1497                vec![family],
1498                true,
1499                FontContainerIdentity::from_bytes(&data).encode(),
1500                data.len() as u64,
1501            ),
1502            data: Bytes::new(data),
1503            embedded: false,
1504        });
1505        Ok(self)
1506    }
1507
1508    /// Sets descriptive metadata.
1509    pub fn metadata(mut self, metadata: PackMetadata) -> Self {
1510        self.metadata = Some(metadata);
1511        self
1512    }
1513
1514    /// Finishes the pack.
1515    pub fn build(self) -> Result<Pack, PackBuildError> {
1516        let entrypoint = canonical_path(PackPathRole::Entrypoint, &self.entrypoint)?;
1517        let font_data = self
1518            .fonts
1519            .iter()
1520            .filter(|font| font.embedded)
1521            .map(|font| {
1522                Ok((
1523                    canonical_path(PackPathRole::FontData, font.entry.path())?,
1524                    font.data.clone(),
1525                ))
1526            })
1527            .collect::<Result<BTreeMap<_, _>, PackInvariantError>>()?;
1528        let vendored_requirements = self
1529            .packages
1530            .values()
1531            .map(|package| {
1532                let (identity, file_count, byte_length) = package_tree_identity(&package.files);
1533                PackageManifest::new(
1534                    package.spec.clone(),
1535                    identity.encode(),
1536                    file_count,
1537                    byte_length,
1538                )
1539            })
1540            .collect();
1541        let external_requirements = self
1542            .external_packages
1543            .values()
1544            .map(|package| {
1545                let (identity, file_count, byte_length) = package_tree_identity(&package.files);
1546                PackageManifest::new(
1547                    package.spec.clone(),
1548                    identity.encode(),
1549                    file_count,
1550                    byte_length,
1551                )
1552            })
1553            .collect();
1554        let manifest = PackManifest::new(
1555            entrypoint.into_string(),
1556            vendored_requirements,
1557            external_requirements,
1558            self.fonts.iter().map(|font| font.entry.clone()).collect(),
1559            self.metadata,
1560        );
1561
1562        Ok(Pack::construct(
1563            manifest,
1564            self.files,
1565            self.packages,
1566            font_data,
1567        )?)
1568    }
1569
1570    /// Picks a unique archive path for a font file.
1571    fn font_path(&self, family: &str, data: &[u8]) -> String {
1572        if let Some(existing) = self.fonts.iter().find(|font| font.data.as_slice() == data) {
1573            return existing.entry.path().to_owned();
1574        }
1575        let extension = match data.get(..4) {
1576            Some(b"OTTO") => "otf",
1577            Some(b"ttcf") => "ttc",
1578            _ => "ttf",
1579        };
1580        let stem: String = family
1581            .to_lowercase()
1582            .chars()
1583            .map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
1584            .collect();
1585        let stem = stem.trim_matches('-');
1586        let stem = if stem.is_empty() { "font" } else { stem };
1587
1588        let mut candidate = format!("fonts/{stem}.{extension}");
1589        let mut counter = 1;
1590        loop {
1591            match self
1592                .fonts
1593                .iter()
1594                .find(|font| font.entry.path() == candidate)
1595            {
1596                None => return candidate,
1597                Some(existing) if existing.data.as_slice() == data => return candidate,
1598                Some(_) => {
1599                    counter += 1;
1600                    candidate = format!("fonts/{stem}-{counter}.{extension}");
1601                }
1602            }
1603        }
1604    }
1605}
1606
1607fn canonical_path(role: PackPathRole, path: &str) -> Result<CanonicalPath, PackInvariantError> {
1608    let invalid = |message: String| PackInvariantError::InvalidPath {
1609        role,
1610        path: path.to_owned(),
1611        message,
1612    };
1613    if path.is_empty() || path.starts_with('/') || path.starts_with('\\') {
1614        return Err(invalid("path must name a root-relative file".to_owned()));
1615    }
1616    if path.contains('\\') {
1617        return Err(invalid(
1618            "backslashes are not portable path separators".to_owned(),
1619        ));
1620    }
1621    if path.contains('\0') {
1622        return Err(invalid("path must not contain NUL bytes".to_owned()));
1623    }
1624    if has_windows_drive_prefix(path) {
1625        return Err(invalid(
1626            "path must not contain a platform root prefix".to_owned(),
1627        ));
1628    }
1629    let vpath = VirtualPath::new(path).map_err(|err| invalid(err.to_string()))?;
1630    let canonical = vpath.get_without_slash();
1631    if canonical.is_empty() {
1632        return Err(invalid("path must name a file".to_owned()));
1633    }
1634    if has_windows_drive_prefix(canonical) {
1635        return Err(invalid(
1636            "path must not contain a platform root prefix".to_owned(),
1637        ));
1638    }
1639    Ok(CanonicalPath(canonical.to_owned()))
1640}
1641
1642fn canonical_archive_name(path: &str) -> Result<String, PackReadError> {
1643    let prefix_normalized_path = strip_current_directory_prefix(path);
1644    if path.is_empty()
1645        || path.starts_with('/')
1646        || path.starts_with('\\')
1647        || path.contains('\\')
1648        || path.contains('\0')
1649        || has_windows_drive_prefix(prefix_normalized_path)
1650    {
1651        return Err(PackReadError::UnsafeEntry(path.to_owned()));
1652    }
1653    let canonical = VirtualPath::new(path)
1654        .map_err(|_| PackReadError::UnsafeEntry(path.to_owned()))?
1655        .get_without_slash()
1656        .to_owned();
1657    if has_windows_drive_prefix(&canonical) {
1658        return Err(PackReadError::UnsafeEntry(path.to_owned()));
1659    }
1660    Ok(canonical)
1661}
1662
1663fn validate_package_spec(spec: &PackageSpec) -> Result<(), PackInvariantError> {
1664    let serialized = spec.to_string();
1665    let parsed = PackageSpec::from_str(&serialized).map_err(|message| {
1666        PackInvariantError::InvalidPackageSpec {
1667            spec: serialized.clone(),
1668            message: message.to_string(),
1669        }
1670    })?;
1671    if parsed != *spec {
1672        return Err(PackInvariantError::InvalidPackageSpec {
1673            spec: serialized,
1674            message: "package specification does not round-trip canonically".to_owned(),
1675        });
1676    }
1677    Ok(())
1678}
1679
1680fn validate_archive_entry_name(
1681    role: PackPathRole,
1682    path: &CanonicalPath,
1683    archive_name_len: usize,
1684) -> Result<(), PackInvariantError> {
1685    if archive_name_len > MAX_ZIP_ENTRY_NAME_LEN {
1686        return Err(PackInvariantError::ArchiveEntryNameTooLong {
1687            role,
1688            path: path.to_string(),
1689        });
1690    }
1691    Ok(())
1692}
1693
1694fn has_windows_drive_prefix(path: &str) -> bool {
1695    let bytes = path.as_bytes();
1696    bytes.len() >= 2 && bytes[0].is_ascii_alphabetic() && bytes[1] == b':'
1697}
1698
1699fn strip_current_directory_prefix(mut path: &str) -> &str {
1700    while let Some(rest) = path.strip_prefix("./") {
1701        path = rest;
1702    }
1703    path
1704}
1705
1706fn find_path_tree_conflict(
1707    mut paths: Vec<(CanonicalPath, PackPathRole)>,
1708) -> Option<PathTreeConflict> {
1709    paths.sort_by(|(left, _), (right, _)| left.cmp(right));
1710    for (ancestor, ancestor_role) in &paths {
1711        let prefix = format!("{ancestor}/");
1712        let candidate = paths.partition_point(|(path, _)| path.as_str() < prefix.as_str());
1713        if let Some((descendant, descendant_role)) = paths.get(candidate)
1714            && descendant.as_str().starts_with(&prefix)
1715        {
1716            return Some(PathTreeConflict {
1717                ancestor: ancestor.clone(),
1718                ancestor_role: *ancestor_role,
1719                descendant: descendant.clone(),
1720                descendant_role: *descendant_role,
1721            });
1722        }
1723    }
1724    None
1725}
1726
1727fn validate_project_declarations(
1728    project_files: impl IntoIterator<Item = CanonicalPath>,
1729) -> Result<(), PackInvariantError> {
1730    let project_paths = project_files
1731        .into_iter()
1732        .map(|path| (path, PackPathRole::ProjectFile))
1733        .collect();
1734    if let Some(conflict) = find_path_tree_conflict(project_paths) {
1735        return Err(PackInvariantError::PathTreeConflict {
1736            ancestor: conflict.ancestor.to_string(),
1737            ancestor_role: conflict.ancestor_role,
1738            descendant: conflict.descendant.to_string(),
1739            descendant_role: conflict.descendant_role,
1740        });
1741    }
1742    Ok(())
1743}
1744
1745fn reserved_font_path_role(path: &CanonicalPath) -> Option<PackPathRole> {
1746    if is_same_or_descendant(path.as_str(), MANIFEST_PATH) {
1747        Some(PackPathRole::PackManifest)
1748    } else if is_same_or_descendant(path.as_str(), PROJECT_PREFIX.trim_end_matches('/')) {
1749        Some(PackPathRole::ProjectFile)
1750    } else if is_same_or_descendant(path.as_str(), PACKAGES_PREFIX.trim_end_matches('/')) {
1751        Some(PackPathRole::PackageFile)
1752    } else {
1753        None
1754    }
1755}
1756
1757fn is_same_or_descendant(path: &str, ancestor: &str) -> bool {
1758    path == ancestor
1759        || path
1760            .strip_prefix(ancestor)
1761            .is_some_and(|suffix| suffix.starts_with('/'))
1762}
1763
1764fn register_archive_identity(
1765    entries: &mut BTreeMap<String, Vec<u8>>,
1766    canonical: String,
1767    raw_name: &[u8],
1768) -> Result<(), PackInvariantError> {
1769    if let Some(first_entry) = entries.get(&canonical) {
1770        if first_entry == raw_name {
1771            return Ok(());
1772        }
1773        return Err(PackInvariantError::CanonicalArchiveEntryCollision {
1774            canonical,
1775            first_entry: display_archive_name(first_entry),
1776            second_entry: display_archive_name(raw_name),
1777        });
1778    }
1779    entries.insert(canonical, raw_name.to_owned());
1780    Ok(())
1781}
1782
1783fn display_archive_name(raw_name: &[u8]) -> String {
1784    String::from_utf8(raw_name.to_owned()).unwrap_or_else(|_| {
1785        raw_name
1786            .iter()
1787            .flat_map(|byte| std::ascii::escape_default(*byte).map(char::from))
1788            .collect()
1789    })
1790}
1791
1792/// A failure while building a pack in memory.
1793#[derive(Debug, thiserror::Error)]
1794pub enum PackBuildError {
1795    /// Builder-provided font bytes do not contain the requested face.
1796    #[error("font input does not contain a valid face at index {index}")]
1797    InvalidFontInput { index: u32 },
1798    #[error(transparent)]
1799    Invariant(#[from] PackInvariantError),
1800}
1801
1802/// A violation of the invariants shared by every [`Pack`] construction path.
1803#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
1804pub enum PackInvariantError {
1805    /// A path cannot identify a canonical file for its declared role.
1806    #[error("invalid {role} path `{path}`: {message}")]
1807    InvalidPath {
1808        role: PackPathRole,
1809        path: String,
1810        message: String,
1811    },
1812    /// A package value cannot be represented as a canonical package specification.
1813    #[error("invalid package spec `{spec}`: {message}")]
1814    InvalidPackageSpec { spec: String, message: String },
1815    /// A Package Requirement has a malformed or unsupported tree identity.
1816    #[error("package requirement `{spec}` has an invalid Complete Package Tree identity")]
1817    InvalidPackageRequirement { spec: String },
1818    /// Embedded package bytes disagree with their declared tree identity.
1819    #[error("embedded package `{0}` does not match its declared Complete Package Tree identity")]
1820    MismatchedEmbeddedPackageIdentity(String),
1821    /// A contained path cannot fit in ZIP's filename field after adding its role prefix.
1822    #[error("the {role} path `{path}` exceeds ZIP's filename length limit")]
1823    ArchiveEntryNameTooLong { role: PackPathRole, path: String },
1824    /// Distinct archive entries identify one canonical contained file.
1825    #[error("archive entries `{first_entry}` and `{second_entry}` both identify `{canonical}`")]
1826    CanonicalArchiveEntryCollision {
1827        canonical: String,
1828        first_entry: String,
1829        second_entry: String,
1830    },
1831    /// One file path is an ancestor of another file path in the same tree.
1832    #[error(
1833        "{ancestor_role} path `{ancestor}` conflicts with {descendant_role} descendant `{descendant}`"
1834    )]
1835    PathTreeConflict {
1836        ancestor: String,
1837        ancestor_role: PackPathRole,
1838        descendant: String,
1839        descendant_role: PackPathRole,
1840    },
1841    /// One package file path is an ancestor of another file path in that package.
1842    #[error(
1843        "package `{package}` {ancestor_role} path `{ancestor}` conflicts with {descendant_role} descendant `{descendant}`"
1844    )]
1845    PackagePathTreeConflict {
1846        package: String,
1847        ancestor: String,
1848        ancestor_role: PackPathRole,
1849        descendant: String,
1850        descendant_role: PackPathRole,
1851    },
1852    /// A package was declared both vendored and unvendored.
1853    #[error("package `{0}` cannot be both vendored and unvendored")]
1854    PackageRoleConflict(String),
1855    /// Package bytes exist without a matching vendored declaration.
1856    #[error("package `{0}` has contained data but is not declared vendored")]
1857    UndeclaredPackageData(String),
1858    /// A vendored package declaration has no contained bytes.
1859    #[error("vendored package `{0}` has no contained data")]
1860    MissingVendoredPackageData(String),
1861    /// A font declaration uses an archive path reserved for another role.
1862    #[error("font data path `{path}` conflicts with the {conflicting_role} archive role")]
1863    ReservedFontPath {
1864        path: String,
1865        conflicting_role: PackPathRole,
1866    },
1867    /// A font declaration has no contained bytes.
1868    #[error("font data `{0}` is missing")]
1869    MissingFontData(String),
1870    /// Contained font bytes do not contain the declared face.
1871    #[error("font data `{path}` does not contain a valid face at index {index}")]
1872    InvalidFontData { path: String, index: u32 },
1873    /// An external font declaration has no valid exact container identity.
1874    #[error("external font `{path}` has an invalid container identity or length")]
1875    InvalidExternalFontIdentity { path: String },
1876    /// Embedded font bytes disagree with their declared exact identity.
1877    #[error("embedded font `{path}` does not match its declared container identity")]
1878    MismatchedEmbeddedFontIdentity { path: String },
1879    /// Faces from one exact container disagree about its length or fulfillment role.
1880    #[error("font `{path}` conflicts with another declaration for the same container")]
1881    InconsistentFontContainer { path: String },
1882    /// An externally fulfilled declaration also has bytes in the Pack.
1883    #[error("external font `{path}` cannot also have contained data")]
1884    ExternalFontHasContainedData { path: String },
1885    /// The same contained font face was declared more than once.
1886    #[error("font `{path}` declares face index {index} more than once")]
1887    DuplicateFontFace { path: String, index: u32 },
1888    /// The declared entrypoint is not present among the packed project files.
1889    #[error("entrypoint `{0}` is not a contained project file")]
1890    MissingEntrypoint(String),
1891}
1892
1893/// The role a path plays in a Pack invariant.
1894#[derive(Debug, Clone, Copy, Eq, PartialEq)]
1895pub enum PackPathRole {
1896    PackManifest,
1897    Entrypoint,
1898    ProjectFile,
1899    PackageFile,
1900    FontData,
1901}
1902
1903impl std::fmt::Display for PackPathRole {
1904    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1905        formatter.write_str(match self {
1906            Self::PackManifest => "Pack Manifest",
1907            Self::Entrypoint => "entrypoint",
1908            Self::ProjectFile => "project file",
1909            Self::PackageFile => "package file",
1910            Self::FontData => "font data",
1911        })
1912    }
1913}