Skip to main content

typst_pack/
fs_fonts.rs

1//! Font Catalog reading for the reference filesystem Font Authority.
2
3#[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
17/// A resource bounded during filesystem Font Catalog reading.
18pub 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
28/// A filesystem font source exceeded a mandatory reading ceiling.
29pub type FilesystemFontLimitError = LimitError<FilesystemFontResource>;
30
31/// Mandatory finite resource ceilings for filesystem Font Catalog reading.
32pub type FilesystemFontLimits = Limits<FilesystemFontResource>;
33
34impl Limits<FilesystemFontResource> {
35    /// Constructs validated mandatory finite reading ceilings.
36    #[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    /// The first-party limits for filesystem font sources.
61    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/// One explicitly configured source of Font Containers.
91#[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    /// Selects the host's standard system font directories at this position.
107    pub fn system(disposition: FontDisposition) -> Self {
108        Self {
109            kind: FilesystemFontSourceKind::System,
110            disposition,
111        }
112    }
113
114    /// Selects Typst's compiled-in Font Containers at this position.
115    #[cfg(feature = "embedded-fonts")]
116    pub fn typst_embedded(disposition: FontDisposition) -> Self {
117        Self {
118            kind: FilesystemFontSourceKind::TypstEmbedded,
119            disposition,
120        }
121    }
122
123    /// Selects every eligible Font Container beneath one directory.
124    pub fn directory(path: impl Into<PathBuf>, disposition: FontDisposition) -> Self {
125        Self {
126            kind: FilesystemFontSourceKind::Directory(path.into()),
127            disposition,
128        }
129    }
130
131    /// The disposition every catalog position from this source carries.
132    pub fn disposition(&self) -> FontDisposition {
133        self.disposition
134    }
135}
136
137/// The kind of an eligible filesystem entry that cannot become a Font Container.
138#[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/// The filesystem operation that failed while reading a Font Catalog.
149#[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/// One independently detectable filesystem font survey issue.
170#[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/// All safely detectable issues found by one filesystem font survey.
203#[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/// One selected filesystem entry whose bytes are not a valid Font Container.
220#[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    /// The selected filesystem path whose exact bytes failed validation.
229    pub fn path(&self) -> &Path {
230        &self.path
231    }
232
233    /// The authoritative Font Container validation failure.
234    pub fn source(&self) -> FontContainerError {
235        self.source
236    }
237}
238
239/// Every invalid Font Container found while validating selected entries.
240#[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/// A failure while reading a Font Catalog from configured filesystem sources.
257#[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
326/// Reads one ordered Font Catalog from explicitly configured sources.
327///
328/// Sources compose in iterator order. Paths within one scanned root compose in
329/// lexical order, and no system or embedded source is added implicitly.
330pub 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    // SAFETY: `handle` remains owned by `file`; a null output buffer requests
719    // the required UTF-16 length and writes no bytes.
720    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    // SAFETY: `buffer` has the size reported by the first call and `handle`
730    // remains valid for the duration of this call.
731    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
747/// The font reader's vocabulary for the shared symlink-refusing open.
748///
749/// Font roots come from host configuration rather than from an already
750/// established project or package root, so an aliased root is refused here.
751struct 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
789/// Opens one selected font container beneath `root`.
790///
791/// Windows exposes no open-time no-follow flag, so the opened handle is
792/// additionally confirmed to resolve inside `boundary`.
793fn 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}