1use 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
83pub 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#[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#[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#[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#[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#[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}