1use std::collections::{BTreeMap, BTreeSet};
4
5#[cfg(feature = "package-reading")]
6use typst::foundations::Bytes;
7use typst::syntax::package::{PackageSpec, PackageVersion};
8
9use crate::paths::{canonical_relative_path, path_tree_conflicts};
10use crate::payload::SharedBytes;
11use crate::{CanonicalIdentity, CanonicalIdentityRole};
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
16pub enum PackageDisposition {
17 Embedded,
19 External,
21}
22
23impl PackageDisposition {
24 pub fn is_embedded(self) -> bool {
26 matches!(self, Self::Embedded)
27 }
28}
29
30#[derive(Clone, Debug, PartialEq, Eq)]
32pub struct PackageTree {
33 files: BTreeMap<String, SharedBytes>,
34 identity: CanonicalIdentity,
35 file_count: u64,
36 byte_length: u64,
37}
38
39impl PackageTree {
40 pub fn from_owned_entries(
42 entries: impl IntoIterator<Item = (impl AsRef<str>, Vec<u8>)>,
43 ) -> Result<Self, PackageTreeError> {
44 Self::from_shared_entries(
45 entries
46 .into_iter()
47 .map(|(path, data)| (path.as_ref().to_owned(), SharedBytes::new(data)))
48 .collect(),
49 )
50 }
51
52 #[cfg(feature = "package-reading")]
53 pub(crate) fn from_typst_entries(
54 entries: Vec<(String, Bytes)>,
55 ) -> Result<Self, PackageTreeError> {
56 Self::from_shared_entries(
57 entries
58 .into_iter()
59 .map(|(path, data)| (path, SharedBytes::from_typst(data)))
60 .collect(),
61 )
62 }
63
64 fn from_shared_entries(entries: Vec<(String, SharedBytes)>) -> Result<Self, PackageTreeError> {
65 let canonical_paths =
66 preflight_package_tree_paths(entries.iter().map(|(path, _)| path), |_| true).map_err(
67 |error| match error {
68 PackageTreePathPreflightError::Invalid(source) => source,
69 PackageTreePathPreflightError::RetentionLimit => {
70 unreachable!("unlimited Package Tree construction preflight cannot exhaust")
71 }
72 },
73 )?;
74 let canonical_entries = canonical_paths
75 .into_iter()
76 .zip(entries.into_iter().map(|(_, data)| data))
77 .collect::<Vec<_>>();
78
79 let files = canonical_entries.into_iter().collect::<BTreeMap<_, _>>();
80 let (identity, file_count, byte_length) =
81 derive_package_tree_identity(files.iter().map(|(path, data)| (path.as_str(), data)));
82 Ok(Self {
83 files,
84 identity,
85 file_count,
86 byte_length,
87 })
88 }
89
90 pub fn copy_from_entries(
92 entries: impl IntoIterator<Item = (impl AsRef<str>, impl AsRef<[u8]>)>,
93 ) -> Result<Self, PackageTreeError> {
94 Self::from_owned_entries(
95 entries
96 .into_iter()
97 .map(|(path, data)| (path.as_ref().to_owned(), data.as_ref().to_vec())),
98 )
99 }
100
101 pub fn files(&self) -> impl Iterator<Item = (&str, &[u8])> {
103 self.files
104 .iter()
105 .map(|(path, data)| (path.as_str(), data.as_slice()))
106 }
107
108 pub fn file(&self, path: &str) -> Option<&[u8]> {
110 self.files.get(path).map(SharedBytes::as_slice)
111 }
112
113 pub fn identity(&self) -> CanonicalIdentity {
115 self.identity
116 }
117
118 pub fn file_count(&self) -> u64 {
120 self.file_count
121 }
122
123 pub fn byte_length(&self) -> u64 {
125 self.byte_length
126 }
127
128 pub(crate) fn shared_files(&self) -> impl Iterator<Item = (&str, &SharedBytes)> {
129 self.files.iter().map(|(path, data)| (path.as_str(), data))
130 }
131
132 pub(crate) fn shared_file(&self, path: &str) -> Option<&SharedBytes> {
133 self.files.get(path)
134 }
135
136 pub(crate) fn into_shared_files(self) -> BTreeMap<String, SharedBytes> {
137 self.files
138 }
139}
140
141pub(crate) fn derive_package_tree_identity<'a>(
142 files: impl IntoIterator<Item = (&'a str, &'a SharedBytes)>,
143) -> (CanonicalIdentity, u64, u64) {
144 let projection = files
145 .into_iter()
146 .map(|(path, data)| (path, data.len() as u64, typst::utils::hash128(data)))
147 .collect::<Vec<_>>();
148 let file_count = projection.len() as u64;
149 let byte_length = projection.iter().map(|(_, length, _)| length).sum();
150 (
151 CanonicalIdentity::from_digest(
152 CanonicalIdentityRole::PackageTree,
153 typst::utils::hash128(&(
154 "typst-pack-complete-package-tree-v1",
155 file_count,
156 byte_length,
157 projection,
158 )),
159 ),
160 file_count,
161 byte_length,
162 )
163}
164
165#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
167#[non_exhaustive]
168pub enum PackageTreeIssue {
169 #[error("package path {path:?} cannot be represented: {message:?}")]
171 InvalidPath { path: String, message: String },
172 #[error("package path {path:?} is supplied more than once")]
174 DuplicatePath { path: String },
175 #[error("package path {ancestor:?} is a file ancestor of {descendant:?}")]
177 PathTreeConflict {
178 ancestor: String,
179 descendant: String,
180 },
181}
182
183impl PackageTreeIssue {
184 fn sort_key(&self) -> (&str, u8, &str) {
185 match self {
186 Self::InvalidPath { path, .. } => (path, 0, ""),
187 Self::DuplicatePath { path } => (path, 1, ""),
188 Self::PathTreeConflict {
189 ancestor,
190 descendant,
191 } => (ancestor, 2, descendant),
192 }
193 }
194}
195
196#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
198#[error("package tree construction failed with {} issue(s)", .issues.len())]
199pub struct PackageTreeError {
200 issues: Vec<PackageTreeIssue>,
201}
202
203impl PackageTreeError {
204 pub fn issues(&self) -> &[PackageTreeIssue] {
206 &self.issues
207 }
208}
209
210pub(crate) enum PackageTreePathPreflightError {
211 Invalid(PackageTreeError),
212 RetentionLimit,
213}
214
215impl PackageTreePathPreflightError {
216 #[cfg(test)]
217 fn issues(&self) -> &[PackageTreeIssue] {
218 match self {
219 Self::Invalid(source) => source.issues(),
220 Self::RetentionLimit => &[],
221 }
222 }
223
224 #[cfg(test)]
225 fn is_retention_limit(&self) -> bool {
226 matches!(self, Self::RetentionLimit)
227 }
228}
229
230pub(crate) fn preflight_package_tree_paths(
236 paths: impl IntoIterator<Item = impl AsRef<str>>,
237 mut retain: impl FnMut(&[usize]) -> bool,
238) -> Result<Vec<String>, PackageTreePathPreflightError> {
239 let mut canonical_paths = Vec::new();
240 let mut issues = Vec::new();
241 for path in paths {
242 let path = path.as_ref();
243 match canonical_relative_path(path) {
244 Ok(canonical) => {
245 if !retain(&[canonical.as_str().len()]) {
246 return Err(PackageTreePathPreflightError::RetentionLimit);
247 }
248 canonical_paths.push(canonical);
249 }
250 Err(message) => {
251 if !retain(&[path.len()]) {
252 return Err(PackageTreePathPreflightError::RetentionLimit);
253 }
254 issues.push(PackageTreeIssue::InvalidPath {
255 path: path.to_owned(),
256 message: message.to_string(),
257 });
258 }
259 }
260 }
261
262 let mut ordered = canonical_paths
263 .iter()
264 .map(|path| path.as_str())
265 .collect::<Vec<_>>();
266 ordered.sort_unstable();
267 let mut last_duplicate = None;
268 for path in ordered
269 .windows(2)
270 .filter(|pair| pair[0] == pair[1])
271 .map(|pair| pair[0])
272 {
273 if last_duplicate == Some(path) {
274 continue;
275 }
276 last_duplicate = Some(path);
277 if !retain(&[path.len()]) {
278 return Err(PackageTreePathPreflightError::RetentionLimit);
279 }
280 issues.push(PackageTreeIssue::DuplicatePath {
281 path: path.to_owned(),
282 });
283 }
284
285 ordered.dedup();
286 for ancestor in &ordered {
287 if !retain(&[ancestor.len() + 1]) {
288 return Err(PackageTreePathPreflightError::RetentionLimit);
289 }
290 }
291 let conflicts = path_tree_conflicts(ordered.iter().map(|path| {
292 let canonical = canonical_paths
293 .iter()
294 .find(|canonical| canonical.as_str() == *path)
295 .expect("an ordered canonical path came from the preflight input");
296 (canonical, ())
297 }));
298 for conflict in conflicts {
299 if !retain(&[
300 conflict.ancestor.as_str().len(),
301 conflict.descendant.as_str().len(),
302 ]) {
303 return Err(PackageTreePathPreflightError::RetentionLimit);
304 }
305 issues.push(PackageTreeIssue::PathTreeConflict {
306 ancestor: conflict.ancestor.to_string(),
307 descendant: conflict.descendant.to_string(),
308 });
309 }
310
311 if issues.is_empty() {
312 Ok(canonical_paths
313 .into_iter()
314 .map(|path| path.into_string())
315 .collect())
316 } else {
317 issues.sort_by(|left, right| left.sort_key().cmp(&right.sort_key()));
318 Err(PackageTreePathPreflightError::Invalid(PackageTreeError {
319 issues,
320 }))
321 }
322}
323
324#[cfg(test)]
325mod path_preflight_tests {
326 use super::{PackageTreeIssue, preflight_package_tree_paths};
327
328 #[test]
329 fn path_preflight_is_the_authority_for_canonical_package_tree_shape() {
330 let paths = [
331 "dir/second.typ",
332 "same.typ",
333 "../escape.typ",
334 "./same.typ",
335 "dir",
336 ];
337 let error = preflight_package_tree_paths(paths, |_| true).unwrap_err();
338
339 assert_eq!(error.issues().len(), 3);
340 assert!(matches!(
341 &error.issues()[0],
342 PackageTreeIssue::InvalidPath { path, .. } if path == "../escape.typ"
343 ));
344 assert_eq!(
345 &error.issues()[1..],
346 &[
347 PackageTreeIssue::PathTreeConflict {
348 ancestor: "dir".to_owned(),
349 descendant: "dir/second.typ".to_owned(),
350 },
351 PackageTreeIssue::DuplicatePath {
352 path: "same.typ".to_owned(),
353 },
354 ]
355 );
356 }
357
358 #[test]
359 fn path_preflight_stops_before_retaining_unbudgeted_evidence() {
360 let mut retained = 0usize;
361 let error = preflight_package_tree_paths(["dir", "dir/file.typ"], |lengths| {
362 let requested = lengths.iter().sum::<usize>();
363 if retained + requested > 20 {
364 return false;
365 }
366 retained += requested;
367 true
368 })
369 .unwrap_err();
370
371 assert!(error.is_retention_limit());
372 assert_eq!(retained, 19);
373 }
374}
375
376#[derive(Clone, Debug, PartialEq, Eq)]
378pub struct PackageCatalogEntry {
379 spec: PackageSpec,
380 tree: PackageTree,
381 disposition: PackageDisposition,
382}
383
384impl PackageCatalogEntry {
385 pub fn spec(&self) -> &PackageSpec {
387 &self.spec
388 }
389
390 pub fn tree(&self) -> &PackageTree {
392 &self.tree
393 }
394
395 pub fn disposition(&self) -> PackageDisposition {
397 self.disposition
398 }
399}
400
401#[derive(Clone, Debug, Default, PartialEq, Eq)]
404pub struct PackageCatalog {
405 entries: BTreeMap<String, PackageCatalogEntry>,
406}
407
408impl PackageCatalog {
409 pub fn new() -> Self {
411 Self::default()
412 }
413
414 pub fn from_entries(
417 entries: impl IntoIterator<Item = (PackageSpec, PackageTree, PackageDisposition)>,
418 ) -> Result<Self, PackageCatalogError> {
419 let entries = entries.into_iter().collect::<Vec<_>>();
420 let mut seen = BTreeSet::new();
421 let mut duplicates = BTreeSet::new();
422 let mut issues = Vec::new();
423 for (spec, tree, _) in &entries {
424 let key = spec.to_string();
425 if !seen.insert(key.clone()) {
426 duplicates.insert(key);
427 }
428 issues.extend(verify_package_declaration(spec, tree));
429 }
430 for key in duplicates {
431 let spec = entries
432 .iter()
433 .find(|(spec, _, _)| spec.to_string() == key)
434 .expect("duplicate specification came from an entry")
435 .0
436 .clone();
437 issues.push(PackageCatalogIssue::DuplicateSpecification { spec });
438 }
439 issues.sort_by_key(PackageCatalogIssue::sort_key);
440 if !issues.is_empty() {
441 return Err(PackageCatalogError { issues });
442 }
443 Ok(Self {
444 entries: entries
445 .into_iter()
446 .map(|(spec, tree, disposition)| {
447 (
448 spec.to_string(),
449 PackageCatalogEntry {
450 spec,
451 tree,
452 disposition,
453 },
454 )
455 })
456 .collect(),
457 })
458 }
459
460 pub fn insert(
463 &mut self,
464 spec: PackageSpec,
465 tree: PackageTree,
466 disposition: PackageDisposition,
467 ) -> Result<(), PackageCatalogError> {
468 let key = spec.to_string();
469 let mut issues = Vec::new();
470 if self.entries.contains_key(&key) {
471 issues.push(PackageCatalogIssue::DuplicateSpecification { spec: spec.clone() });
472 }
473 issues.extend(verify_package_declaration(&spec, &tree));
474 issues.sort_by_key(PackageCatalogIssue::sort_key);
475 if !issues.is_empty() {
476 return Err(PackageCatalogError { issues });
477 }
478 self.entries.insert(
479 key,
480 PackageCatalogEntry {
481 spec,
482 tree,
483 disposition,
484 },
485 );
486 Ok(())
487 }
488
489 pub fn entries(&self) -> impl Iterator<Item = &PackageCatalogEntry> {
491 self.entries.values()
492 }
493
494 pub fn get(&self, spec: &PackageSpec) -> Option<&PackageCatalogEntry> {
496 self.entries.get(&spec.to_string())
497 }
498}
499
500const PACKAGE_DECLARATION_PATH: &str = "typst.toml";
501
502fn verify_package_declaration(spec: &PackageSpec, tree: &PackageTree) -> Vec<PackageCatalogIssue> {
503 let Some(data) = tree.file(PACKAGE_DECLARATION_PATH) else {
504 return vec![PackageCatalogIssue::MissingDeclaration { spec: spec.clone() }];
505 };
506 let Ok(text) = std::str::from_utf8(data) else {
507 return vec![PackageCatalogIssue::DeclarationNotUtf8 { spec: spec.clone() }];
508 };
509 let declaration = match toml::from_str::<SuppliedPackageDeclaration>(text) {
510 Ok(declaration) => declaration,
511 Err(error) => {
512 return vec![PackageCatalogIssue::MalformedDeclaration {
513 spec: spec.clone(),
514 message: error.message().to_owned(),
515 }];
516 }
517 };
518
519 let mut issues = Vec::new();
520 if declaration.package.name != spec.name.as_str() {
521 issues.push(PackageCatalogIssue::MismatchedName {
522 spec: spec.clone(),
523 declared: declaration.package.name,
524 });
525 }
526 if declaration.package.version != spec.version {
527 issues.push(PackageCatalogIssue::MismatchedVersion {
528 spec: spec.clone(),
529 declared: declaration.package.version,
530 });
531 }
532 issues
533}
534
535#[derive(serde::Deserialize)]
536struct SuppliedPackageDeclaration {
537 package: DeclaredPackage,
538}
539
540#[derive(serde::Deserialize)]
541struct DeclaredPackage {
542 name: String,
543 version: PackageVersion,
544}
545
546#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
548#[non_exhaustive]
549pub enum PackageCatalogIssue {
550 #[error("package specification {spec} is supplied more than once")]
552 DuplicateSpecification { spec: PackageSpec },
553 #[error("the tree supplied for package {spec} holds no `typst.toml`")]
555 MissingDeclaration { spec: PackageSpec },
556 #[error("the tree supplied for package {spec} has a non-UTF-8 `typst.toml`")]
558 DeclarationNotUtf8 { spec: PackageSpec },
559 #[error("the tree supplied for package {spec} has malformed `typst.toml`: {message:?}")]
561 MalformedDeclaration { spec: PackageSpec, message: String },
562 #[error("the tree supplied for package {spec} declares the name {declared:?}")]
564 MismatchedName { spec: PackageSpec, declared: String },
565 #[error("the tree supplied for package {spec} declares the version {declared}")]
567 MismatchedVersion {
568 spec: PackageSpec,
569 declared: PackageVersion,
570 },
571}
572
573impl PackageCatalogIssue {
574 fn spec(&self) -> &PackageSpec {
575 match self {
576 Self::DuplicateSpecification { spec }
577 | Self::MissingDeclaration { spec }
578 | Self::DeclarationNotUtf8 { spec }
579 | Self::MalformedDeclaration { spec, .. }
580 | Self::MismatchedName { spec, .. }
581 | Self::MismatchedVersion { spec, .. } => spec,
582 }
583 }
584
585 fn sort_key(&self) -> (String, u8, String) {
586 let (rank, detail) = match self {
587 Self::DuplicateSpecification { .. } => (0, String::new()),
588 Self::MissingDeclaration { .. } => (1, String::new()),
589 Self::DeclarationNotUtf8 { .. } => (2, String::new()),
590 Self::MalformedDeclaration { message, .. } => (3, message.clone()),
591 Self::MismatchedName { declared, .. } => (4, declared.clone()),
592 Self::MismatchedVersion { declared, .. } => (5, declared.to_string()),
593 };
594 (self.spec().to_string(), rank, detail)
595 }
596}
597
598#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
600#[error("package catalog construction failed with {} issue(s)", .issues.len())]
601pub struct PackageCatalogError {
602 issues: Vec<PackageCatalogIssue>,
603}
604
605impl PackageCatalogError {
606 pub fn issues(&self) -> &[PackageCatalogIssue] {
609 &self.issues
610 }
611}