diff --git a/src/lib.rs b/src/lib.rs index 5353ad9..71a55b1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -56,14 +56,15 @@ use theme::BASE_PATHS; use crate::cache::{CACHE, CacheEntry}; use crate::theme::{THEMES, Theme, try_build_icon_path}; +use std::ffi::OsStr; use std::hash::{Hash, Hasher}; use std::io::BufRead; use std::ops::ControlFlow; use std::path::PathBuf; -use std::time::Instant; mod cache; mod theme; +mod walk_dir; /// Return the list of installed themes on the system /// @@ -274,29 +275,48 @@ impl<'a> LookupBuilder<'a> { } if !self.extra_paths.is_empty() { - let extensions = if self.force_svg { - [".svg", ".png", ".xpm"] - } else { - [".png", ".svg", ".xpm"] - }; - let mut name_buf = String::new(); + let mut svg_path = None; + let mut png_path = None; + let mut xpm_path = None; - let result = extensions - .into_iter() - .try_for_each(|ext| { - self.extra_paths.iter().try_for_each(|dir| { - let mut path = dir.clone(); - if try_build_icon_path(&mut path, &mut name_buf, self.name, ext) { - return ControlFlow::Break(path); + for file_path in walk_dir::Iter::new(self.extra_paths.iter().cloned()) { + if let Some(file_name) = file_path.file_stem().and_then(OsStr::to_str) + && file_name != self.name + { + continue; + } + + if let Some(this_ext) = file_path.extension().and_then(OsStr::to_str) { + match this_ext { + "svg" => { + svg_path = Some(file_path); + if self.force_svg || png_path.is_some() { + break; + } } - name_buf.clear(); - ControlFlow::Continue(()) - }) - }) - .break_value(); - if result.is_some() { - return result; + "png" => { + png_path = Some(file_path); + if !self.force_svg || svg_path.is_some() { + break; + } + } + + "xpm" => { + xpm_path = Some(file_path); + } + + _ => (), + } + } + } + + if let Some(path) = if self.force_svg { + svg_path.or(png_path).or(xpm_path) + } else { + png_path.or(svg_path).or(xpm_path) + } { + return Some(path); } } diff --git a/src/walk_dir.rs b/src/walk_dir.rs new file mode 100644 index 0000000..9b6df97 --- /dev/null +++ b/src/walk_dir.rs @@ -0,0 +1,100 @@ +// Copyright 2026 System76 +// SPDX-License-Identifier: MIT + +//! Search for files within multiple directories. Follows symlinks, avoids loops, and +//! limits the max depth to 5. + +use std::{ + collections::{BTreeSet, VecDeque}, + fs, + path::PathBuf, +}; + +const MAX_DEPTH: usize = 5; + +pub struct Iter { + directories_to_walk: VecDeque<(PathBuf, usize)>, + actively_walking: Option>, + visited: BTreeSet, +} + +impl Iter { + /// Directories will be processed in order. + #[inline] + pub fn new>(directories_to_walk: I) -> Self { + Self { + directories_to_walk: directories_to_walk.map(|dir| (dir, 0)).collect(), + actively_walking: None, + visited: BTreeSet::default(), + } + } +} + +impl Iterator for Iter { + type Item = PathBuf; + + fn next(&mut self) -> Option { + 'outer: loop { + let mut paths = match self.actively_walking.take() { + Some(dir) => dir, + None => { + while let Some((mut path, depth)) = self.directories_to_walk.pop_front() { + path = path.canonicalize().map_or(path, |canonical| canonical); + self.visited.insert(path.clone()); + match fs::read_dir(&path) { + Ok(dir) => { + self.actively_walking = Some({ + // Pre-sort the walked directories as order of parsing affects appid matches. + let mut entries = dir + .filter_map(Result::ok) + .map(|entry| (entry.path(), depth)) + .collect::>(); + entries.make_contiguous().sort_unstable(); + entries + }); + + continue 'outer; + } + + // Skip directories_to_walk which could not be read or that were already visited + _ => continue, + } + } + + return None; + } + }; + + 'inner: while let Some((mut path, mut depth)) = paths.pop_front() { + if !path.exists() { + continue 'inner; + } + + if path.is_dir() { + depth += 1; + + if MAX_DEPTH == depth { + continue; + } + + path = match path.canonicalize() { + Ok(canonicalized) => canonicalized, + Err(_) => continue 'inner, + }; + } + + if let Ok(metadata) = path.metadata() { + if metadata.is_dir() { + // Skip visited directories to mitigate against file system loops + if self.visited.insert(path.clone()) { + self.directories_to_walk.push_front((path, depth)); + } + } else if metadata.is_file() { + self.actively_walking = Some(paths); + return Some(path); + } + } + } + } + } +}