freedesktop_desktop_entry/
iter.rs1use std::{
5 collections::{BTreeSet, VecDeque},
6 fs,
7 path::PathBuf,
8};
9
10use crate::DesktopEntry;
11
12pub struct Iter {
13 directories_to_walk: VecDeque<PathBuf>,
14 actively_walking: Option<VecDeque<PathBuf>>,
15 visited: BTreeSet<PathBuf>,
16}
17
18impl Iter {
19 #[inline]
21 pub fn new<I: Iterator<Item = PathBuf>>(directories_to_walk: I) -> Self {
22 Self {
23 directories_to_walk: directories_to_walk.collect(),
24 actively_walking: None,
25 visited: BTreeSet::default(),
26 }
27 }
28}
29
30impl Iterator for Iter {
31 type Item = PathBuf;
32
33 fn next(&mut self) -> Option<Self::Item> {
34 'outer: loop {
35 let mut paths = match self.actively_walking.take() {
36 Some(dir) => dir,
37 None => {
38 while let Some(mut path) = self.directories_to_walk.pop_front() {
39 path = path.canonicalize().map_or(path, |canonical| canonical);
40 self.visited.insert(path.clone());
41 match fs::read_dir(&path) {
42 Ok(dir) => {
43 self.actively_walking = Some({
44 let mut entries = dir
46 .filter_map(Result::ok)
47 .map(|entry| entry.path())
48 .collect::<VecDeque<_>>();
49 entries.make_contiguous().sort_unstable();
50 entries
51 });
52
53 continue 'outer;
54 }
55
56 _ => continue,
59 }
60 }
61
62 return None;
63 }
64 };
65
66 'inner: while let Some(mut path) = paths.pop_front() {
67 if !path.exists() {
68 continue 'inner;
69 }
70
71 if path.is_dir() {
72 path = match path.canonicalize() {
73 Ok(canonicalized) => canonicalized,
74 Err(_) => continue 'inner,
75 };
76 }
77
78 if let Ok(metadata) = path.metadata() {
79 if metadata.is_dir() {
80 if self.visited.insert(path.clone()) {
82 self.directories_to_walk.push_front(path);
83 }
84 } else if metadata.is_file()
85 && path.extension().is_some_and(|ext| ext == "desktop")
86 {
87 self.actively_walking = Some(paths);
88 return Some(path);
89 }
90 }
91 }
92 }
93 }
94}
95
96impl Iter {
97 #[inline]
98 pub fn entries<'i, 'l: 'i, L>(
99 self,
100 locales_filter: Option<&'l [L]>,
101 ) -> impl Iterator<Item = DesktopEntry> + 'i
102 where
103 L: AsRef<str>,
104 {
105 self.map(move |path| DesktopEntry::from_path(path, locales_filter))
106 .filter_map(|e| e.ok())
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use std::{fs, os::unix};
113
114 use super::{DesktopEntry, Iter};
115
116 #[test]
117 fn iter_yields_all_entries() {
118 let temp = tempfile::tempdir().unwrap();
119 let root = temp.path();
120
121 let dir_a = root.join("a");
124 let dir_a_a = dir_a.join("aa");
125 fs::create_dir_all(&dir_a_a).unwrap();
126 let file_a = dir_a.join("a.desktop");
127 let file_b = dir_a.join("b.desktop");
128 let file_c = dir_a_a.join("c.desktop");
129
130 let dir_b_bb_bbb = root.join("b/bb/bbb");
132 fs::create_dir_all(&dir_b_bb_bbb).unwrap();
133 let file_d = dir_b_bb_bbb.join("d.desktop");
134
135 let file_e = root.join("e.desktop");
137
138 let all_files = [file_a, file_b, file_c, file_d, file_e];
140 for file in &all_files {
141 let (name, _) = file
142 .file_name()
143 .unwrap()
144 .to_str()
145 .unwrap()
146 .split_once('.')
147 .unwrap();
148 fs::write(file, DesktopEntry::from_appid(name.to_string()).to_string()).unwrap();
149 }
150
151 let written_entries = Iter::new(std::iter::once(root.to_owned())).collect::<Vec<_>>();
152
153 eprintln!("expected: {all_files:?}\nactual: {written_entries:?}");
154 assert!(all_files.len() == written_entries.len());
155 for entry in written_entries {
156 assert!(all_files.contains(&entry));
157 }
158 }
159
160 #[test]
161 fn iter_no_infinite_loop() {
162 let temp = tempfile::tempdir().unwrap();
164 let root = temp.path();
165 let dir = root.join("loop");
166 unix::fs::symlink(root, &dir).expect("Linking {dir:?} to {root:?}");
167
168 assert_eq!(
170 fs::canonicalize(root).unwrap(),
171 fs::canonicalize(&dir).unwrap(),
172 "Expected a loop where {dir:?} points to {root:?}"
173 );
174
175 let entry = DesktopEntry::from_appid("joshfakeapp123".into());
177 let entry_path = root.join("joshfakeapp123.desktop");
178 fs::write(&entry_path, entry.to_string()).expect("Writing entry: {entry_path:?}");
179
180 for (i, de) in Iter::new(
182 fs::read_dir(root)
183 .unwrap()
184 .map(|entry| entry.unwrap().path()),
185 )
186 .entries(Option::<&[&str]>::None)
187 .enumerate()
188 {
189 assert_eq!(entry.appid, de.appid);
190 if i > 0 {
191 panic!("infinite loop");
192 }
193 }
194 }
195}