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(e) => {
75 eprintln!("warning: {e}");
76 continue;
77 }
78 };
79 if entry.file_type().is_file() && is_font(entry.path()) {
80 found.push(TypgFontSourceRef {
81 path: entry.path().to_path_buf(),
82 });
83 }
84 }
85 }
86
87 Ok(found)
88 }
89}
90
91fn is_font(path: &Path) -> bool {
93 let ext = match path.extension().and_then(|e| e.to_str()) {
94 Some(ext) => ext.to_ascii_lowercase(),
95 None => return false,
96 };
97
98 matches!(ext.as_str(), "ttf" | "otf" | "ttc" | "otc")
99}
100
101#[cfg(test)]
102mod tests {
103 use super::is_font;
104 use super::FontDiscovery;
105 use super::PathDiscovery;
106 use std::fs;
107 use tempfile::tempdir;
108
109 #[test]
110 fn recognises_font_extensions() {
111 assert!(is_font("/A/B/font.ttf".as_ref()));
112 assert!(is_font("/A/B/font.OTF".as_ref()));
113 assert!(!is_font("/A/B/font.txt".as_ref()));
114 assert!(!is_font("/A/B/font".as_ref()));
115 }
116
117 #[test]
118 fn discovers_nested_fonts() {
119 let tmp = tempdir().expect("tempdir");
120 let nested = tmp.path().join("a/b");
121 fs::create_dir_all(&nested).expect("mkdir");
122 let font_path = nested.join("sample.ttf");
123 fs::write(&font_path, b"").expect("touch font");
124
125 let discovery = PathDiscovery::new([tmp.path()]);
126 let fonts = discovery.discover().expect("discover");
127
128 assert!(fonts.iter().any(|f| f.path == font_path));
129 }
130
131 #[cfg(unix)]
132 #[test]
133 fn follows_symlinks_when_enabled() {
134 use std::os::unix::fs::symlink;
135
136 let tmp = tempdir().expect("tempdir");
137 let real_dir = tmp.path().join("real");
138 let link_dir = tmp.path().join("link");
139 fs::create_dir_all(&real_dir).expect("mkdir real");
140 let font_path = real_dir.join("linked.otf");
141 fs::write(&font_path, b"").expect("touch font");
142 symlink(&real_dir, &link_dir).expect("symlink");
143
144 let discovery = PathDiscovery::new([&link_dir]).follow_symlinks(true);
145 let fonts = discovery.discover().expect("discover");
146
147 assert!(fonts.iter().any(|f| f.path.ends_with("linked.otf")));
148 }
149}