Skip to main content

typg_core/
discovery.rs

1/// The neighborhood explorer who knows where all the fonts are hiding
2///
3/// Think of us as the friendly neighborhood font scout who goes door-to-door
4/// finding every font that calls your filesystem home. We'll climb through
5/// directory trees, peek into folder corners, and come back with a complete
6/// census of all the typographic residents in your chosen neighborhoods.
7///
8/// Whether fonts are living openly in plain sight or hiding in nested
9/// subdirectories, we'll find them. We're even brave enough to follow
10/// symlinks if you give us permission - those mysterious pathways often
11/// lead to the most interesting font discoveries.
12///
13/// Made with adventurous spirit at FontLab https://www.fontlab.com/
14use std::path::{Path, PathBuf};
15
16use anyhow::{anyhow, Result};
17use walkdir::WalkDir;
18
19/// A business card for every font we meet on our explorations
20///
21/// When we find a font during our neighborhood adventures, we give it
22/// this simple calling card that tells you exactly where it lives. No
23/// frills, no fuss - just the perfect address for later visits.
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct TypgFontSourceRef {
26    /// The exact street address where this font hangs out
27    pub path: PathBuf,
28}
29
30/// The explorer's contract - how we promise to find fonts for you
31///
32/// Any font discovery service must be brave enough to venture into
33/// the unknown and return with tales of all the fonts they met.
34/// Whether they're crawling filesystems, consulting databases, or
35/// reading cache indices, they all come back with the same treasure:
36/// a list of fonts waiting to be discovered.
37pub trait FontDiscovery {
38    /// Sets out on expedition and returns with all the fonts found
39    fn discover(&self) -> Result<Vec<TypgFontSourceRef>>;
40}
41
42/// The brave filesystem explorer who never says no to an adventure
43///
44/// We're the expedition leaders who'll climb any directory tree,
45/// cross any symlink bridge, and search every nook and cranny for
46/// typographic treasures. Give us a list of neighborhoods to explore
47/// and we'll come back with a complete census of every font that
48/// calls those places home.
49#[derive(Debug, Clone)]
50pub struct PathDiscovery {
51    /// The starting points for our font-finding expeditions
52    roots: Vec<PathBuf>,
53    /// Should we be brave enough to follow those mysterious symlink shortcuts?
54    follow_symlinks: bool,
55}
56
57impl PathDiscovery {
58    /// Assembles our expedition team and maps out our adventure route
59    ///
60    /// Give us your list of neighborhoods to explore and we'll prepare
61    /// our expedition kit. By default, we play it safe and stick to the
62    /// beaten path - no mysterious symlink shortcuts unless you say so.
63    pub fn new<I, P>(roots: I) -> Self
64    where
65        I: IntoIterator<Item = P>,
66        P: Into<PathBuf>,
67    {
68        let roots = roots.into_iter().map(Into::into).collect();
69        Self {
70            roots,
71            follow_symlinks: false,
72        }
73    }
74
75    /// Decides whether we're brave enough to follow mysterious shortcuts
76    ///
77    /// Symlinks are like teleportation portals in the filesystem - they
78    /// can lead to wondrous discoveries or endless loops. We'll follow them
79    /// if you're feeling adventurous, but we're happy to stay on solid ground
80    /// if you prefer the conservative approach.
81    pub fn follow_symlinks(mut self, follow: bool) -> Self {
82        self.follow_symlinks = follow;
83        self
84    }
85}
86
87impl FontDiscovery for PathDiscovery {
88    /// Sets out on our grand font-finding expedition through the filesystem jungle
89    ///
90    /// We'll visit every neighborhood on our map, climb directory trees with
91    /// the agility of a seasoned explorer, and carefully examine every file
92    /// we encounter. Only the true typographic treasures get added to our
93    /// collection - we're discerning explorers who know quality when we see it.
94    ///
95    /// Returns: A complete catalog of every font we discovered on our adventure.
96    fn discover(&self) -> Result<Vec<TypgFontSourceRef>> {
97        let mut found = Vec::new();
98
99        for root in &self.roots {
100            if !root.exists() {
101                return Err(anyhow!("root path does not exist: {}", root.display()));
102            }
103
104            for entry in WalkDir::new(root).follow_links(self.follow_symlinks) {
105                let entry = entry?;
106                if entry.file_type().is_file() && is_font(entry.path()) {
107                    found.push(TypgFontSourceRef {
108                        path: entry.path().to_path_buf(),
109                    });
110                }
111            }
112        }
113
114        Ok(found)
115    }
116}
117
118/// The expert detective who can spot a font from just its file extension
119///
120/// We've seen thousands of fonts in our day, and we've learned to
121/// recognize them by their distinctive signatures. TTF, OTF, TTC, OTC -
122/// we know them all. Case doesn't matter to us - we're equal-opportunity
123/// font identifiers who believe every font deserves to be discovered.
124///
125/// Returns true if this extension belongs to a legitimate format.
126fn is_font(path: &Path) -> bool {
127    let ext = match path.extension().and_then(|e| e.to_str()) {
128        Some(ext) => ext.to_ascii_lowercase(),
129        None => return false,
130    };
131
132    matches!(ext.as_str(), "ttf" | "otf" | "ttc" | "otc")
133}
134
135#[cfg(test)]
136mod tests {
137    use super::is_font;
138    use super::FontDiscovery;
139    use super::PathDiscovery;
140    use std::fs;
141    use tempfile::tempdir;
142
143    #[test]
144    fn recognises_font_extensions() {
145        assert!(is_font("/A/B/font.ttf".as_ref()));
146        assert!(is_font("/A/B/font.OTF".as_ref()));
147        assert!(!is_font("/A/B/font.txt".as_ref()));
148        assert!(!is_font("/A/B/font".as_ref()));
149    }
150
151    #[test]
152    fn discovers_nested_fonts() {
153        let tmp = tempdir().expect("tempdir");
154        let nested = tmp.path().join("a/b");
155        fs::create_dir_all(&nested).expect("mkdir");
156        let font_path = nested.join("sample.ttf");
157        fs::write(&font_path, b"").expect("touch font");
158
159        let discovery = PathDiscovery::new([tmp.path()]);
160        let fonts = discovery.discover().expect("discover");
161
162        assert!(fonts.iter().any(|f| f.path == font_path));
163    }
164
165    #[cfg(unix)]
166    #[test]
167    fn follows_symlinks_when_enabled() {
168        use std::os::unix::fs::symlink;
169
170        let tmp = tempdir().expect("tempdir");
171        let real_dir = tmp.path().join("real");
172        let link_dir = tmp.path().join("link");
173        fs::create_dir_all(&real_dir).expect("mkdir real");
174        let font_path = real_dir.join("linked.otf");
175        fs::write(&font_path, b"").expect("touch font");
176        symlink(&real_dir, &link_dir).expect("symlink");
177
178        let discovery = PathDiscovery::new([&link_dir]).follow_symlinks(true);
179        let fonts = discovery.discover().expect("discover");
180
181        assert!(fonts.iter().any(|f| f.path.ends_with("linked.otf")));
182    }
183}