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