1use 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
19pub 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#[derive(Debug, Clone)]
36pub struct Pack {
37 manifest: PackManifest,
38 files: BTreeMap<CanonicalPath, Bytes>,
39 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#[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
81pub(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#[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#[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#[derive(Debug, Clone)]
188pub struct PackFont {
189 entry: FontManifest,
191 data: Bytes,
193 font: Font,
194}
195
196#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
198pub struct FontContainerIdentity(u128);
199
200impl FontContainerIdentity {
201 pub fn from_bytes(data: &[u8]) -> Self {
203 Self(typst::utils::hash128(&data))
204 }
205
206 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#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
234pub struct FontFaceIdentity {
235 container: FontContainerIdentity,
236 index: u32,
237}
238
239impl FontFaceIdentity {
240 pub fn container(self) -> FontContainerIdentity {
242 self.container
243 }
244
245 pub fn index(self) -> u32 {
247 self.index
248 }
249}
250
251#[derive(Debug, Clone, PartialEq, Eq)]
253pub struct PackFontCatalogFace {
254 identity: FontFaceIdentity,
255 embedded: bool,
256}
257
258impl PackFontCatalogFace {
259 pub fn identity(&self) -> FontFaceIdentity {
261 self.identity
262 }
263
264 pub fn is_embedded(&self) -> bool {
266 self.embedded
267 }
268}
269
270#[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 pub fn manifest(&self) -> &FontManifest {
300 &self.entry
301 }
302
303 pub fn data(&self) -> &Bytes {
305 &self.data
306 }
307
308 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 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 pub fn manifest(&self) -> &PackManifest {
634 &self.manifest
635 }
636
637 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 pub fn entrypoint(&self) -> &str {
678 self.manifest.project().entrypoint()
679 }
680
681 pub fn files(&self) -> impl Iterator<Item = (&str, &Bytes)> {
683 self.files.iter().map(|(path, data)| (path.as_str(), data))
684 }
685
686 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 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 pub fn package_file(&self, spec: &PackageSpec, path: &str) -> Option<&Bytes> {
714 self.packages.get(&spec.to_string())?.files.get(path)
715 }
716
717 pub fn has_package(&self, spec: &PackageSpec) -> bool {
719 self.packages.contains_key(&spec.to_string())
720 }
721
722 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 pub fn fonts(&self) -> &[PackFont] {
807 &self.fonts
808 }
809
810 pub fn font_catalog(&self) -> &[PackFontCatalogFace] {
812 &self.font_catalog
813 }
814
815 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 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 pub fn from_bytes(bytes: impl Into<Vec<u8>>) -> Result<Self, PackReadError> {
1093 Self::read(Cursor::new(bytes.into()))
1094 }
1095
1096 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 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#[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#[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#[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 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
1316fn 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#[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#[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#[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 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 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 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 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 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 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 pub fn metadata(mut self, metadata: PackMetadata) -> Self {
1510 self.metadata = Some(metadata);
1511 self
1512 }
1513
1514 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 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#[derive(Debug, thiserror::Error)]
1794pub enum PackBuildError {
1795 #[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#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
1804pub enum PackInvariantError {
1805 #[error("invalid {role} path `{path}`: {message}")]
1807 InvalidPath {
1808 role: PackPathRole,
1809 path: String,
1810 message: String,
1811 },
1812 #[error("invalid package spec `{spec}`: {message}")]
1814 InvalidPackageSpec { spec: String, message: String },
1815 #[error("package requirement `{spec}` has an invalid Complete Package Tree identity")]
1817 InvalidPackageRequirement { spec: String },
1818 #[error("embedded package `{0}` does not match its declared Complete Package Tree identity")]
1820 MismatchedEmbeddedPackageIdentity(String),
1821 #[error("the {role} path `{path}` exceeds ZIP's filename length limit")]
1823 ArchiveEntryNameTooLong { role: PackPathRole, path: String },
1824 #[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 #[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 #[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 #[error("package `{0}` cannot be both vendored and unvendored")]
1854 PackageRoleConflict(String),
1855 #[error("package `{0}` has contained data but is not declared vendored")]
1857 UndeclaredPackageData(String),
1858 #[error("vendored package `{0}` has no contained data")]
1860 MissingVendoredPackageData(String),
1861 #[error("font data path `{path}` conflicts with the {conflicting_role} archive role")]
1863 ReservedFontPath {
1864 path: String,
1865 conflicting_role: PackPathRole,
1866 },
1867 #[error("font data `{0}` is missing")]
1869 MissingFontData(String),
1870 #[error("font data `{path}` does not contain a valid face at index {index}")]
1872 InvalidFontData { path: String, index: u32 },
1873 #[error("external font `{path}` has an invalid container identity or length")]
1875 InvalidExternalFontIdentity { path: String },
1876 #[error("embedded font `{path}` does not match its declared container identity")]
1878 MismatchedEmbeddedFontIdentity { path: String },
1879 #[error("font `{path}` conflicts with another declaration for the same container")]
1881 InconsistentFontContainer { path: String },
1882 #[error("external font `{path}` cannot also have contained data")]
1884 ExternalFontHasContainedData { path: String },
1885 #[error("font `{path}` declares face index {index} more than once")]
1887 DuplicateFontFace { path: String, index: u32 },
1888 #[error("entrypoint `{0}` is not a contained project file")]
1890 MissingEntrypoint(String),
1891}
1892
1893#[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}