Skip to main content

typst_pack/compile/
fulfillment.rs

1//! Validated external compilation fulfillment inputs.
2
3use std::collections::{BTreeMap, BTreeSet};
4
5use typst::syntax::package::PackageSpec;
6
7use crate::{CanonicalIdentity, FontContainer, Pack, PackageTree};
8
9#[derive(Debug, Clone)]
10pub struct PackageTreeFulfillment {
11    pub(super) spec: PackageSpec,
12    pub(super) tree: PackageTree,
13    pub(super) provenance: Option<String>,
14    pub(super) cache_hit: bool,
15}
16
17impl PackageTreeFulfillment {
18    pub fn new(spec: PackageSpec, tree: PackageTree) -> Self {
19        Self {
20            spec,
21            tree,
22            provenance: None,
23            cache_hit: false,
24        }
25    }
26
27    pub fn spec(&self) -> &PackageSpec {
28        &self.spec
29    }
30
31    pub fn tree(&self) -> &PackageTree {
32        &self.tree
33    }
34
35    pub fn provenance(mut self, provenance: impl Into<String>) -> Self {
36        self.provenance = Some(provenance.into());
37        self
38    }
39
40    pub fn cache_hit(mut self, cache_hit: bool) -> Self {
41        self.cache_hit = cache_hit;
42        self
43    }
44}
45
46#[derive(Debug, Clone)]
47pub struct FontContainerFulfillment {
48    pub(super) expected_identity: CanonicalIdentity,
49    pub(super) container: FontContainer,
50    pub(super) provenance: Option<String>,
51    pub(super) licensing: Option<String>,
52}
53
54impl FontContainerFulfillment {
55    pub fn new(expected_identity: CanonicalIdentity, container: FontContainer) -> Self {
56        Self {
57            expected_identity,
58            container,
59            provenance: None,
60            licensing: None,
61        }
62    }
63
64    pub fn expected_identity(&self) -> CanonicalIdentity {
65        self.expected_identity
66    }
67
68    pub fn container(&self) -> &FontContainer {
69        &self.container
70    }
71
72    pub fn provenance(mut self, provenance: impl Into<String>) -> Self {
73        self.provenance = Some(provenance.into());
74        self
75    }
76
77    pub fn licensing(mut self, licensing: impl Into<String>) -> Self {
78        self.licensing = Some(licensing.into());
79        self
80    }
81}
82
83/// Resolves the Pack's external Font Requirements from exact source-container bytes.
84///
85/// Each matching container produces at most one fulfillment. Unrelated and
86/// duplicate sources are ignored, and requirements with no matching source
87/// remain absent for compilation's exact fulfillment verification to report.
88pub fn resolve_external_font_requirements<I, S>(
89    pack: &Pack,
90    sources: I,
91) -> Result<Vec<FontContainerFulfillment>, crate::FontContainerError>
92where
93    I: IntoIterator<Item = S>,
94    S: AsRef<[u8]>,
95{
96    let required = pack
97        .font_requirements()
98        .iter()
99        .filter(|requirement| !requirement.is_embedded())
100        .map(|requirement| requirement.container_identity())
101        .collect::<BTreeSet<_>>();
102    let mut fulfillments = BTreeMap::new();
103    for source in sources {
104        let data = source.as_ref();
105        let identity = CanonicalIdentity::for_font_container_bytes(data);
106        if required.contains(&identity) && !fulfillments.contains_key(&identity) {
107            let container = FontContainer::new(data.to_vec())?;
108            fulfillments.insert(identity, FontContainerFulfillment::new(identity, container));
109        }
110    }
111    Ok(fulfillments.into_values().collect())
112}
113
114#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
115#[non_exhaustive]
116pub enum CompilationFulfillmentSetIssue {
117    #[error("package specification {spec} is fulfilled more than once")]
118    DuplicatePackageSpecification { spec: PackageSpec },
119    #[error("font container {identity} is fulfilled more than once")]
120    DuplicateFontContainerIdentity { identity: CanonicalIdentity },
121}
122
123#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
124#[error("Compilation Fulfillment Set construction failed with {} issue(s)", .issues.len())]
125pub struct CompilationFulfillmentSetError {
126    issues: Vec<CompilationFulfillmentSetIssue>,
127}
128
129impl CompilationFulfillmentSetError {
130    pub fn issues(&self) -> &[CompilationFulfillmentSetIssue] {
131        &self.issues
132    }
133}
134
135#[derive(Debug, Clone, Default)]
136pub struct CompilationFulfillmentSet {
137    pub(super) packages: BTreeMap<String, PackageTreeFulfillment>,
138    pub(super) fonts: BTreeMap<CanonicalIdentity, FontContainerFulfillment>,
139}
140
141impl CompilationFulfillmentSet {
142    pub fn new(
143        packages: impl IntoIterator<Item = PackageTreeFulfillment>,
144        fonts: impl IntoIterator<Item = FontContainerFulfillment>,
145    ) -> Result<Self, CompilationFulfillmentSetError> {
146        let packages = packages.into_iter().collect::<Vec<_>>();
147        let fonts = fonts.into_iter().collect::<Vec<_>>();
148        let mut package_keys = BTreeSet::new();
149        let mut duplicate_packages = BTreeSet::new();
150        for fulfillment in &packages {
151            let key = fulfillment.spec.to_string();
152            if !package_keys.insert(key.clone()) {
153                duplicate_packages.insert(key);
154            }
155        }
156        let mut font_keys = BTreeSet::new();
157        let mut duplicate_fonts = BTreeSet::new();
158        for fulfillment in &fonts {
159            if !font_keys.insert(fulfillment.expected_identity) {
160                duplicate_fonts.insert(fulfillment.expected_identity);
161            }
162        }
163        let mut issues = duplicate_packages
164            .into_iter()
165            .map(|key| {
166                let spec = packages
167                    .iter()
168                    .find(|fulfillment| fulfillment.spec.to_string() == key)
169                    .expect("duplicate key came from a package fulfillment")
170                    .spec
171                    .clone();
172                CompilationFulfillmentSetIssue::DuplicatePackageSpecification { spec }
173            })
174            .collect::<Vec<_>>();
175        issues.extend(duplicate_fonts.into_iter().map(|identity| {
176            CompilationFulfillmentSetIssue::DuplicateFontContainerIdentity { identity }
177        }));
178        if !issues.is_empty() {
179            return Err(CompilationFulfillmentSetError { issues });
180        }
181        Ok(Self {
182            packages: packages
183                .into_iter()
184                .map(|fulfillment| (fulfillment.spec.to_string(), fulfillment))
185                .collect(),
186            fonts: fonts
187                .into_iter()
188                .map(|fulfillment| (fulfillment.expected_identity, fulfillment))
189                .collect(),
190        })
191    }
192
193    pub fn empty() -> Self {
194        Self::default()
195    }
196
197    pub fn packages(&self) -> impl ExactSizeIterator<Item = &PackageTreeFulfillment> {
198        self.packages.values()
199    }
200
201    pub fn fonts(&self) -> impl ExactSizeIterator<Item = &FontContainerFulfillment> {
202        self.fonts.values()
203    }
204}
205
206/// Operational evidence retained for one exact package fulfillment.
207#[derive(Debug, Clone, PartialEq, Eq)]
208pub struct PackageFulfillmentReport {
209    pub(super) spec: PackageSpec,
210    pub(super) required_tree_identity: Option<CanonicalIdentity>,
211    pub(super) supplied_tree_identity: Option<CanonicalIdentity>,
212    pub(super) declared: bool,
213    pub(super) embedded: bool,
214    pub(super) provenance: Option<String>,
215    pub(super) cache_hit: bool,
216}
217
218impl PackageFulfillmentReport {
219    pub fn spec(&self) -> &PackageSpec {
220        &self.spec
221    }
222
223    pub fn required_tree_identity(&self) -> Option<CanonicalIdentity> {
224        self.required_tree_identity
225    }
226
227    pub fn supplied_tree_identity(&self) -> Option<CanonicalIdentity> {
228        self.supplied_tree_identity
229    }
230
231    pub fn declared(&self) -> bool {
232        self.declared
233    }
234
235    pub fn embedded(&self) -> bool {
236        self.embedded
237    }
238
239    pub fn provenance(&self) -> Option<&str> {
240        self.provenance.as_deref()
241    }
242
243    pub fn cache_hit(&self) -> bool {
244        self.cache_hit
245    }
246}
247
248/// Operational evidence retained for one exact font fulfillment.
249#[derive(Debug, Clone, PartialEq, Eq)]
250pub struct FontFulfillmentReport {
251    pub(super) container_identity: CanonicalIdentity,
252    pub(super) supplied_container_identity: Option<CanonicalIdentity>,
253    pub(super) declared: bool,
254    pub(super) embedded: bool,
255    pub(super) provenance: Option<String>,
256    pub(super) licensing: Option<String>,
257}
258
259impl FontFulfillmentReport {
260    pub fn container_identity(&self) -> CanonicalIdentity {
261        self.container_identity
262    }
263
264    pub fn supplied_container_identity(&self) -> Option<CanonicalIdentity> {
265        self.supplied_container_identity
266    }
267
268    pub fn declared(&self) -> bool {
269        self.declared
270    }
271
272    pub fn embedded(&self) -> bool {
273        self.embedded
274    }
275
276    pub fn provenance(&self) -> Option<&str> {
277        self.provenance.as_deref()
278    }
279
280    pub fn licensing(&self) -> Option<&str> {
281        self.licensing.as_deref()
282    }
283}
284
285/// Operational dependency evidence surrounding one official semantic result.
286#[derive(Debug, Clone, Default, PartialEq, Eq)]
287pub struct CompilationFulfillmentReport {
288    pub(super) packages: Vec<PackageFulfillmentReport>,
289    pub(super) fonts: Vec<FontFulfillmentReport>,
290}
291
292impl CompilationFulfillmentReport {
293    pub fn packages(&self) -> &[PackageFulfillmentReport] {
294        &self.packages
295    }
296
297    pub fn fonts(&self) -> &[FontFulfillmentReport] {
298        &self.fonts
299    }
300}
301
302/// One exact-set deviation detected before private World materialization.
303#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
304#[non_exhaustive]
305pub enum CompilationFulfillmentIssue {
306    #[error("required package {spec} was not supplied")]
307    MissingExternalPackage { spec: PackageSpec },
308    #[error("package fulfillment for undeclared specification {spec} supplied {actual}")]
309    UndeclaredPackage {
310        spec: PackageSpec,
311        actual: CanonicalIdentity,
312    },
313    #[error("embedded package {spec} was unexpectedly fulfilled externally")]
314    UnexpectedEmbeddedPackage { spec: PackageSpec },
315    #[error(
316        "package fulfillment for {spec} supplied {actual} ({actual_file_count} files, \
317         {actual_byte_length} bytes), expected {expected} ({expected_file_count} files, \
318         {expected_byte_length} bytes)"
319    )]
320    MismatchedPackageTree {
321        spec: PackageSpec,
322        expected: CanonicalIdentity,
323        actual: CanonicalIdentity,
324        expected_file_count: u64,
325        actual_file_count: u64,
326        expected_byte_length: u64,
327        actual_byte_length: u64,
328    },
329    #[error("required font container {identity} was not supplied")]
330    MissingExternalFont { identity: CanonicalIdentity },
331    #[error("font container fulfillment for undeclared identity {identity} supplied {actual}")]
332    UndeclaredFont {
333        identity: CanonicalIdentity,
334        actual: CanonicalIdentity,
335    },
336    #[error("embedded font container {identity} was unexpectedly fulfilled externally")]
337    UnexpectedEmbeddedFont { identity: CanonicalIdentity },
338    #[error(
339        "font container fulfillment for {expected} supplied {actual} \
340         ({actual_length} bytes, expected {expected_length} bytes)"
341    )]
342    MismatchedFontContainer {
343        expected: CanonicalIdentity,
344        actual: CanonicalIdentity,
345        expected_length: u64,
346        actual_length: u64,
347    },
348    #[error("font container {identity} has no required face at index {index}")]
349    MissingFontFace {
350        identity: CanonicalIdentity,
351        index: u32,
352    },
353}
354
355/// Complete canonical evidence that a fulfillment set is not exact.
356///
357/// The [`Display`](std::fmt::Display) form summarizes the deviation; callers
358/// that present failures to a person should enumerate [`Self::issues`], because
359/// only the individual issues name the packages and font containers involved.
360#[derive(Debug, Clone, PartialEq, Eq)]
361pub struct InvalidCompilationFulfillmentSet {
362    pub(super) issues: Vec<CompilationFulfillmentIssue>,
363}
364
365impl InvalidCompilationFulfillmentSet {
366    pub fn issues(&self) -> &[CompilationFulfillmentIssue] {
367        &self.issues
368    }
369}
370
371impl std::fmt::Display for InvalidCompilationFulfillmentSet {
372    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
373        if let [issue] = self.issues.as_slice() {
374            return write!(formatter, "{issue}");
375        }
376        write!(
377            formatter,
378            "the pack's dependencies were not fulfilled ({} issues)",
379            self.issues.len()
380        )
381    }
382}
383
384impl std::error::Error for InvalidCompilationFulfillmentSet {}
385
386pub(super) fn verify_compilation_fulfillment_set(
387    pack: &Pack,
388    package_fulfillments: &BTreeMap<String, PackageTreeFulfillment>,
389    font_fulfillments: &BTreeMap<CanonicalIdentity, FontContainerFulfillment>,
390) -> Vec<CompilationFulfillmentIssue> {
391    let package_requirements = pack
392        .package_requirements()
393        .iter()
394        .map(|requirement| (requirement.spec().to_string(), requirement))
395        .collect::<BTreeMap<_, _>>();
396    let package_keys = package_requirements
397        .keys()
398        .chain(package_fulfillments.keys())
399        .cloned()
400        .collect::<BTreeSet<_>>();
401    let mut issues = Vec::new();
402    for key in package_keys {
403        match (
404            package_requirements.get(&key),
405            package_fulfillments.get(&key),
406        ) {
407            (Some(requirement), None) if !requirement.is_embedded() => {
408                issues.push(CompilationFulfillmentIssue::MissingExternalPackage {
409                    spec: requirement.spec().clone(),
410                });
411            }
412            (None, Some(fulfillment)) => {
413                issues.push(CompilationFulfillmentIssue::UndeclaredPackage {
414                    spec: fulfillment.spec.clone(),
415                    actual: fulfillment.tree.identity(),
416                });
417            }
418            (Some(requirement), Some(fulfillment)) => {
419                if requirement.is_embedded() {
420                    issues.push(CompilationFulfillmentIssue::UnexpectedEmbeddedPackage {
421                        spec: requirement.spec().clone(),
422                    });
423                }
424                if fulfillment.tree.identity() != requirement.tree_identity()
425                    || fulfillment.tree.file_count() != requirement.file_count()
426                    || fulfillment.tree.byte_length() != requirement.byte_length()
427                {
428                    issues.push(CompilationFulfillmentIssue::MismatchedPackageTree {
429                        spec: requirement.spec().clone(),
430                        expected: requirement.tree_identity(),
431                        actual: fulfillment.tree.identity(),
432                        expected_file_count: requirement.file_count(),
433                        actual_file_count: fulfillment.tree.file_count(),
434                        expected_byte_length: requirement.byte_length(),
435                        actual_byte_length: fulfillment.tree.byte_length(),
436                    });
437                }
438            }
439            _ => {}
440        }
441    }
442
443    let font_requirements = pack
444        .font_requirements()
445        .iter()
446        .map(|requirement| (requirement.container_identity(), requirement))
447        .collect::<BTreeMap<_, _>>();
448    let font_keys = font_requirements
449        .keys()
450        .chain(font_fulfillments.keys())
451        .copied()
452        .collect::<BTreeSet<_>>();
453    for identity in font_keys {
454        match (
455            font_requirements.get(&identity),
456            font_fulfillments.get(&identity),
457        ) {
458            (Some(requirement), None) if !requirement.is_embedded() => {
459                issues.push(CompilationFulfillmentIssue::MissingExternalFont { identity });
460            }
461            (None, Some(fulfillment)) => {
462                issues.push(CompilationFulfillmentIssue::UndeclaredFont {
463                    identity,
464                    actual: fulfillment.container.identity(),
465                });
466            }
467            (Some(requirement), Some(fulfillment)) => {
468                if requirement.is_embedded() {
469                    issues.push(CompilationFulfillmentIssue::UnexpectedEmbeddedFont { identity });
470                }
471                let actual = fulfillment.container.identity();
472                let actual_length = fulfillment.container.data().len() as u64;
473                if actual != identity || actual_length != requirement.container_length() {
474                    issues.push(CompilationFulfillmentIssue::MismatchedFontContainer {
475                        expected: identity,
476                        actual,
477                        expected_length: requirement.container_length(),
478                        actual_length,
479                    });
480                }
481                for index in requirement
482                    .face_indices()
483                    .iter()
484                    .copied()
485                    .collect::<BTreeSet<_>>()
486                {
487                    if !fulfillment
488                        .container
489                        .faces()
490                        .iter()
491                        .any(|face| face.identity().index() == index)
492                    {
493                        issues
494                            .push(CompilationFulfillmentIssue::MissingFontFace { identity, index });
495                    }
496                }
497            }
498            _ => {}
499        }
500    }
501    issues
502}