1use std::path::{Path, PathBuf};
8
9use anyhow::{anyhow, Result};
10use walkdir::WalkDir;
11
12#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct TypgFontSourceRef {
15 pub path: PathBuf,
17}
18
19pub trait FontDiscovery {
21 fn discover(&self) -> Result<Vec<TypgFontSourceRef>>;
23}
24
25#[derive(Debug, Clone)]
30pub struct PathDiscovery {
31 roots: Vec<PathBuf>,
33 follow_symlinks: bool,
35}
36
37impl PathDiscovery {
38 pub fn new<I, P>(roots: I) -> Self
40 where
41 I: IntoIterator<Item = P>,
42 P: Into<PathBuf>,
43 {
44 let roots = roots.into_iter().map(Into::into).collect();
45 Self {
46 roots,
47 follow_symlinks: false,
48 }
49 }
50
51 pub fn follow_symlinks(mut self, follow: bool) -> Self {
53 self.follow_symlinks = follow;
54 self
55 }
56}
57
58impl FontDiscovery for PathDiscovery {
59 fn discover(&self) -> Result<Vec<TypgFontSourceRef>> {
64 let mut found = Vec::new();
65
66 for root in &self.roots {
67 if !root.exists() {
68 return Err(anyhow!("path does not exist: {}", root.display()));
69 }
70
71 for entry in WalkDir::new(root).follow_links(self.follow_symlinks) {
72 let entry = match entry {
73 Ok(e) => e,
74 Err(_) => {
75 continue;
76 }
77 };
78 if entry.file_type().is_file() && is_font(entry.path()) {
79 found.push(TypgFontSourceRef {
80 path: entry.path().to_path_buf(),
81 });
82 }
83 }
84 }
85
86 Ok(found)
87 }
88}
89
90fn is_font(path: &Path) -> bool {
92 let ext = match path.extension().and_then(|e| e.to_str()) {
93 Some(ext) => ext.to_ascii_lowercase(),
94 None => return false,
95 };
96
97 matches!(ext.as_str(), "ttf" | "otf" | "ttc" | "otc")
98}
99
100#[cfg(test)]
101mod tests {
102 use super::is_font;
103 use super::FontDiscovery;
104 use super::PathDiscovery;
105 use std::fs;
106 use tempfile::tempdir;
107
108 #[test]
109 fn recognises_font_extensions() {
110 assert!(is_font("/A/B/font.ttf".as_ref()));
111 assert!(is_font("/A/B/font.OTF".as_ref()));
112 assert!(!is_font("/A/B/font.txt".as_ref()));
113 assert!(!is_font("/A/B/font".as_ref()));
114 }
115
116 #[test]
117 fn discovers_nested_fonts() {
118 let tmp = tempdir().expect("tempdir");
119 let nested = tmp.path().join("a/b");
120 fs::create_dir_all(&nested).expect("mkdir");
121 let font_path = nested.join("sample.ttf");
122 fs::write(&font_path, b"").expect("touch font");
123
124 let discovery = PathDiscovery::new([tmp.path()]);
125 let fonts = discovery.discover().expect("discover");
126
127 assert!(fonts.iter().any(|f| f.path == font_path));
128 }
129
130 #[cfg(unix)]
131 #[test]
132 fn follows_symlinks_when_enabled() {
133 use std::os::unix::fs::symlink;
134
135 let tmp = tempdir().expect("tempdir");
136 let real_dir = tmp.path().join("real");
137 let link_dir = tmp.path().join("link");
138 fs::create_dir_all(&real_dir).expect("mkdir real");
139 let font_path = real_dir.join("linked.otf");
140 fs::write(&font_path, b"").expect("touch font");
141 symlink(&real_dir, &link_dir).expect("symlink");
142
143 let discovery = PathDiscovery::new([&link_dir]).follow_symlinks(true);
144 let fonts = discovery.discover().expect("discover");
145
146 assert!(fonts.iter().any(|f| f.path.ends_with("linked.otf")));
147 }
148}