Skip to main content

supreme_fs/game_storage/
discovery.rs

1//! Walking the game directory tree to discover files and directories.
2
3use super::GameStorage;
4use std::io;
5use std::path::{Path, PathBuf};
6use walkdir::WalkDir;
7
8#[derive(PartialEq, Eq, Clone, Copy, Debug)]
9pub enum DiscoverMode {
10    All,
11    Files,
12    Directories,
13}
14
15impl GameStorage {
16    /// Discover paths in a specific subdirectory relative to the root.
17    ///
18    /// # Errors
19    ///
20    /// Returns an error if the directory cannot be read.
21    pub fn discover_relative(
22        &self,
23        rel_path: &Path,
24        mode: DiscoverMode,
25    ) -> io::Result<Vec<PathBuf>> {
26        let mut paths = Vec::new();
27        let full_path = self.root_path.join(rel_path);
28
29        if !full_path.exists() {
30            return Ok(paths);
31        }
32
33        for entry in WalkDir::new(&full_path)
34            .into_iter()
35            .filter_map(std::result::Result::ok)
36        {
37            let path = entry.path();
38
39            if path != full_path
40                && (mode == DiscoverMode::All
41                    || (mode == DiscoverMode::Files && path.is_file())
42                    || (mode == DiscoverMode::Directories && path.is_dir()))
43            {
44                paths.push(path.to_path_buf());
45            }
46        }
47        Ok(paths)
48    }
49
50    /// Discover all paths (files and directories) in the game directory not starting with `.`.
51    ///
52    /// # Errors
53    ///
54    /// Returns an error if the game directory cannot be read.
55    pub fn discover(&self, mode: DiscoverMode) -> io::Result<Vec<PathBuf>> {
56        let mut paths = Vec::new();
57        let root_path = &self.root_path;
58        for entry in WalkDir::new(root_path)
59            .into_iter()
60            .filter_map(std::result::Result::ok)
61        {
62            let path = entry.path();
63
64            // Skip hidden files/directories (starting with .) relative to the root
65            if let Ok(rel_path) = path.strip_prefix(root_path) {
66                let is_hidden = rel_path
67                    .components()
68                    .any(|c| c.as_os_str().to_str().unwrap_or_default().starts_with('.'));
69                if is_hidden {
70                    continue;
71                }
72            }
73
74            if path != root_path
75                && (mode == DiscoverMode::All
76                    || (mode == DiscoverMode::Files && path.is_file())
77                    || (mode == DiscoverMode::Directories && path.is_dir()))
78            {
79                paths.push(path.to_path_buf());
80            }
81        }
82        Ok(paths)
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89    use std::fs;
90    use tempfile::tempdir;
91
92    fn make_storage() -> (tempfile::TempDir, GameStorage) {
93        let dir = tempdir().expect("tempdir");
94        let storage = GameStorage::new(dir.path());
95        (dir, storage)
96    }
97
98    #[test]
99    fn discover_on_empty_directory_returns_empty() {
100        let (_dir, storage) = make_storage();
101        let paths = storage.discover(DiscoverMode::All).expect("discover");
102        assert!(paths.is_empty());
103    }
104
105    #[test]
106    fn discover_finds_files_and_directories() {
107        let (dir, storage) = make_storage();
108        fs::create_dir_all(dir.path().join("levels")).unwrap();
109        fs::write(dir.path().join("levels/level1.dat"), b"data").unwrap();
110        fs::write(dir.path().join("readme.txt"), b"hi").unwrap();
111
112        let all = storage.discover(DiscoverMode::All).expect("discover");
113        assert_eq!(all.len(), 3); // levels/, levels/level1.dat, readme.txt
114
115        let files = storage.discover(DiscoverMode::Files).expect("discover");
116        assert_eq!(files.len(), 2);
117        assert!(files.iter().all(|p| p.is_file()));
118
119        let dirs = storage
120            .discover(DiscoverMode::Directories)
121            .expect("discover");
122        assert_eq!(dirs.len(), 1);
123        assert!(dirs[0].ends_with("levels"));
124    }
125
126    #[test]
127    fn discover_skips_hidden_files_and_directories() {
128        let (dir, storage) = make_storage();
129        fs::create_dir_all(dir.path().join(".supreme")).unwrap();
130        fs::write(dir.path().join(".supreme/manifest.json"), b"{}").unwrap();
131        fs::write(dir.path().join("visible.txt"), b"hi").unwrap();
132
133        let all = storage.discover(DiscoverMode::All).expect("discover");
134        assert_eq!(all.len(), 1);
135        assert!(all[0].ends_with("visible.txt"));
136    }
137
138    #[test]
139    fn discover_relative_only_walks_requested_subdirectory() {
140        let (dir, storage) = make_storage();
141        fs::create_dir_all(dir.path().join("levels")).unwrap();
142        fs::write(dir.path().join("levels/level1.dat"), b"data").unwrap();
143        fs::write(dir.path().join("outside.txt"), b"data").unwrap();
144
145        let found = storage
146            .discover_relative(Path::new("levels"), DiscoverMode::Files)
147            .expect("discover_relative");
148
149        assert_eq!(found.len(), 1);
150        assert!(found[0].ends_with("level1.dat"));
151    }
152
153    #[test]
154    fn discover_relative_on_missing_directory_returns_empty_ok() {
155        let (_dir, storage) = make_storage();
156        let found = storage
157            .discover_relative(Path::new("does_not_exist"), DiscoverMode::All)
158            .expect("discover_relative should not error on missing dir");
159        assert!(found.is_empty());
160    }
161}