hexomc_lib/assets/
detector.rs1use std::path::{Path, PathBuf};
2
3#[derive(Debug, Clone)]
4pub struct ResourcePack {
5 pub name: String,
6 pub path: PathBuf,
7 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 pub icon: Option<Vec<u8>>,
24}
25
26pub 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
47pub 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
72pub 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
226fn 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}