Skip to main content

hexomc_lib/assets/
detector.rs

1use std::path::{Path, PathBuf};
2
3#[derive(Debug, Clone)]
4pub struct ResourcePack {
5    pub name: String,
6    pub path: PathBuf,
7    /// true = folder, false = .zip
8    pub is_folder: bool,
9}
10
11#[derive(Debug, Clone)]
12pub struct ShaderPack {
13    pub name: String,
14    pub path: PathBuf,
15    pub is_folder: bool,
16}
17
18#[derive(Debug, Clone)]
19pub struct WorldMap {
20    pub name: String,
21    pub path: PathBuf,
22    /// Raw bytes of the world's icon.png, if present.
23    pub icon: Option<Vec<u8>>,
24}
25
26/// Scan `.minecraft/resourcepacks/`. A valid pack has `pack.mcmeta` (folder) or is
27/// a `.zip` with `pack.mcmeta` at its root.
28pub fn detect_resource_packs(minecraft_dir: &Path) -> Vec<ResourcePack> {
29    let dir = minecraft_dir.join("resourcepacks");
30    scan_packs(&dir, is_resource_pack_zip, is_resource_pack_folder)
31        .into_iter()
32        .map(|(name, path, is_folder)| ResourcePack { name, path, is_folder })
33        .collect()
34}
35
36fn is_resource_pack_zip(zip_path: &Path) -> bool {
37    let Ok(file) = std::fs::File::open(zip_path) else { return false; };
38    let Ok(mut archive) = zip::ZipArchive::new(file) else { return false; };
39    let result = archive.by_name("pack.mcmeta").is_ok();
40    result
41}
42
43fn is_resource_pack_folder(folder: &Path) -> bool {
44    folder.join("pack.mcmeta").exists()
45}
46
47/// Scan `.minecraft/shaderpacks/`. A valid pack has a `shaders/` directory
48/// (folder) or a `shaders/` folder inside the zip.
49pub fn detect_shader_packs(minecraft_dir: &Path) -> Vec<ShaderPack> {
50    let dir = minecraft_dir.join("shaderpacks");
51    scan_packs(&dir, is_shader_zip, is_shader_folder)
52        .into_iter()
53        .map(|(name, path, is_folder)| ShaderPack { name, path, is_folder })
54        .collect()
55}
56
57fn is_shader_zip(zip_path: &Path) -> bool {
58    let Ok(file) = std::fs::File::open(zip_path) else { return false; };
59    let Ok(mut archive) = zip::ZipArchive::new(file) else { return false; };
60    let result = (0..archive.len()).any(|i| {
61        archive.by_index_raw(i)
62            .map(|e| e.name().starts_with("shaders/"))
63            .unwrap_or(false)
64    });
65    result
66}
67
68fn is_shader_folder(folder: &Path) -> bool {
69    folder.join("shaders").is_dir()
70}
71
72/// Scan `.minecraft/saves/`. A folder containing `level.dat` is a world.
73pub fn detect_maps(minecraft_dir: &Path) -> Vec<WorldMap> {
74    let saves_dir = minecraft_dir.join("saves");
75    let mut maps = Vec::new();
76
77    let Ok(entries) = std::fs::read_dir(&saves_dir) else {
78        return maps;
79    };
80
81    for entry in entries.flatten() {
82        let path = entry.path();
83        if !path.is_dir() {
84            continue;
85        }
86        if !path.join("level.dat").exists() {
87            continue;
88        }
89
90        let name = path.file_name().unwrap_or_default().to_string_lossy().to_string();
91        let icon_path = path.join("icon.png");
92        let icon = std::fs::read(&icon_path).ok();
93
94        maps.push(WorldMap {
95            name,
96            path,
97            icon,
98        });
99    }
100
101    maps
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use std::fs;
108    use std::io::Write;
109    use tempfile::TempDir;
110    use zip::write::{FileOptions, ZipWriter};
111
112    fn make_zip_with_file(dir: &std::path::Path, zip_name: &str, inner_file: &str) -> PathBuf {
113        let zip_path = dir.join(zip_name);
114        let file = fs::File::create(&zip_path).unwrap();
115        let mut zip = ZipWriter::new(file);
116        let opts = FileOptions::<()>::default();
117        zip.start_file(inner_file, opts).unwrap();
118        zip.write_all(b"{}").unwrap();
119        zip.finish().unwrap();
120        zip_path
121    }
122
123    #[test]
124    fn detect_resource_pack_zip() {
125        let tmp = TempDir::new().unwrap();
126        let mc_dir = tmp.path();
127        let rp_dir = mc_dir.join("resourcepacks");
128        fs::create_dir_all(&rp_dir).unwrap();
129        make_zip_with_file(&rp_dir, "MyPack.zip", "pack.mcmeta");
130
131        let packs = detect_resource_packs(mc_dir);
132        assert_eq!(packs.len(), 1);
133        assert_eq!(packs[0].name, "MyPack");
134        assert!(!packs[0].is_folder);
135    }
136
137    #[test]
138    fn detect_resource_pack_folder() {
139        let tmp = TempDir::new().unwrap();
140        let mc_dir = tmp.path();
141        let rp_dir = mc_dir.join("resourcepacks").join("FolderPack");
142        fs::create_dir_all(&rp_dir).unwrap();
143        fs::write(rp_dir.join("pack.mcmeta"), b"{}").unwrap();
144
145        let packs = detect_resource_packs(mc_dir);
146        assert_eq!(packs.len(), 1);
147        assert_eq!(packs[0].name, "FolderPack");
148        assert!(packs[0].is_folder);
149    }
150
151    #[test]
152    fn invalid_zip_ignored() {
153        let tmp = TempDir::new().unwrap();
154        let mc_dir = tmp.path();
155        let rp_dir = mc_dir.join("resourcepacks");
156        fs::create_dir_all(&rp_dir).unwrap();
157        make_zip_with_file(&rp_dir, "NotAPack.zip", "some_other_file.txt");
158
159        let packs = detect_resource_packs(mc_dir);
160        assert!(packs.is_empty());
161    }
162
163    #[test]
164    fn detect_shader_pack_folder() {
165        let tmp = TempDir::new().unwrap();
166        let mc_dir = tmp.path();
167        let sp_dir = mc_dir.join("shaderpacks").join("MyShader");
168        fs::create_dir_all(sp_dir.join("shaders")).unwrap();
169
170        let shaders = detect_shader_packs(mc_dir);
171        assert_eq!(shaders.len(), 1);
172        assert_eq!(shaders[0].name, "MyShader");
173    }
174
175    #[test]
176    fn detect_shader_pack_zip() {
177        let tmp = TempDir::new().unwrap();
178        let mc_dir = tmp.path();
179        let sp_dir = mc_dir.join("shaderpacks");
180        fs::create_dir_all(&sp_dir).unwrap();
181        make_zip_with_file(&sp_dir, "CoolShader.zip", "shaders/final.fsh");
182
183        let shaders = detect_shader_packs(mc_dir);
184        assert_eq!(shaders.len(), 1);
185        assert_eq!(shaders[0].name, "CoolShader");
186    }
187
188    #[test]
189    fn detect_world_map() {
190        let tmp = TempDir::new().unwrap();
191        let mc_dir = tmp.path();
192        let world_dir = mc_dir.join("saves").join("My World");
193        fs::create_dir_all(&world_dir).unwrap();
194        fs::write(world_dir.join("level.dat"), b"\x1f\x8b").unwrap();
195
196        let maps = detect_maps(mc_dir);
197        assert_eq!(maps.len(), 1);
198        assert_eq!(maps[0].name, "My World");
199        assert!(maps[0].icon.is_none());
200    }
201
202    #[test]
203    fn world_with_icon() {
204        let tmp = TempDir::new().unwrap();
205        let mc_dir = tmp.path();
206        let world_dir = mc_dir.join("saves").join("IconWorld");
207        fs::create_dir_all(&world_dir).unwrap();
208        fs::write(world_dir.join("level.dat"), b"data").unwrap();
209        fs::write(world_dir.join("icon.png"), b"\x89PNG").unwrap();
210
211        let maps = detect_maps(mc_dir);
212        assert_eq!(maps.len(), 1);
213        assert!(maps[0].icon.is_some());
214        assert_eq!(maps[0].icon.as_deref(), Some(b"\x89PNG".as_ref()));
215    }
216
217    #[test]
218    fn empty_saves_dir() {
219        let tmp = TempDir::new().unwrap();
220        let mc_dir = tmp.path();
221        fs::create_dir_all(mc_dir.join("saves")).unwrap();
222        assert!(detect_maps(mc_dir).is_empty());
223    }
224}
225
226/// Generic scan returning (name, path, is_folder).
227fn scan_packs(
228    dir: &Path,
229    check_zip: impl Fn(&Path) -> bool,
230    check_folder: impl Fn(&Path) -> bool,
231) -> Vec<(String, PathBuf, bool)> {
232    let mut results = Vec::new();
233
234    let Ok(entries) = std::fs::read_dir(dir) else {
235        return results;
236    };
237
238    for entry in entries.flatten() {
239        let path = entry.path();
240
241        if path.is_file() {
242            if path.extension().and_then(|e| e.to_str()) == Some("zip") {
243                if check_zip(&path) {
244                    let name = path
245                        .file_stem()
246                        .unwrap_or_default()
247                        .to_string_lossy()
248                        .to_string();
249                    results.push((name, path, false));
250                }
251            }
252        } else if path.is_dir() {
253            if check_folder(&path) {
254                let name = path.file_name().unwrap_or_default().to_string_lossy().to_string();
255                results.push((name, path, true));
256            }
257        }
258    }
259
260    results
261}