1#[cfg(windows)]
4use std::fs::File;
5use std::io::Read;
6use std::path::{Path, PathBuf};
7
8use crate::error_display::format_error_list;
9#[cfg(feature = "embedded-fonts")]
10use crate::font_catalog::typst_embedded_font_containers;
11use crate::font_catalog::{
12 FontCatalog, FontCatalogEntry, FontContainer, FontContainerError, FontDisposition,
13};
14use crate::fs_traversal::{self, TraversalPolicy, UnsupportedEntry};
15use crate::limits::{LimitError, Limits, ResourceKind};
16
17pub type FilesystemFontResource = ResourceKind<2>;
19
20#[allow(non_upper_case_globals)]
21impl ResourceKind<2> {
22 pub const VisitedEntries: Self = Self::new(0);
23 pub const AcceptedContainers: Self = Self::new(1);
24 pub const ContainerBytes: Self = Self::new(2);
25 pub const TotalAcceptedBytes: Self = Self::new(3);
26}
27
28pub type FilesystemFontLimitError = LimitError<FilesystemFontResource>;
30
31pub type FilesystemFontLimits = Limits<FilesystemFontResource>;
33
34impl Limits<FilesystemFontResource> {
35 #[track_caller]
37 pub fn new(
38 visited_entries: u64,
39 accepted_containers: u64,
40 container_bytes: u64,
41 total_accepted_bytes: u64,
42 ) -> Self {
43 Self::from_ceilings([
44 visited_entries,
45 accepted_containers,
46 container_bytes,
47 total_accepted_bytes,
48 0,
49 0,
50 0,
51 ])
52 .assert_probe_resources([
53 FilesystemFontResource::VisitedEntries,
54 FilesystemFontResource::AcceptedContainers,
55 FilesystemFontResource::ContainerBytes,
56 FilesystemFontResource::TotalAcceptedBytes,
57 ])
58 }
59
60 pub const fn reference_v1() -> Self {
62 Self::from_ceilings([
63 100_000,
64 16_384,
65 256 * 1024 * 1024,
66 2 * 1024 * 1024 * 1024,
67 0,
68 0,
69 0,
70 ])
71 }
72
73 pub const fn visited_entries(&self) -> u64 {
74 self.ceilings[0]
75 }
76
77 pub const fn accepted_containers(&self) -> u64 {
78 self.ceilings[1]
79 }
80
81 pub const fn container_bytes(&self) -> u64 {
82 self.ceilings[2]
83 }
84
85 pub const fn total_accepted_bytes(&self) -> u64 {
86 self.ceilings[3]
87 }
88}
89
90#[derive(Clone, Debug, Eq, PartialEq)]
92pub struct FilesystemFontSource {
93 kind: FilesystemFontSourceKind,
94 disposition: FontDisposition,
95}
96
97#[derive(Clone, Debug, Eq, PartialEq)]
98enum FilesystemFontSourceKind {
99 System,
100 #[cfg(feature = "embedded-fonts")]
101 TypstEmbedded,
102 Directory(PathBuf),
103}
104
105impl FilesystemFontSource {
106 pub fn system(disposition: FontDisposition) -> Self {
108 Self {
109 kind: FilesystemFontSourceKind::System,
110 disposition,
111 }
112 }
113
114 #[cfg(feature = "embedded-fonts")]
116 pub fn typst_embedded(disposition: FontDisposition) -> Self {
117 Self {
118 kind: FilesystemFontSourceKind::TypstEmbedded,
119 disposition,
120 }
121 }
122
123 pub fn directory(path: impl Into<PathBuf>, disposition: FontDisposition) -> Self {
125 Self {
126 kind: FilesystemFontSourceKind::Directory(path.into()),
127 disposition,
128 }
129 }
130
131 pub fn disposition(&self) -> FontDisposition {
133 self.disposition
134 }
135}
136
137#[derive(Debug, Clone, Copy, Eq, PartialEq)]
139#[non_exhaustive]
140pub enum FilesystemFontEntryKind {
141 Socket,
142 Fifo,
143 BlockDevice,
144 CharacterDevice,
145 Unknown,
146}
147
148#[derive(Debug, Clone, Copy, Eq, PartialEq)]
150#[non_exhaustive]
151pub enum FilesystemFontOperation {
152 InspectRoot,
153 SurveyEntry,
154 InspectContainer,
155 ReadContainer,
156}
157
158impl std::fmt::Display for FilesystemFontOperation {
159 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160 formatter.write_str(match self {
161 Self::InspectRoot => "inspect font source root",
162 Self::SurveyEntry => "survey font source entry",
163 Self::InspectContainer => "inspect selected Font Container",
164 Self::ReadContainer => "read selected Font Container",
165 })
166 }
167}
168
169#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
171#[non_exhaustive]
172pub enum FilesystemFontIssue {
173 #[error("unsupported filesystem entry {path:?}: aliases cannot become Font Containers")]
174 Alias { path: PathBuf },
175 #[error("unsupported eligible font entry {path:?}")]
176 UnsupportedEntry {
177 path: PathBuf,
178 kind: FilesystemFontEntryKind,
179 },
180 #[error("filesystem font source root {path:?} is not a directory")]
181 RootNotDirectory { path: PathBuf },
182}
183
184impl FilesystemFontIssue {
185 fn path(&self) -> &Path {
186 match self {
187 Self::Alias { path }
188 | Self::UnsupportedEntry { path, .. }
189 | Self::RootNotDirectory { path } => path,
190 }
191 }
192
193 fn rank(&self) -> u8 {
194 match self {
195 Self::Alias { .. } => 0,
196 Self::UnsupportedEntry { .. } => 1,
197 Self::RootNotDirectory { .. } => 2,
198 }
199 }
200}
201
202#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
204#[error(
205 "filesystem font survey found {} issue(s){}",
206 .issues.len(),
207 format_error_list(.issues.as_slice())
208)]
209pub struct FilesystemFontSurveyError {
210 issues: Vec<FilesystemFontIssue>,
211}
212
213impl FilesystemFontSurveyError {
214 pub fn issues(&self) -> &[FilesystemFontIssue] {
215 &self.issues
216 }
217}
218
219#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
221#[error("invalid Font Container at {path:?}: {source}")]
222pub struct FilesystemFontContainerIssue {
223 path: PathBuf,
224 source: FontContainerError,
225}
226
227impl FilesystemFontContainerIssue {
228 pub fn path(&self) -> &Path {
230 &self.path
231 }
232
233 pub fn source(&self) -> FontContainerError {
235 self.source
236 }
237}
238
239#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
241#[error(
242 "filesystem font validation found {} invalid container(s){}",
243 .issues.len(),
244 format_error_list(.issues.as_slice())
245)]
246pub struct FilesystemFontValidationError {
247 issues: Vec<FilesystemFontContainerIssue>,
248}
249
250impl FilesystemFontValidationError {
251 pub fn issues(&self) -> &[FilesystemFontContainerIssue] {
252 &self.issues
253 }
254}
255
256#[derive(Debug, thiserror::Error)]
258#[non_exhaustive]
259pub enum FilesystemFontReadError {
260 #[error("failed to {operation} {path:?}: {source}")]
261 Io {
262 operation: FilesystemFontOperation,
263 path: PathBuf,
264 #[source]
265 source: std::io::Error,
266 },
267 #[error(transparent)]
268 Survey(FilesystemFontSurveyError),
269 #[error("filesystem font resource limit at {path:?}: {source}")]
270 Limit {
271 path: PathBuf,
272 source: FilesystemFontLimitError,
273 },
274 #[error(transparent)]
275 InvalidContainers(FilesystemFontValidationError),
276}
277
278impl FilesystemFontReadError {
279 fn io(
280 operation: FilesystemFontOperation,
281 path: impl Into<PathBuf>,
282 source: std::io::Error,
283 ) -> Self {
284 Self::Io {
285 operation,
286 path: path.into(),
287 source,
288 }
289 }
290
291 fn limit(path: &Path, source: FilesystemFontLimitError) -> Self {
292 Self::Limit {
293 path: path.to_owned(),
294 source,
295 }
296 }
297}
298
299struct SelectedFont {
300 root: PathBuf,
301 boundary: PathBuf,
302 path: PathBuf,
303}
304
305enum PlannedSource {
306 Filesystem {
307 selected: Vec<SelectedFont>,
308 disposition: FontDisposition,
309 },
310 #[cfg(feature = "embedded-fonts")]
311 TypstEmbedded {
312 containers: Vec<FontContainer>,
313 disposition: FontDisposition,
314 },
315}
316
317#[derive(Default)]
318struct SurveyState {
319 visited_entries: u64,
320 accepted_containers: u64,
321 declared_total: u64,
322 issues: Vec<FilesystemFontIssue>,
323 deferred_limit: Option<FilesystemFontReadError>,
324}
325
326pub fn read_filesystem_fonts(
331 sources: impl IntoIterator<Item = FilesystemFontSource>,
332 limits: FilesystemFontLimits,
333) -> Result<FontCatalog, FilesystemFontReadError> {
334 let mut state = SurveyState::default();
335 let mut plans = Vec::new();
336
337 for source in sources {
338 match source.kind {
339 FilesystemFontSourceKind::System => {
340 let selected = survey_system_fonts(limits, &mut state)?;
341 plans.push(PlannedSource::Filesystem {
342 selected,
343 disposition: source.disposition,
344 });
345 }
346 #[cfg(feature = "embedded-fonts")]
347 FilesystemFontSourceKind::TypstEmbedded => {
348 let mut containers = Vec::new();
349 for container in typst_embedded_font_containers() {
350 containers.push(container);
351 }
352 plans.push(PlannedSource::TypstEmbedded {
353 containers,
354 disposition: source.disposition,
355 });
356 }
357 FilesystemFontSourceKind::Directory(root) => {
358 let selected = survey_root(&root, true, limits, &mut state)?;
359 plans.push(PlannedSource::Filesystem {
360 selected,
361 disposition: source.disposition,
362 });
363 }
364 }
365 }
366
367 if !state.issues.is_empty() {
368 state.issues.sort_by(|left, right| {
369 left.path()
370 .cmp(right.path())
371 .then_with(|| left.rank().cmp(&right.rank()))
372 });
373 return Err(FilesystemFontReadError::Survey(FilesystemFontSurveyError {
374 issues: state.issues,
375 }));
376 }
377 if let Some(error) = state.deferred_limit {
378 return Err(error);
379 }
380
381 let mut catalog = FontCatalog::new();
382 let mut actual_total = 0u64;
383 let mut invalid_containers = Vec::new();
384 for plan in plans {
385 match plan {
386 PlannedSource::Filesystem {
387 selected,
388 disposition,
389 } => {
390 for selected in selected {
391 let bytes = read_bounded(
392 &selected.root,
393 &selected.boundary,
394 &selected.path,
395 actual_total,
396 limits,
397 )?;
398 actual_total = checked_add(
399 actual_total,
400 bytes.len() as u64,
401 FilesystemFontResource::TotalAcceptedBytes,
402 )
403 .map_err(|source| FilesystemFontReadError::limit(&selected.path, source))?;
404 match FontContainer::new(bytes) {
405 Ok(container) => {
406 catalog.push(FontCatalogEntry::new(container, disposition));
407 }
408 Err(source) => invalid_containers.push(FilesystemFontContainerIssue {
409 path: selected.path,
410 source,
411 }),
412 }
413 }
414 }
415 #[cfg(feature = "embedded-fonts")]
416 PlannedSource::TypstEmbedded {
417 containers,
418 disposition,
419 } => {
420 for container in containers {
421 catalog.push(FontCatalogEntry::new(container, disposition));
422 }
423 }
424 }
425 }
426 if !invalid_containers.is_empty() {
427 invalid_containers.sort_by(|left, right| left.path.cmp(&right.path));
428 return Err(FilesystemFontReadError::InvalidContainers(
429 FilesystemFontValidationError {
430 issues: invalid_containers,
431 },
432 ));
433 }
434 Ok(catalog)
435}
436
437fn survey_system_fonts(
438 limits: FilesystemFontLimits,
439 state: &mut SurveyState,
440) -> Result<Vec<SelectedFont>, FilesystemFontReadError> {
441 let mut selected = Vec::new();
442 for root in system_font_roots() {
443 selected.extend(survey_root(&root, false, limits, state)?);
444 }
445
446 #[cfg(target_os = "macos")]
447 {
448 selected.extend(survey_macos_downloadable_fonts(limits, state)?);
449 for root in macos_system_font_roots_after_downloadable() {
450 selected.extend(survey_root(&root, false, limits, state)?);
451 }
452 }
453
454 Ok(selected)
455}
456
457#[cfg(target_os = "macos")]
458fn survey_macos_downloadable_fonts(
459 limits: FilesystemFontLimits,
460 state: &mut SurveyState,
461) -> Result<Vec<SelectedFont>, FilesystemFontReadError> {
462 let root = Path::new("/System/Library/AssetsV2");
463 let entries = match std::fs::read_dir(root) {
464 Ok(entries) => entries,
465 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
466 Err(error) => {
467 return Err(FilesystemFontReadError::io(
468 FilesystemFontOperation::SurveyEntry,
469 root,
470 error,
471 ));
472 }
473 };
474 let mut roots = Vec::new();
475 for entry in entries {
476 let entry = entry.map_err(|error| {
477 FilesystemFontReadError::io(FilesystemFontOperation::SurveyEntry, root, error)
478 })?;
479 state.visited_entries = checked_add(
480 state.visited_entries,
481 1,
482 FilesystemFontResource::VisitedEntries,
483 )
484 .map_err(|source| FilesystemFontReadError::limit(&entry.path(), source))?;
485 check_limit(
486 FilesystemFontResource::VisitedEntries,
487 limits.visited_entries(),
488 state.visited_entries,
489 )
490 .map_err(|source| FilesystemFontReadError::limit(&entry.path(), source))?;
491 if entry
492 .file_name()
493 .to_string_lossy()
494 .starts_with("com_apple_MobileAsset_Font")
495 {
496 roots.push(entry.path());
497 }
498 }
499 roots.sort();
500
501 let mut selected = Vec::new();
502 for root in roots {
503 selected.extend(survey_root(&root, false, limits, state)?);
504 }
505 Ok(selected)
506}
507
508fn survey_root(
509 root: &Path,
510 required: bool,
511 limits: FilesystemFontLimits,
512 state: &mut SurveyState,
513) -> Result<Vec<SelectedFont>, FilesystemFontReadError> {
514 let metadata = match std::fs::symlink_metadata(root) {
515 Ok(metadata) => metadata,
516 Err(error) if !required && error.kind() == std::io::ErrorKind::NotFound => {
517 return Ok(Vec::new());
518 }
519 Err(error) => {
520 return Err(FilesystemFontReadError::io(
521 FilesystemFontOperation::InspectRoot,
522 root,
523 error,
524 ));
525 }
526 };
527 if metadata.file_type().is_symlink() {
528 state.issues.push(FilesystemFontIssue::Alias {
529 path: root.to_owned(),
530 });
531 return Ok(Vec::new());
532 }
533 if !metadata.is_dir() {
534 state.issues.push(FilesystemFontIssue::RootNotDirectory {
535 path: root.to_owned(),
536 });
537 return Ok(Vec::new());
538 }
539 let boundary = std::fs::canonicalize(root).map_err(|source| {
540 FilesystemFontReadError::io(FilesystemFontOperation::InspectRoot, root, source)
541 })?;
542
543 let mut selected = Vec::new();
544 for entry in walkdir::WalkDir::new(root).follow_links(false) {
545 let entry = entry.map_err(|error| {
546 let path = error.path().unwrap_or(root).to_owned();
547 let source = error
548 .into_io_error()
549 .unwrap_or_else(|| std::io::Error::other("filesystem traversal failed"));
550 FilesystemFontReadError::io(FilesystemFontOperation::SurveyEntry, path, source)
551 })?;
552 if entry.depth() == 0 {
553 continue;
554 }
555
556 state.visited_entries = checked_add(
557 state.visited_entries,
558 1,
559 FilesystemFontResource::VisitedEntries,
560 )
561 .map_err(|source| FilesystemFontReadError::limit(entry.path(), source))?;
562 check_limit(
563 FilesystemFontResource::VisitedEntries,
564 limits.visited_entries(),
565 state.visited_entries,
566 )
567 .map_err(|source| FilesystemFontReadError::limit(entry.path(), source))?;
568
569 let file_type = entry.file_type();
570 if file_type.is_dir() {
571 continue;
572 }
573 if file_type.is_symlink() {
574 state.issues.push(FilesystemFontIssue::Alias {
575 path: entry.path().to_owned(),
576 });
577 continue;
578 }
579 if !font_eligible(entry.path()) {
580 continue;
581 }
582 if !file_type.is_file() {
583 state.issues.push(FilesystemFontIssue::UnsupportedEntry {
584 path: entry.path().to_owned(),
585 kind: UnsupportedEntry::of(&file_type).into(),
586 });
587 continue;
588 }
589
590 let metadata = entry.metadata().map_err(|error| {
591 let path = error.path().unwrap_or(entry.path()).to_owned();
592 let source = error
593 .into_io_error()
594 .unwrap_or_else(|| std::io::Error::other("failed to inspect Font Container"));
595 FilesystemFontReadError::io(FilesystemFontOperation::InspectContainer, path, source)
596 })?;
597 let accepted = account_container(entry.path(), metadata.len(), limits, state)?;
598 if accepted {
599 selected.push(SelectedFont {
600 root: root.to_owned(),
601 boundary: boundary.clone(),
602 path: entry.path().to_owned(),
603 });
604 }
605 }
606 selected.sort_by(|left, right| left.path.cmp(&right.path));
607 Ok(selected)
608}
609
610fn account_container(
611 path: &Path,
612 declared: u64,
613 limits: FilesystemFontLimits,
614 state: &mut SurveyState,
615) -> Result<bool, FilesystemFontReadError> {
616 state.accepted_containers = checked_add(
617 state.accepted_containers,
618 1,
619 FilesystemFontResource::AcceptedContainers,
620 )
621 .map_err(|source| FilesystemFontReadError::limit(path, source))?;
622 if let Err(source) = check_limit(
623 FilesystemFontResource::AcceptedContainers,
624 limits.accepted_containers(),
625 state.accepted_containers,
626 ) {
627 if state.deferred_limit.is_none() {
628 state.deferred_limit = Some(FilesystemFontReadError::limit(path, source));
629 }
630 return Ok(false);
631 }
632 if let Err(source) = check_limit(
633 FilesystemFontResource::ContainerBytes,
634 limits.container_bytes(),
635 declared,
636 ) {
637 if state.deferred_limit.is_none() {
638 state.deferred_limit = Some(FilesystemFontReadError::limit(path, source));
639 }
640 return Ok(false);
641 }
642 state.declared_total = checked_add(
643 state.declared_total,
644 declared,
645 FilesystemFontResource::TotalAcceptedBytes,
646 )
647 .map_err(|source| FilesystemFontReadError::limit(path, source))?;
648 if let Err(source) = check_limit(
649 FilesystemFontResource::TotalAcceptedBytes,
650 limits.total_accepted_bytes(),
651 state.declared_total,
652 ) {
653 if state.deferred_limit.is_none() {
654 state.deferred_limit = Some(FilesystemFontReadError::limit(path, source));
655 }
656 return Ok(false);
657 }
658 Ok(true)
659}
660
661fn read_bounded(
662 root: &Path,
663 boundary: &Path,
664 path: &Path,
665 total_before: u64,
666 limits: FilesystemFontLimits,
667) -> Result<Vec<u8>, FilesystemFontReadError> {
668 let total_allowance = limits.total_accepted_bytes().saturating_sub(total_before);
669 let allowance = limits.container_bytes().min(total_allowance);
670 let mut file = open_without_following(root, boundary, path)?;
671 let mut bytes = Vec::new();
672 file.by_ref()
673 .take(allowance + 1)
674 .read_to_end(&mut bytes)
675 .map_err(|error| {
676 FilesystemFontReadError::io(FilesystemFontOperation::ReadContainer, path, error)
677 })?;
678 let observed = u64::try_from(bytes.len()).map_err(|_| {
679 FilesystemFontReadError::limit(
680 path,
681 FilesystemFontLimitError::AccountingOverflow {
682 resource: FilesystemFontResource::ContainerBytes,
683 },
684 )
685 })?;
686 check_limit(
687 FilesystemFontResource::ContainerBytes,
688 limits.container_bytes(),
689 observed,
690 )
691 .map_err(|source| FilesystemFontReadError::limit(path, source))?;
692 let total = checked_add(
693 total_before,
694 observed,
695 FilesystemFontResource::TotalAcceptedBytes,
696 )
697 .map_err(|source| FilesystemFontReadError::limit(path, source))?;
698 check_limit(
699 FilesystemFontResource::TotalAcceptedBytes,
700 limits.total_accepted_bytes(),
701 total,
702 )
703 .map_err(|source| FilesystemFontReadError::limit(path, source))?;
704 Ok(bytes)
705}
706
707#[cfg(windows)]
708fn validate_windows_boundary(
709 file: &File,
710 boundary: &Path,
711 path: &Path,
712) -> Result<(), FilesystemFontReadError> {
713 use std::os::windows::ffi::OsStringExt;
714 use std::os::windows::io::AsRawHandle;
715 use windows_sys::Win32::Storage::FileSystem::GetFinalPathNameByHandleW;
716
717 let handle = file.as_raw_handle();
718 let required = unsafe { GetFinalPathNameByHandleW(handle, std::ptr::null_mut(), 0, 0) };
721 if required == 0 {
722 return Err(FilesystemFontReadError::io(
723 FilesystemFontOperation::ReadContainer,
724 path,
725 std::io::Error::last_os_error(),
726 ));
727 }
728 let mut buffer = vec![0u16; required as usize + 1];
729 let written =
732 unsafe { GetFinalPathNameByHandleW(handle, buffer.as_mut_ptr(), buffer.len() as u32, 0) };
733 if written == 0 || written as usize >= buffer.len() {
734 return Err(FilesystemFontReadError::io(
735 FilesystemFontOperation::ReadContainer,
736 path,
737 std::io::Error::last_os_error(),
738 ));
739 }
740 let opened = PathBuf::from(std::ffi::OsString::from_wide(&buffer[..written as usize]));
741 if !opened.starts_with(boundary) {
742 return Err(alias_error(path));
743 }
744 Ok(())
745}
746
747struct FontTraversal;
752
753impl TraversalPolicy for FontTraversal {
754 type Error = FilesystemFontReadError;
755
756 const ROOT_INVARIANT: &'static str = "a selected font path remains beneath its root";
757 const ALIASED_ROOT_IS_REFUSED: bool = true;
758
759 fn io(&self, path: &Path, source: std::io::Error) -> Self::Error {
760 FilesystemFontReadError::io(FilesystemFontOperation::ReadContainer, path, source)
761 }
762
763 fn alias(&self, path: &Path) -> Self::Error {
764 alias_error(path)
765 }
766
767 fn unsupported_entry(&self, path: &Path, entry: UnsupportedEntry) -> Self::Error {
768 FilesystemFontReadError::Survey(FilesystemFontSurveyError {
769 issues: vec![FilesystemFontIssue::UnsupportedEntry {
770 path: path.to_owned(),
771 kind: entry.into(),
772 }],
773 })
774 }
775}
776
777impl From<UnsupportedEntry> for FilesystemFontEntryKind {
778 fn from(entry: UnsupportedEntry) -> Self {
779 match entry {
780 UnsupportedEntry::Socket => Self::Socket,
781 UnsupportedEntry::Fifo => Self::Fifo,
782 UnsupportedEntry::BlockDevice => Self::BlockDevice,
783 UnsupportedEntry::CharacterDevice => Self::CharacterDevice,
784 UnsupportedEntry::Unknown => Self::Unknown,
785 }
786 }
787}
788
789fn open_without_following(
794 root: &Path,
795 boundary: &Path,
796 path: &Path,
797) -> Result<std::fs::File, FilesystemFontReadError> {
798 #[cfg(not(windows))]
799 let _ = boundary;
800
801 let file = fs_traversal::open_without_following(&FontTraversal, root, path)?;
802 #[cfg(windows)]
803 validate_windows_boundary(&file, boundary, path)?;
804 Ok(file)
805}
806
807fn alias_error(path: &Path) -> FilesystemFontReadError {
808 FilesystemFontReadError::Survey(FilesystemFontSurveyError {
809 issues: vec![FilesystemFontIssue::Alias {
810 path: path.to_owned(),
811 }],
812 })
813}
814
815fn font_eligible(path: &Path) -> bool {
816 path.extension()
817 .and_then(|extension| extension.to_str())
818 .is_some_and(crate::read_layout::is_font_container_extension)
819}
820
821fn checked_add(
822 total: u64,
823 value: u64,
824 resource: FilesystemFontResource,
825) -> Result<u64, FilesystemFontLimitError> {
826 total
827 .checked_add(value)
828 .ok_or(FilesystemFontLimitError::AccountingOverflow { resource })
829}
830
831fn check_limit(
832 resource: FilesystemFontResource,
833 ceiling: u64,
834 observed: u64,
835) -> Result<(), FilesystemFontLimitError> {
836 if observed > ceiling {
837 return Err(FilesystemFontLimitError::exceeded(resource, ceiling));
838 }
839 Ok(())
840}
841
842fn system_font_roots() -> Vec<PathBuf> {
843 let mut roots = Vec::new();
844
845 #[cfg(target_os = "windows")]
846 {
847 let system_root = std::env::var_os("SYSTEMROOT")
848 .map(PathBuf::from)
849 .unwrap_or_else(|| PathBuf::from(r"C:\Windows"));
850 roots.push(system_root.join("Fonts"));
851 if let Some(home) = std::env::var_os("USERPROFILE").map(PathBuf::from) {
852 roots.push(home.join(r"AppData\Local\Microsoft\Windows\Fonts"));
853 roots.push(home.join(r"AppData\Roaming\Microsoft\Windows\Fonts"));
854 }
855 if let Some(data) = std::env::var_os("APPDATA").map(PathBuf::from) {
856 roots.push(data.join("Adobe/CoreSync/plugins/livetype/r"));
857 roots.push(data.join("Adobe/User Owned Fonts"));
858 }
859 }
860
861 #[cfg(target_os = "macos")]
862 {
863 roots.extend([
864 PathBuf::from("/Library/Fonts"),
865 PathBuf::from("/System/Library/Fonts"),
866 ]);
867 }
868
869 #[cfg(target_os = "redox")]
870 roots.push(PathBuf::from("/ui/fonts"));
871
872 #[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
873 roots.extend(fontconfig_roots());
874
875 roots
876}
877
878#[cfg(target_os = "macos")]
879fn macos_system_font_roots_after_downloadable() -> Vec<PathBuf> {
880 let mut roots = vec![PathBuf::from("/Network/Library/Fonts")];
881 if let Some(home) = std::env::var_os("HOME").map(PathBuf::from) {
882 roots.push(home.join("Library/Fonts"));
883 let data = home.join("Library/Application Support/Adobe");
884 roots.push(data.join("CoreSync/plugins/livetype/.r"));
885 roots.push(data.join(".User Owned Fonts"));
886 }
887 roots
888}
889
890#[cfg(all(unix, not(any(target_os = "macos", target_os = "android"))))]
891fn fontconfig_roots() -> Vec<PathBuf> {
892 let mut config = fontconfig_parser::FontConfig::default();
893 let home = std::env::var_os("HOME").map(PathBuf::from);
894 if let Some(config_file) = std::env::var_os("FONTCONFIG_FILE") {
895 let _ = config.merge_config(Path::new(&config_file));
896 } else {
897 let user_config = std::env::var_os("XDG_CONFIG_HOME")
898 .map(PathBuf::from)
899 .or_else(|| home.as_ref().map(|home| home.join(".config")));
900 let read_global = user_config.is_none_or(|path| {
901 config
902 .merge_config(&path.join("fontconfig/fonts.conf"))
903 .is_err()
904 });
905 if read_global {
906 let _ = config.merge_config(Path::new("/etc/fonts/local.conf"));
907 }
908 let _ = config.merge_config(Path::new("/etc/fonts/fonts.conf"));
909 }
910
911 let mut roots = config
912 .dirs
913 .into_iter()
914 .filter_map(|directory| {
915 if directory.path.starts_with("~") {
916 home.as_ref()
917 .map(|home| home.join(directory.path.strip_prefix("~").unwrap()))
918 } else {
919 Some(directory.path)
920 }
921 })
922 .collect::<Vec<_>>();
923 if roots.is_empty() {
924 roots.extend([
925 PathBuf::from("/usr/share/fonts"),
926 PathBuf::from("/usr/local/share/fonts"),
927 ]);
928 if let Some(home) = home {
929 roots.push(home.join(".fonts"));
930 roots.push(home.join(".local/share/fonts"));
931 }
932 }
933 roots
934}
935
936#[cfg(test)]
937mod tests {
938 use super::*;
939
940 #[test]
941 fn accepted_container_accounting_overflow_is_typed() {
942 assert_eq!(
943 checked_add(u64::MAX, 1, FilesystemFontResource::AcceptedContainers),
944 Err(FilesystemFontLimitError::AccountingOverflow {
945 resource: FilesystemFontResource::AcceptedContainers,
946 })
947 );
948 }
949
950 #[cfg(unix)]
951 #[test]
952 fn bounded_reads_do_not_follow_an_alias_created_after_survey() {
953 use std::os::unix::fs::symlink;
954
955 let directory = tempfile::tempdir().unwrap();
956 let outside = directory.path().join("outside.ttf");
957 let selected = directory.path().join("selected.ttf");
958 let boundary = directory.path().canonicalize().unwrap();
959 std::fs::write(&outside, b"outside").unwrap();
960 symlink(&outside, &selected).unwrap();
961
962 let error = read_bounded(
963 directory.path(),
964 &boundary,
965 &selected,
966 0,
967 FilesystemFontLimits::new(1, 1, 16, 16),
968 )
969 .unwrap_err();
970
971 assert!(matches!(
972 error,
973 FilesystemFontReadError::Survey(ref survey)
974 if matches!(survey.issues(), [FilesystemFontIssue::Alias { path }] if path == &selected)
975 ));
976 }
977
978 #[cfg(unix)]
979 #[test]
980 fn bounded_reads_do_not_follow_a_root_alias_created_after_survey() {
981 use std::os::unix::fs::symlink;
982
983 let directory = tempfile::tempdir().unwrap();
984 let root = directory.path().join("fonts");
985 let outside = directory.path().join("outside");
986 let selected = root.join("selected.ttf");
987 std::fs::create_dir(&root).unwrap();
988 std::fs::create_dir(&outside).unwrap();
989 std::fs::write(&selected, b"surveyed").unwrap();
990 std::fs::write(outside.join("selected.ttf"), b"outside").unwrap();
991 let boundary = root.canonicalize().unwrap();
992 std::fs::remove_dir_all(&root).unwrap();
993 symlink(&outside, &root).unwrap();
994
995 let error = read_bounded(
996 &root,
997 &boundary,
998 &selected,
999 0,
1000 FilesystemFontLimits::new(1, 1, 16, 16),
1001 )
1002 .unwrap_err();
1003
1004 assert!(matches!(
1005 error,
1006 FilesystemFontReadError::Survey(ref survey)
1007 if matches!(survey.issues(), [FilesystemFontIssue::Alias { path }] if path == &root)
1008 ));
1009 }
1010}