freedesktop_desktop_entry/
iter.rs

1// Copyright 2021 System76 <info@system76.com>
2// SPDX-License-Identifier: MPL-2.0
3
4use 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    /// Directories will be processed in order.
20    #[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                                    // Pre-sort the walked directories as order of parsing affects appid matches.
45                                    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                            // Skip directories_to_walk which could not be read or that were
57                            // already visited
58                            _ => 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                        // Skip visited directories to mitigate against file system loops
81                        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        // File hierarchy
122        // Directory 'a'
123        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        // Directory 'b'
131        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        // Files in root
136        let file_e = root.join("e.desktop");
137
138        // Write entries for each file
139        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        // Hierarchy with an infinite loop
163        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        // Sanity check that we have a loop
169        assert_eq!(
170            fs::canonicalize(root).unwrap(),
171            fs::canonicalize(&dir).unwrap(),
172            "Expected a loop where {dir:?} points to {root:?}"
173        );
174
175        // Now we need a fake desktop entry that will be yielded endlessly with a broken iter
176        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        // Finally, check that the iterator is eventually exhausted
181        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}