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(path) = self.directories_to_walk.pop_front() {
39                        match fs::read_dir(&path) {
40                            Ok(dir) if self.visited.insert(path.clone()) => {
41                                self.actively_walking = Some({
42                                    // Pre-sort the walked directories as order of parsing affects appid matches.
43                                    let mut entries = dir
44                                        .filter_map(Result::ok)
45                                        .map(|entry| entry.path())
46                                        .collect::<VecDeque<_>>();
47                                    entries.make_contiguous().sort_unstable();
48                                    entries
49                                });
50
51                                continue 'outer;
52                            }
53
54                            // Skip directories_to_walk which could not be read or that were
55                            // already visited
56                            _ => continue,
57                        }
58                    }
59
60                    return None;
61                }
62            };
63
64            'inner: while let Some(mut path) = paths.pop_front() {
65                path = match path.canonicalize() {
66                    Ok(canonicalized) => canonicalized,
67                    Err(_) => continue 'inner,
68                };
69
70                if let Ok(metadata) = path.metadata() {
71                    if metadata.is_dir() {
72                        // Skip visited directories to mitigate against file system loops
73                        if self.visited.insert(path.clone()) {
74                            self.directories_to_walk.push_front(path);
75                        }
76                    } else if metadata.is_file()
77                        && path.extension().is_some_and(|ext| ext == "desktop")
78                    {
79                        self.actively_walking = Some(paths);
80                        return Some(path);
81                    }
82                }
83            }
84        }
85    }
86}
87
88impl Iter {
89    #[inline]
90    pub fn entries<'i, 'l: 'i, L>(
91        self,
92        locales_filter: Option<&'l [L]>,
93    ) -> impl Iterator<Item = DesktopEntry> + 'i
94    where
95        L: AsRef<str>,
96    {
97        self.map(move |path| DesktopEntry::from_path(path, locales_filter))
98            .filter_map(|e| e.ok())
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use std::{fs, os::unix};
105
106    use super::{DesktopEntry, Iter};
107
108    #[test]
109    fn iter_yields_all_entries() {
110        let temp = tempfile::tempdir().unwrap();
111        let root = temp.path();
112
113        // File hierarchy
114        // Directory 'a'
115        let dir_a = root.join("a");
116        let dir_a_a = dir_a.join("aa");
117        fs::create_dir_all(&dir_a_a).unwrap();
118        let file_a = dir_a.join("a.desktop");
119        let file_b = dir_a.join("b.desktop");
120        let file_c = dir_a_a.join("c.desktop");
121
122        // Directory 'b'
123        let dir_b_bb_bbb = root.join("b/bb/bbb");
124        fs::create_dir_all(&dir_b_bb_bbb).unwrap();
125        let file_d = dir_b_bb_bbb.join("d.desktop");
126
127        // Files in root
128        let file_e = root.join("e.desktop");
129
130        // Write entries for each file
131        let all_files = [file_a, file_b, file_c, file_d, file_e];
132        for file in &all_files {
133            let (name, _) = file
134                .file_name()
135                .unwrap()
136                .to_str()
137                .unwrap()
138                .split_once('.')
139                .unwrap();
140            fs::write(file, DesktopEntry::from_appid(name.to_string()).to_string()).unwrap();
141        }
142
143        let mut iter = Iter::new(
144            fs::read_dir(root)
145                .unwrap()
146                .map(|entry| entry.unwrap().path()),
147        );
148        for (expected, actual) in all_files.iter().zip(&mut iter) {
149            assert_eq!(*expected, actual);
150        }
151
152        assert_eq!(None, iter.next());
153    }
154
155    #[test]
156    fn iter_no_infinite_loop() {
157        // Hierarchy with an infinite loop
158        let temp = tempfile::tempdir().unwrap();
159        let root = temp.path();
160        let dir = root.join("loop");
161        unix::fs::symlink(root, &dir).expect("Linking {dir:?} to {root:?}");
162
163        // Sanity check that we have a loop
164        assert_eq!(
165            fs::canonicalize(root).unwrap(),
166            fs::canonicalize(&dir).unwrap(),
167            "Expected a loop where {dir:?} points to {root:?}"
168        );
169
170        // Now we need a fake desktop entry that will be yielded endlessly with a broken iter
171        let entry = DesktopEntry::from_appid("joshfakeapp123".into());
172        let entry_path = root.join("joshfakeapp123.desktop");
173        fs::write(&entry_path, entry.to_string()).expect("Writing entry: {entry_path:?}");
174
175        // Finally, check that the iterator is eventually exhausted
176        for (i, de) in Iter::new(
177            fs::read_dir(root)
178                .unwrap()
179                .map(|entry| entry.unwrap().path()),
180        )
181        .entries(Option::<&[&str]>::None)
182        .enumerate()
183        {
184            assert_eq!(entry.appid, de.appid);
185            if i > 0 {
186                panic!("Infinite loop");
187            }
188        }
189    }
190}