1#[cfg(feature = "egress")]
11use std::io::BufReader;
12use std::io::Read;
13use std::path::{Path, PathBuf};
14#[cfg(feature = "egress")]
15use std::sync::Arc;
16use std::sync::Mutex;
17#[cfg(feature = "egress")]
18use std::sync::OnceLock;
19
20#[cfg(feature = "egress")]
21use typst::diag::PackageError;
22use typst::foundations::Bytes;
23use typst::syntax::package::PackageSpec;
24use typst_kit::packages::FsPackages;
25
26use crate::error_display::format_error_list;
27use crate::fs_traversal::{self, TraversalPolicy, UnsupportedEntry};
28use crate::limits::{LimitError, Limits, ResourceKind};
29use crate::package_catalog::{PackageTree, PackageTreeError};
30use crate::package_failure::{PackageReadFailure, PackageReadFailureReason};
31#[cfg(feature = "egress")]
32use crate::{
33 PackageArchiveReadError, PackageExpansionLimits, PackageReadError, expand_package_archive,
34 package_archive_url, read_package_archive,
35};
36
37#[cfg(feature = "egress")]
38const USER_AGENT: &str = concat!("typst-pack/", env!("CARGO_PKG_VERSION"));
39
40#[cfg(all(feature = "_test-package-download-probe", debug_assertions))]
41const PACKAGE_DOWNLOAD_PROBE_ENV: &str = "TYPST_PACK_TEST_PACKAGE_DOWNLOAD_PROBE";
42
43pub type FilesystemPackageResource = ResourceKind<1>;
45
46#[allow(non_upper_case_globals)]
47impl ResourceKind<1> {
48 pub const VisitedEntries: Self = Self::new(0);
49 pub const SelectedFiles: Self = Self::new(1);
50 pub const SelectedFileBytes: Self = Self::new(2);
51 pub const PackageTreeBytes: Self = Self::new(3);
52}
53
54pub type FilesystemPackageLimitError = LimitError<FilesystemPackageResource>;
56
57pub type FilesystemPackageLimits = Limits<FilesystemPackageResource>;
59
60impl Limits<FilesystemPackageResource> {
61 #[track_caller]
62 pub fn new(
63 visited_entries: u64,
64 selected_files: u64,
65 selected_file_bytes: u64,
66 package_tree_bytes: u64,
67 ) -> Self {
68 Self::from_ceilings([
69 visited_entries,
70 selected_files,
71 selected_file_bytes,
72 package_tree_bytes,
73 0,
74 0,
75 0,
76 ])
77 .assert_probe_resources([
78 FilesystemPackageResource::VisitedEntries,
79 FilesystemPackageResource::SelectedFiles,
80 FilesystemPackageResource::SelectedFileBytes,
81 FilesystemPackageResource::PackageTreeBytes,
82 ])
83 }
84
85 pub const fn reference_v1() -> Self {
87 Self::from_ceilings([
88 100_000,
89 50_000,
90 64 * 1024 * 1024,
91 512 * 1024 * 1024,
92 0,
93 0,
94 0,
95 ])
96 }
97
98 pub const fn visited_entries(&self) -> u64 {
99 self.ceilings[0]
100 }
101
102 pub const fn selected_files(&self) -> u64 {
103 self.ceilings[1]
104 }
105
106 pub const fn selected_file_bytes(&self) -> u64 {
107 self.ceilings[2]
108 }
109
110 pub const fn package_tree_bytes(&self) -> u64 {
111 self.ceilings[3]
112 }
113}
114
115#[derive(Debug, Clone, Copy, Eq, PartialEq)]
117#[non_exhaustive]
118pub enum FilesystemPackageEntryKind {
119 Socket,
120 Fifo,
121 BlockDevice,
122 CharacterDevice,
123 Unknown,
124}
125
126#[derive(Debug, Clone, Copy, Eq, PartialEq)]
128#[non_exhaustive]
129pub enum FilesystemPackageOperation {
130 InspectRoot,
131 SurveyEntry,
132 InspectSelectedFile,
133 ReadSelectedFile,
134}
135
136impl std::fmt::Display for FilesystemPackageOperation {
137 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138 formatter.write_str(match self {
139 Self::InspectRoot => "inspect package root",
140 Self::SurveyEntry => "survey package entry",
141 Self::InspectSelectedFile => "inspect selected package file",
142 Self::ReadSelectedFile => "read selected package file",
143 })
144 }
145}
146
147#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
149#[non_exhaustive]
150pub enum FilesystemPackageIssue {
151 #[error("unsupported filesystem entry {path:?}: aliases cannot become package files")]
152 Alias { path: PathBuf },
153 #[error("unsupported filesystem entry {path:?} in the package tree")]
154 UnsupportedEntry {
155 path: PathBuf,
156 kind: FilesystemPackageEntryKind,
157 },
158 #[error("package path {path:?} is not valid UTF-8")]
159 UnrepresentablePath { path: PathBuf },
160 #[error("filesystem package root {path:?} is not a directory")]
161 RootNotDirectory { path: PathBuf },
162}
163
164impl FilesystemPackageIssue {
165 fn path(&self) -> &Path {
166 match self {
167 Self::Alias { path }
168 | Self::UnsupportedEntry { path, .. }
169 | Self::UnrepresentablePath { path }
170 | Self::RootNotDirectory { path } => path,
171 }
172 }
173
174 fn rank(&self) -> u8 {
175 match self {
176 Self::Alias { .. } => 0,
177 Self::UnsupportedEntry { .. } => 1,
178 Self::UnrepresentablePath { .. } => 2,
179 Self::RootNotDirectory { .. } => 3,
180 }
181 }
182}
183
184#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
186#[error(
187 "filesystem package survey found {} issue(s){}",
188 .issues.len(),
189 format_error_list(.issues.as_slice())
190)]
191pub struct FilesystemPackageSurveyError {
192 issues: Vec<FilesystemPackageIssue>,
193}
194
195impl FilesystemPackageSurveyError {
196 pub fn issues(&self) -> &[FilesystemPackageIssue] {
197 &self.issues
198 }
199}
200
201#[derive(Debug, thiserror::Error)]
203#[non_exhaustive]
204pub enum FilesystemPackageReadError {
205 #[error("failed to {operation} {path:?}: {source}")]
206 Io {
207 operation: FilesystemPackageOperation,
208 path: PathBuf,
209 #[source]
210 source: std::io::Error,
211 },
212 #[error(transparent)]
213 Survey(FilesystemPackageSurveyError),
214 #[error("filesystem package resource limit at {path:?}: {source}")]
215 Limit {
216 path: PathBuf,
217 source: FilesystemPackageLimitError,
218 },
219 #[error("selected filesystem entries do not form a Package Tree: {0}")]
220 PackageTree(#[source] PackageTreeError),
221}
222
223impl FilesystemPackageReadError {
224 fn io(
225 operation: FilesystemPackageOperation,
226 path: impl Into<PathBuf>,
227 source: std::io::Error,
228 ) -> Self {
229 Self::Io {
230 operation,
231 path: path.into(),
232 source,
233 }
234 }
235
236 fn limit(path: impl Into<PathBuf>, source: FilesystemPackageLimitError) -> Self {
237 Self::Limit {
238 path: path.into(),
239 source,
240 }
241 }
242}
243
244pub fn read_filesystem_package(
246 root: impl AsRef<Path>,
247 limits: FilesystemPackageLimits,
248) -> Result<PackageTree, FilesystemPackageReadError> {
249 let root = root.as_ref();
250 let root_metadata = std::fs::symlink_metadata(root).map_err(|source| {
251 FilesystemPackageReadError::io(FilesystemPackageOperation::InspectRoot, root, source)
252 })?;
253 if root_metadata.file_type().is_symlink() {
254 return Err(FilesystemPackageReadError::Survey(
255 FilesystemPackageSurveyError {
256 issues: vec![FilesystemPackageIssue::Alias {
257 path: root.to_owned(),
258 }],
259 },
260 ));
261 }
262 if !root_metadata.is_dir() {
263 return Err(FilesystemPackageReadError::Survey(
264 FilesystemPackageSurveyError {
265 issues: vec![FilesystemPackageIssue::RootNotDirectory {
266 path: root.to_owned(),
267 }],
268 },
269 ));
270 }
271
272 let mut visited_entries = 0u64;
273 let mut selected_files = 0u64;
274 let mut declared_total = 0u64;
275 let mut selected = Vec::new();
276 let mut issues = Vec::new();
277 let mut deferred_limit = None;
278 for entry in walkdir::WalkDir::new(root).follow_links(false) {
279 let entry = entry.map_err(|error| {
280 let path = error.path().unwrap_or(root).to_owned();
281 let source = error
282 .into_io_error()
283 .unwrap_or_else(|| std::io::Error::other("filesystem traversal failed"));
284 FilesystemPackageReadError::io(FilesystemPackageOperation::SurveyEntry, path, source)
285 })?;
286 if entry.depth() == 0 {
287 continue;
288 }
289
290 visited_entries = checked_add(
291 visited_entries,
292 1,
293 FilesystemPackageResource::VisitedEntries,
294 )
295 .map_err(|source| FilesystemPackageReadError::limit(entry.path(), source))?;
296 check_limit(
297 FilesystemPackageResource::VisitedEntries,
298 limits.visited_entries(),
299 visited_entries,
300 )
301 .map_err(|source| FilesystemPackageReadError::limit(entry.path(), source))?;
302
303 let relative = entry
304 .path()
305 .strip_prefix(root)
306 .expect("walk remains beneath package root");
307 let Some(path) = slash_path(relative) else {
308 issues.push(FilesystemPackageIssue::UnrepresentablePath {
309 path: entry.path().to_owned(),
310 });
311 continue;
312 };
313 let file_type = entry.file_type();
314 if file_type.is_dir() {
315 continue;
316 }
317 if file_type.is_symlink() {
318 issues.push(FilesystemPackageIssue::Alias {
319 path: entry.path().to_owned(),
320 });
321 continue;
322 }
323 if !file_type.is_file() {
324 issues.push(FilesystemPackageIssue::UnsupportedEntry {
325 path: entry.path().to_owned(),
326 kind: UnsupportedEntry::of(&file_type).into(),
327 });
328 continue;
329 }
330
331 selected_files =
332 checked_add(selected_files, 1, FilesystemPackageResource::SelectedFiles)
333 .map_err(|source| FilesystemPackageReadError::limit(entry.path(), source))?;
334 if let Err(source) = check_limit(
335 FilesystemPackageResource::SelectedFiles,
336 limits.selected_files(),
337 selected_files,
338 ) {
339 deferred_limit
340 .get_or_insert_with(|| FilesystemPackageReadError::limit(entry.path(), source));
341 continue;
342 }
343 let metadata = entry.metadata().map_err(|error| {
344 let path = error.path().unwrap_or(entry.path()).to_owned();
345 let source = error.into_io_error().unwrap_or_else(|| {
346 std::io::Error::other("failed to inspect selected package file")
347 });
348 FilesystemPackageReadError::io(
349 FilesystemPackageOperation::InspectSelectedFile,
350 path,
351 source,
352 )
353 })?;
354 if let Err(source) = check_limit(
355 FilesystemPackageResource::SelectedFileBytes,
356 limits.selected_file_bytes(),
357 metadata.len(),
358 ) {
359 deferred_limit
360 .get_or_insert_with(|| FilesystemPackageReadError::limit(entry.path(), source));
361 continue;
362 }
363 declared_total = checked_add(
364 declared_total,
365 metadata.len(),
366 FilesystemPackageResource::PackageTreeBytes,
367 )
368 .map_err(|source| FilesystemPackageReadError::limit(entry.path(), source))?;
369 if let Err(source) = check_limit(
370 FilesystemPackageResource::PackageTreeBytes,
371 limits.package_tree_bytes(),
372 declared_total,
373 ) {
374 deferred_limit
375 .get_or_insert_with(|| FilesystemPackageReadError::limit(entry.path(), source));
376 continue;
377 }
378 selected.push((path, entry.path().to_owned()));
379 }
380
381 if !issues.is_empty() {
382 issues.sort_by(|left, right| {
383 left.path()
384 .cmp(right.path())
385 .then_with(|| left.rank().cmp(&right.rank()))
386 });
387 return Err(FilesystemPackageReadError::Survey(
388 FilesystemPackageSurveyError { issues },
389 ));
390 }
391 if let Some(error) = deferred_limit {
392 return Err(error);
393 }
394
395 selected.sort_by(|(left, _), (right, _)| left.cmp(right));
396 let mut actual_total = 0u64;
397 let mut entries = Vec::with_capacity(selected.len());
398 for (path, source) in selected {
399 let mut file = fs_traversal::open_without_following(&PackageTraversal, root, &source)?;
400 let bytes = read_bounded_package_file(
401 &mut file,
402 &source,
403 limits.selected_file_bytes(),
404 actual_total,
405 limits.package_tree_bytes(),
406 )?;
407 let observed = u64::try_from(bytes.len()).map_err(|_| {
408 FilesystemPackageReadError::limit(
409 &source,
410 FilesystemPackageLimitError::AccountingOverflow {
411 resource: FilesystemPackageResource::PackageTreeBytes,
412 },
413 )
414 })?;
415 actual_total = checked_add(
416 actual_total,
417 observed,
418 FilesystemPackageResource::PackageTreeBytes,
419 )
420 .map_err(|source_error| FilesystemPackageReadError::limit(&source, source_error))?;
421 entries.push((path, bytes));
422 }
423
424 PackageTree::from_owned_entries(entries).map_err(FilesystemPackageReadError::PackageTree)
425}
426
427fn read_bounded_package_file(
428 mut reader: impl Read,
429 path: &Path,
430 selected_file_ceiling: u64,
431 total_before: u64,
432 package_tree_ceiling: u64,
433) -> Result<Vec<u8>, FilesystemPackageReadError> {
434 let total_allowance = package_tree_ceiling.saturating_sub(total_before);
435 let allowance = selected_file_ceiling.min(total_allowance);
436 let mut bytes = Vec::new();
437 reader
438 .by_ref()
439 .take(allowance + 1)
440 .read_to_end(&mut bytes)
441 .map_err(|error| {
442 FilesystemPackageReadError::io(
443 FilesystemPackageOperation::ReadSelectedFile,
444 path,
445 error,
446 )
447 })?;
448 let observed = u64::try_from(bytes.len()).map_err(|_| {
449 FilesystemPackageReadError::limit(
450 path,
451 FilesystemPackageLimitError::AccountingOverflow {
452 resource: FilesystemPackageResource::SelectedFileBytes,
453 },
454 )
455 })?;
456 check_limit(
457 FilesystemPackageResource::SelectedFileBytes,
458 selected_file_ceiling,
459 observed,
460 )
461 .map_err(|source| FilesystemPackageReadError::limit(path, source))?;
462 let total = checked_add(
463 total_before,
464 observed,
465 FilesystemPackageResource::PackageTreeBytes,
466 )
467 .map_err(|source| FilesystemPackageReadError::limit(path, source))?;
468 check_limit(
469 FilesystemPackageResource::PackageTreeBytes,
470 package_tree_ceiling,
471 total,
472 )
473 .map_err(|source| FilesystemPackageReadError::limit(path, source))?;
474 Ok(bytes)
475}
476
477struct PackageTraversal;
479
480impl TraversalPolicy for PackageTraversal {
481 type Error = FilesystemPackageReadError;
482
483 const ROOT_INVARIANT: &'static str = "a selected package path remains beneath its root";
484
485 fn io(&self, path: &Path, source: std::io::Error) -> Self::Error {
486 FilesystemPackageReadError::io(FilesystemPackageOperation::ReadSelectedFile, path, source)
487 }
488
489 fn alias(&self, path: &Path) -> Self::Error {
490 alias_error(path)
491 }
492
493 fn unsupported_entry(&self, path: &Path, entry: UnsupportedEntry) -> Self::Error {
494 FilesystemPackageReadError::Survey(FilesystemPackageSurveyError {
495 issues: vec![FilesystemPackageIssue::UnsupportedEntry {
496 path: path.to_owned(),
497 kind: entry.into(),
498 }],
499 })
500 }
501}
502
503impl From<UnsupportedEntry> for FilesystemPackageEntryKind {
504 fn from(entry: UnsupportedEntry) -> Self {
505 match entry {
506 UnsupportedEntry::Socket => Self::Socket,
507 UnsupportedEntry::Fifo => Self::Fifo,
508 UnsupportedEntry::BlockDevice => Self::BlockDevice,
509 UnsupportedEntry::CharacterDevice => Self::CharacterDevice,
510 UnsupportedEntry::Unknown => Self::Unknown,
511 }
512 }
513}
514
515fn alias_error(path: &Path) -> FilesystemPackageReadError {
516 FilesystemPackageReadError::Survey(FilesystemPackageSurveyError {
517 issues: vec![FilesystemPackageIssue::Alias {
518 path: path.to_owned(),
519 }],
520 })
521}
522
523fn slash_path(path: &Path) -> Option<String> {
524 path.components()
525 .map(|component| component.as_os_str().to_str())
526 .collect::<Option<Vec<_>>>()
527 .map(|components| components.join("/"))
528}
529
530fn checked_add(
531 total: u64,
532 value: u64,
533 resource: FilesystemPackageResource,
534) -> Result<u64, FilesystemPackageLimitError> {
535 total
536 .checked_add(value)
537 .ok_or(FilesystemPackageLimitError::AccountingOverflow { resource })
538}
539
540fn check_limit(
541 resource: FilesystemPackageResource,
542 ceiling: u64,
543 observed: u64,
544) -> Result<(), FilesystemPackageLimitError> {
545 if observed > ceiling {
546 return Err(FilesystemPackageLimitError::exceeded(resource, ceiling));
547 }
548 Ok(())
549}
550
551#[derive(Debug, thiserror::Error)]
557#[non_exhaustive]
558pub enum FilesystemPackageAuthorityReadError {
559 #[error(transparent)]
560 Unavailable(PackageReadFailure),
561 #[error("{failure}: {source}")]
562 Filesystem {
563 failure: PackageReadFailure,
564 #[source]
565 source: Box<FilesystemPackageReadError>,
566 },
567 #[cfg(feature = "egress")]
568 #[error("{failure}: {source}")]
569 RegistryUrl {
570 failure: PackageReadFailure,
571 #[source]
572 source: Box<PackageReadError>,
573 },
574 #[cfg(feature = "egress")]
575 #[error("{failure}: {source}")]
576 Download {
577 failure: PackageReadFailure,
578 #[source]
579 source: std::io::Error,
580 },
581 #[cfg(feature = "egress")]
582 #[error("{failure}: {source}")]
583 DownloadSize {
584 failure: PackageReadFailure,
585 #[source]
586 source: std::num::TryFromIntError,
587 },
588 #[cfg(feature = "egress")]
589 #[error("{failure}: {source}")]
590 ArchiveRead {
591 failure: PackageReadFailure,
592 #[source]
593 source: Box<PackageArchiveReadError>,
594 },
595 #[cfg(feature = "egress")]
596 #[error("{failure}: {source}")]
597 ArchiveExpansion {
598 failure: PackageReadFailure,
599 #[source]
600 source: Box<PackageReadError>,
601 },
602 #[cfg(feature = "egress")]
603 #[error("{failure}: {source}")]
604 Cache {
605 failure: PackageReadFailure,
606 #[source]
607 source: Box<PackageError>,
608 },
609}
610
611impl FilesystemPackageAuthorityReadError {
612 pub fn failure(&self) -> &PackageReadFailure {
614 match self {
615 Self::Unavailable(failure) | Self::Filesystem { failure, .. } => failure,
616 #[cfg(feature = "egress")]
617 Self::RegistryUrl { failure, .. }
618 | Self::Download { failure, .. }
619 | Self::DownloadSize { failure, .. }
620 | Self::ArchiveRead { failure, .. }
621 | Self::ArchiveExpansion { failure, .. }
622 | Self::Cache { failure, .. } => failure,
623 }
624 }
625}
626
627#[derive(Debug)]
632pub struct FilesystemPackageAuthority {
633 data: Option<FsPackages>,
634 cache: Option<FsPackages>,
635 offline: bool,
636 source_limits: FilesystemPackageLimits,
637 #[cfg(feature = "egress")]
638 expansion_limits: PackageExpansionLimits,
639 #[cfg(feature = "egress")]
640 certificate: Option<PathBuf>,
641}
642
643impl FilesystemPackageAuthority {
644 pub fn new(
646 package_path: Option<&Path>,
647 package_cache_path: Option<&Path>,
648 offline: bool,
649 ) -> Self {
650 Self::with_limits(
651 package_path,
652 package_cache_path,
653 offline,
654 FilesystemPackageLimits::reference_v1(),
655 #[cfg(feature = "egress")]
656 PackageExpansionLimits::reference_v1(),
657 )
658 }
659
660 pub(crate) fn with_limits(
661 package_path: Option<&Path>,
662 package_cache_path: Option<&Path>,
663 offline: bool,
664 source_limits: FilesystemPackageLimits,
665 #[cfg(feature = "egress")] expansion_limits: PackageExpansionLimits,
666 ) -> Self {
667 let data = match package_path {
668 Some(path) => Some(FsPackages::new(path)),
669 None => FsPackages::system_data(),
670 };
671 let cache = match package_cache_path {
672 Some(path) => Some(FsPackages::new(path)),
673 None => FsPackages::system_cache(),
674 };
675 Self {
676 data,
677 cache,
678 offline,
679 source_limits,
680 #[cfg(feature = "egress")]
681 expansion_limits,
682 #[cfg(feature = "egress")]
683 certificate: None,
684 }
685 }
686
687 #[cfg(feature = "egress")]
689 pub fn certificate(mut self, certificate: Option<PathBuf>) -> Self {
690 self.certificate = certificate;
691 self
692 }
693
694 pub fn read(
697 &self,
698 spec: &PackageSpec,
699 ) -> Result<ReadPackage, FilesystemPackageAuthorityReadError> {
700 if let Some(read) = self.read_from(&self.data, spec)? {
701 return Ok(read);
702 }
703 if let Some(read) = self.read_from(&self.cache, spec)? {
704 return Ok(read);
705 }
706 if self.offline {
707 return Err(FilesystemPackageAuthorityReadError::Unavailable(not_found(
708 spec,
709 )));
710 }
711
712 #[cfg(feature = "egress")]
713 {
714 self.read_from_registry(spec)
715 }
716 #[cfg(not(feature = "egress"))]
717 {
718 Err(FilesystemPackageAuthorityReadError::Unavailable(not_found(
719 spec,
720 )))
721 }
722 }
723
724 fn read_from(
725 &self,
726 packages: &Option<FsPackages>,
727 spec: &PackageSpec,
728 ) -> Result<Option<ReadPackage>, FilesystemPackageAuthorityReadError> {
729 let Some(root) = packages.as_ref().and_then(|packages| packages.obtain(spec)) else {
730 return Ok(None);
731 };
732 let tree = read_filesystem_package(root.path(), self.source_limits).map_err(|source| {
733 let failure = other_failure(spec, source.to_string());
734 FilesystemPackageAuthorityReadError::Filesystem {
735 failure,
736 source: Box::new(source),
737 }
738 })?;
739 Ok(Some(ReadPackage {
740 tree,
741 root: Some(root.path().to_owned()),
742 }))
743 }
744
745 #[cfg(feature = "egress")]
746 fn read_from_registry(
747 &self,
748 spec: &PackageSpec,
749 ) -> Result<ReadPackage, FilesystemPackageAuthorityReadError> {
750 let url = package_archive_url(spec).map_err(|source| {
751 FilesystemPackageAuthorityReadError::RegistryUrl {
752 failure: not_found(spec),
753 source: Box::new(source),
754 }
755 })?;
756 let downloader = RustlsDownloader::new(USER_AGENT, self.certificate.clone());
757 use typst_kit::downloader::Downloader;
758 let (known_size, reader) = downloader.stream(spec, &url).map_err(|source| {
759 let failure = if source.kind() == std::io::ErrorKind::NotFound {
762 not_found(spec)
763 } else {
764 PackageReadFailure::new(
765 spec.clone(),
766 PackageReadFailureReason::NetworkFailed {
767 detail: Some(source.to_string()),
768 },
769 )
770 };
771 FilesystemPackageAuthorityReadError::Download { failure, source }
772 })?;
773 let known_size = known_size
774 .map(u64::try_from)
775 .transpose()
776 .map_err(|source| FilesystemPackageAuthorityReadError::DownloadSize {
777 failure: other_failure(spec, "download size is not representable"),
778 source,
779 })?;
780 let tree = read_registry_tree(spec, reader, known_size, self.expansion_limits)?;
781
782 let root = if let Some(cache) = &self.cache {
783 cache
784 .store(spec, |directory| write_tree(directory, &tree))
785 .map_err(|source| FilesystemPackageAuthorityReadError::Cache {
786 failure: other_failure(spec, source.to_string()),
787 source: Box::new(source),
788 })?;
789 Some(package_root(cache.path(), spec))
790 } else {
791 None
792 };
793 Ok(ReadPackage { tree, root })
794 }
795}
796
797#[derive(Debug)]
799pub struct ReadPackage {
800 tree: PackageTree,
801 root: Option<PathBuf>,
802}
803
804impl ReadPackage {
805 pub fn tree(&self) -> &PackageTree {
807 &self.tree
808 }
809
810 pub fn root(&self) -> Option<&Path> {
812 self.root.as_deref()
813 }
814
815 pub fn into_parts(self) -> (PackageTree, Option<PathBuf>) {
817 (self.tree, self.root)
818 }
819}
820
821fn not_found(spec: &PackageSpec) -> PackageReadFailure {
822 PackageReadFailure::new(spec.clone(), PackageReadFailureReason::NotFound)
823}
824
825fn other_failure(spec: &PackageSpec, detail: impl Into<String>) -> PackageReadFailure {
826 PackageReadFailure::new(
827 spec.clone(),
828 PackageReadFailureReason::Other {
829 detail: Some(detail.into()),
830 },
831 )
832}
833
834#[cfg(feature = "egress")]
835fn read_registry_tree(
836 spec: &PackageSpec,
837 reader: impl Read,
838 known_size: Option<u64>,
839 limits: PackageExpansionLimits,
840) -> Result<PackageTree, FilesystemPackageAuthorityReadError> {
841 let archive = read_package_archive(reader, known_size, limits).map_err(|source| {
842 let failure = match &source {
843 PackageArchiveReadError::Read(error) => PackageReadFailure::new(
844 spec.clone(),
845 PackageReadFailureReason::NetworkFailed {
846 detail: Some(error.to_string()),
847 },
848 ),
849 PackageArchiveReadError::Limit(error) => other_failure(spec, error.to_string()),
850 };
851 FilesystemPackageAuthorityReadError::ArchiveRead {
852 failure,
853 source: Box::new(source),
854 }
855 })?;
856 expand_package_archive(spec.clone(), &archive, limits).map_err(|source| {
857 let failure = match &source {
858 PackageReadError::UnservedNamespace { .. } => not_found(spec),
859 PackageReadError::ExpansionLimit { .. } => other_failure(spec, source.to_string()),
860 PackageReadError::MalformedArchive { .. }
861 | PackageReadError::InvalidPackageTree { .. } => PackageReadFailure::new(
862 spec.clone(),
863 PackageReadFailureReason::MalformedArchive {
864 detail: Some(source.to_string()),
865 },
866 ),
867 };
868 FilesystemPackageAuthorityReadError::ArchiveExpansion {
869 failure,
870 source: Box::new(source),
871 }
872 })
873}
874
875#[cfg(feature = "egress")]
876fn package_root(base: &Path, spec: &PackageSpec) -> PathBuf {
877 base.join(crate::read_layout::package_tree_key(spec))
878}
879
880#[cfg(feature = "egress")]
881fn write_tree(directory: &Path, tree: &PackageTree) -> typst::diag::PackageResult<()> {
882 for (path, data) in tree.files() {
883 let destination = directory.join(path);
884 if let Some(parent) = destination.parent() {
885 std::fs::create_dir_all(parent).map_err(package_cache_error)?;
886 }
887 std::fs::write(destination, data).map_err(package_cache_error)?;
888 }
889 Ok(())
890}
891
892#[cfg(feature = "egress")]
893fn package_cache_error(error: std::io::Error) -> PackageError {
894 PackageError::Other(Some(
895 format!("failed to cache downloaded package: {error}").into(),
896 ))
897}
898
899pub(crate) struct ReadPackages {
901 trees: Mutex<Vec<(PackageSpec, PackageTree)>>,
902}
903
904impl ReadPackages {
905 pub(crate) fn new() -> Self {
906 Self {
907 trees: Mutex::new(Vec::new()),
908 }
909 }
910
911 pub(crate) fn record(&self, spec: PackageSpec, tree: PackageTree) {
912 self.trees
913 .lock()
914 .expect("read package lock poisoned")
915 .push((spec, tree));
916 }
917
918 pub(crate) fn file(&self, spec: &PackageSpec, path: &str) -> Option<Bytes> {
921 self.trees
922 .lock()
923 .expect("read package lock poisoned")
924 .iter()
925 .find(|(candidate, _)| candidate == spec)?
926 .1
927 .shared_file(path)
928 .map(|data| data.to_typst())
929 }
930}
931
932#[cfg(feature = "egress")]
933struct RustlsDownloader {
934 user_agent: &'static str,
935 certificate: Option<PathBuf>,
936 tls: OnceLock<Result<Option<Arc<ureq::rustls::ClientConfig>>, String>>,
937}
938
939#[cfg(feature = "egress")]
940impl RustlsDownloader {
941 fn new(user_agent: &'static str, certificate: Option<PathBuf>) -> Self {
942 Self {
943 user_agent,
944 certificate,
945 tls: OnceLock::new(),
946 }
947 }
948
949 fn root_store(path: &Path) -> Result<ureq::rustls::RootCertStore, String> {
954 use ureq::rustls::pki_types::CertificateDer;
955 use ureq::rustls::pki_types::pem::PemObject;
956
957 let file = std::fs::File::open(path).map_err(|error| error.to_string())?;
958 let reader = BufReader::new(file);
959 let mut roots = ureq::rustls::RootCertStore {
960 roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(),
961 };
962 for certificate in CertificateDer::pem_reader_iter(reader) {
963 let certificate = certificate.map_err(|error| error.to_string())?;
964 roots.add(certificate).map_err(|error| error.to_string())?;
965 }
966 Ok(roots)
967 }
968
969 fn tls_config(&self) -> std::io::Result<Option<Arc<ureq::rustls::ClientConfig>>> {
970 match self.tls.get_or_init(|| {
971 let Some(path) = &self.certificate else {
972 return Ok(None);
973 };
974 let roots = Self::root_store(path)?;
975 let tls = ureq::rustls::ClientConfig::builder()
976 .with_root_certificates(roots)
977 .with_no_client_auth();
978 Ok(Some(Arc::new(tls)))
979 }) {
980 Ok(tls) => Ok(tls.clone()),
981 Err(error) => Err(std::io::Error::other(error.clone())),
982 }
983 }
984}
985
986#[cfg(feature = "egress")]
987impl typst_kit::downloader::Downloader for RustlsDownloader {
988 fn stream(
989 &self,
990 _key: &dyn std::any::Any,
991 url: &str,
992 ) -> std::io::Result<(Option<usize>, Box<dyn Read>)> {
993 #[cfg(all(feature = "_test-package-download-probe", debug_assertions))]
994 if let Some(output) = std::env::var_os(PACKAGE_DOWNLOAD_PROBE_ENV) {
995 let certificate = self
996 .certificate
997 .as_deref()
998 .map(|path| path.to_string_lossy())
999 .unwrap_or_default();
1000 std::fs::write(output, certificate.as_bytes())?;
1001 return Err(std::io::Error::new(
1002 std::io::ErrorKind::PermissionDenied,
1003 "package download stopped by test probe",
1004 ));
1005 }
1006
1007 let mut builder = ureq::AgentBuilder::new().user_agent(self.user_agent);
1008 if let Some(proxy) = env_proxy::for_url_str(url)
1009 .to_url()
1010 .and_then(|url| ureq::Proxy::new(url).ok())
1011 {
1012 builder = builder.proxy(proxy);
1013 }
1014 if let Some(tls) = self.tls_config()? {
1015 builder = builder.tls_config(tls);
1016 }
1017 let response = builder
1018 .build()
1019 .get(url)
1020 .call()
1021 .map_err(|error| match error {
1022 ureq::Error::Status(404, _) => {
1023 std::io::Error::new(std::io::ErrorKind::NotFound, error)
1024 }
1025 error => std::io::Error::other(error),
1026 })?;
1027 let content_length = response
1028 .header("Content-Length")
1029 .and_then(|value| value.parse().ok());
1030 Ok((content_length, response.into_reader()))
1031 }
1032}
1033
1034#[cfg(test)]
1035mod tests {
1036 use super::*;
1037
1038 #[cfg(feature = "egress")]
1041 const TEST_CERTIFICATE_PEM: &str = "\
1042-----BEGIN CERTIFICATE-----\n\
1043MIIDHTCCAgWgAwIBAgIUd7rNizjvFLekUK8kk8YGERd1Ru8wDQYJKoZIhvcNAQEL\n\
1044BQAwHTEbMBkGA1UEAwwSdHlwc3QtcGFjayB0ZXN0IENBMCAXDTI2MDgyNTEyNTcz\n\
1045NloYDzIxMjYwODAxMTI1NzM2WjAdMRswGQYDVQQDDBJ0eXBzdC1wYWNrIHRlc3Qg\n\
1046Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaimCA3A7auE+n08On\n\
10474RKiJPJzM8Aqv9zYvjXRJsMuo1aXBme9MLYEAu+T9DCxPWaVJYEKqZEkNYJVOF04\n\
10485ONBZYsk7uQ8a72DZtBV123RXWPa/cD9ieXVgN4oRC2FdfyELyTcUHtRo1zFlSSx\n\
1049dz2TY0Zzk5ON62d1O89zdeUPJ/mah4jxuPJeFPTeJZFdYOjaOSUr06MVWgYQ1hC0\n\
1050nNYG3rFqVucb+83stE1nZFBAegwrdE6poQUFT3rE+ApxVzHDWi1gaD3VVBszmMjZ\n\
1051FwufcnW2kCXVvmM5YebnvLyqIHvNddWkMygWSPwAJDFXWRdi6MzYkoC1qTpbkj8J\n\
1052PI+7AgMBAAGjUzBRMB0GA1UdDgQWBBRlvjvjxE0mp20Do4trUIL0RMDa8jAfBgNV\n\
1053HSMEGDAWgBRlvjvjxE0mp20Do4trUIL0RMDa8jAPBgNVHRMBAf8EBTADAQH/MA0G\n\
1054CSqGSIb3DQEBCwUAA4IBAQAigZCXG4KVwXHFE7G0wLxWuWizmLgj2fEU7W0Wy1rp\n\
1055qXEOlKCNG0MX/JTlqydyv+ZSKPMyl9eX+SPFlhlm/Az8kPHYWd7CqdO4Kz0fSEhx\n\
1056IrChOWM5OfEUn+JXRSDJnBFcYVgucKdeC9yiP3WqQc0fJ5j14+tWUsMU1lr/392L\n\
1057xZni29e/+sP8VvO2eGO+CyYsAlgTJ0Ka1QwSJYS3jLMfYhsNpMpnmzL7bAy+Uyuz\n\
1058/Xt4qjy1STvhUAruckTFLjJ/6i0GK2mm0XjI4/xXoyh2c40yj16l87Ncur2bjQ3o\n\
1059D/11HiutermQt0RJByBT1FPDdLyRtRPwY1PoAi2/OlAb\n\
1060-----END CERTIFICATE-----\n\
1061";
1062
1063 #[cfg(feature = "egress")]
1064 #[test]
1065 fn custom_certificate_pem_is_added_to_the_root_store() {
1066 let directory = tempfile::tempdir().unwrap();
1067 let path = directory.path().join("ca.pem");
1068 std::fs::write(&path, TEST_CERTIFICATE_PEM).unwrap();
1069
1070 let roots = RustlsDownloader::root_store(&path).expect("a valid PEM certificate is read");
1072 assert_eq!(roots.roots.len(), webpki_roots::TLS_SERVER_ROOTS.len() + 1);
1073
1074 let downloader = RustlsDownloader::new("typst-pack-test", Some(path));
1075 assert!(
1076 downloader
1077 .tls_config()
1078 .expect("a valid PEM certificate is accepted")
1079 .is_some()
1080 );
1081 }
1082
1083 #[cfg(feature = "egress")]
1087 #[test]
1088 fn certificate_file_without_a_pem_section_keeps_the_default_roots() {
1089 let directory = tempfile::tempdir().unwrap();
1090 let path = directory.path().join("ca.pem");
1091 std::fs::write(&path, "not a certificate").unwrap();
1092
1093 let roots =
1094 RustlsDownloader::root_store(&path).expect("a section-free file is not an error");
1095 assert_eq!(roots.roots.len(), webpki_roots::TLS_SERVER_ROOTS.len());
1096 }
1097
1098 #[cfg(feature = "egress")]
1099 #[test]
1100 fn corrupt_certificate_pem_is_rejected() {
1101 let directory = tempfile::tempdir().unwrap();
1102 let path = directory.path().join("ca.pem");
1103 std::fs::write(
1104 &path,
1105 "-----BEGIN CERTIFICATE-----\nnot base64!!\n-----END CERTIFICATE-----\n",
1106 )
1107 .unwrap();
1108
1109 assert!(RustlsDownloader::root_store(&path).is_err());
1110
1111 let downloader = RustlsDownloader::new("typst-pack-test", Some(path));
1112 assert!(downloader.tls_config().is_err());
1113 }
1114
1115 #[cfg(feature = "egress")]
1116 #[test]
1117 fn absent_certificate_leaves_the_default_roots_in_place() {
1118 let downloader = RustlsDownloader::new("typst-pack-test", None);
1119 assert!(downloader.tls_config().unwrap().is_none());
1120 }
1121
1122 #[cfg(feature = "egress")]
1123 fn package_archive(files: &[(&str, &[u8])]) -> Vec<u8> {
1124 let mut builder = tar::Builder::new(flate2::write::GzEncoder::new(
1125 Vec::new(),
1126 flate2::Compression::default(),
1127 ));
1128 for (path, data) in files {
1129 let mut header = tar::Header::new_gnu();
1130 header.set_size(data.len() as u64);
1131 header.set_mode(0o644);
1132 builder.append_data(&mut header, path, *data).unwrap();
1133 }
1134 builder.into_inner().unwrap().finish().unwrap()
1135 }
1136
1137 #[test]
1138 fn package_tree_accounting_overflow_is_typed() {
1139 assert_eq!(
1140 checked_add(u64::MAX, 1, FilesystemPackageResource::PackageTreeBytes),
1141 Err(FilesystemPackageLimitError::AccountingOverflow {
1142 resource: FilesystemPackageResource::PackageTreeBytes,
1143 })
1144 );
1145 }
1146
1147 #[test]
1148 fn incremental_package_file_read_stops_at_the_plus_one_byte() {
1149 let error =
1150 read_bounded_package_file(&b"12345-extra"[..], Path::new("package.typ"), 4, 0, 100)
1151 .unwrap_err();
1152 assert!(matches!(
1153 error,
1154 FilesystemPackageReadError::Limit {
1155 source,
1156 ..
1157 } if source == FilesystemPackageLimitError::exceeded(
1158 FilesystemPackageResource::SelectedFileBytes,
1159 4,
1160 )
1161 ));
1162 }
1163
1164 #[test]
1165 fn incremental_package_tree_read_reports_accounting_overflow() {
1166 let error =
1167 read_bounded_package_file(&b"x"[..], Path::new("package.typ"), 1, u64::MAX, u64::MAX)
1168 .unwrap_err();
1169 assert!(matches!(
1170 error,
1171 FilesystemPackageReadError::Limit {
1172 source: FilesystemPackageLimitError::AccountingOverflow {
1173 resource: FilesystemPackageResource::PackageTreeBytes,
1174 },
1175 ..
1176 }
1177 ));
1178 }
1179
1180 #[cfg(feature = "egress")]
1181 #[test]
1182 fn registry_response_bytes_are_bounded_then_expanded_without_a_reread() {
1183 let spec = "@preview/example:1.0.0".parse().unwrap();
1184 let archive = package_archive(&[("lib.typ", b"exact registry bytes")]);
1185
1186 let tree = read_registry_tree(
1187 &spec,
1188 std::io::Cursor::new(&archive),
1189 Some(archive.len() as u64),
1190 PackageExpansionLimits::reference_v1(),
1191 )
1192 .unwrap();
1193
1194 assert_eq!(tree.file("lib.typ"), Some(&b"exact registry bytes"[..]));
1195 }
1196
1197 #[cfg(feature = "egress")]
1198 #[test]
1199 fn registry_expansion_limits_retain_the_typed_cause_without_claiming_malformed_bytes() {
1200 let spec = "@preview/example:1.0.0".parse().unwrap();
1201 let archive = package_archive(&[("lib.typ", b"12345")]);
1202 let limits = PackageExpansionLimits::new(1024 * 1024, 10, 100, 4, 100);
1203
1204 let error = read_registry_tree(
1205 &spec,
1206 std::io::Cursor::new(&archive),
1207 Some(archive.len() as u64),
1208 limits,
1209 )
1210 .unwrap_err();
1211
1212 let FilesystemPackageAuthorityReadError::ArchiveExpansion { failure, source } = error
1213 else {
1214 panic!("expected a typed archive expansion cause");
1215 };
1216 assert!(matches!(
1217 failure.reason(),
1218 PackageReadFailureReason::Other { .. }
1219 ));
1220 assert!(matches!(
1221 source.as_ref(),
1222 PackageReadError::ExpansionLimit { .. }
1223 ));
1224 }
1225}