1use std::cell::RefCell;
26use std::collections::{BTreeMap, HashMap, VecDeque};
27use std::fs::File;
28use std::path::{Path, PathBuf};
29use std::sync::Arc;
30
31const OPEN_FILES: usize = 64;
33
34enum Source {
35 Memory(Vec<u8>),
36 File { file: File, path: PathBuf },
38 Lazy { path: PathBuf, len: u64 },
40}
41
42struct Entry {
43 source: Source,
44 from_dir: bool,
45}
46
47#[derive(Default)]
48pub struct Assets {
49 exact: HashMap<String, Entry>,
50 folded: BTreeMap<String, Vec<String>>,
52 sorted: Vec<String>,
54 open: RefCell<VecDeque<(PathBuf, Arc<File>)>>,
56}
57
58impl From<HashMap<String, Vec<u8>>> for Assets {
59 fn from(map: HashMap<String, Vec<u8>>) -> Assets {
60 let mut a = Assets::default();
61 for (name, bytes) in map {
62 a.insert_memory(&name, bytes);
63 }
64 a.finish();
65 a
66 }
67}
68
69#[cfg(unix)]
70fn pread(file: &File, buf: &mut [u8], offset: u64) -> std::io::Result<usize> {
71 std::os::unix::fs::FileExt::read_at(file, buf, offset)
72}
73
74#[cfg(windows)]
75fn pread(file: &File, buf: &mut [u8], offset: u64) -> std::io::Result<usize> {
76 std::os::windows::fs::FileExt::seek_read(file, buf, offset)
77}
78
79fn read_file(file: &File, path: &Path, offset: u64, dst: &mut [u8]) -> usize {
81 let mut done = 0;
82 while done < dst.len() {
83 match pread(file, &mut dst[done..], offset + done as u64) {
84 Ok(0) => break,
85 Ok(n) => done += n,
86 Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {}
87 Err(e) => {
88 eprintln!("[gasm] assets: {}: {e}", path.display());
89 break;
90 }
91 }
92 }
93 done
94}
95
96pub struct Asset<'a> {
98 assets: &'a Assets,
99 source: &'a Source,
100}
101
102impl Asset<'_> {
103 pub fn size(&self) -> u64 {
105 match self.source {
106 Source::Memory(b) => b.len() as u64,
107 Source::File { file, .. } => file.metadata().map_or(0, |m| m.len()),
108 Source::Lazy { len, .. } => *len,
109 }
110 }
111
112 pub fn read_at(&self, offset: u64, dst: &mut [u8]) -> usize {
116 match self.source {
117 Source::Memory(b) => {
118 let start = offset.min(b.len() as u64) as usize;
119 let n = (b.len() - start).min(dst.len());
120 dst[..n].copy_from_slice(&b[start..start + n]);
121 n
122 }
123 Source::File { file, path } => read_file(file, path, offset, dst),
124 Source::Lazy { path, .. } => match self.assets.open_lazy(path) {
125 Some(file) => read_file(&file, path, offset, dst),
126 None => 0,
127 },
128 }
129 }
130}
131
132impl Assets {
133 pub fn new() -> Assets {
134 Assets::default()
135 }
136
137 pub fn len(&self) -> usize {
138 self.exact.len()
139 }
140
141 pub fn is_empty(&self) -> bool {
142 self.exact.is_empty()
143 }
144
145 pub fn insert_memory(&mut self, name: &str, bytes: Vec<u8>) {
147 self.exact.insert(name.to_owned(), Entry { source: Source::Memory(bytes), from_dir: false });
148 }
149
150 pub fn insert_file(&mut self, name: &str, path: &Path) -> Result<(), String> {
153 let file = File::open(path).map_err(|e| format!("{}: {e}", path.display()))?;
154 if !file.metadata().map_err(|e| format!("{}: {e}", path.display()))?.is_file() {
155 return Err(format!("{}: not a regular file", path.display()));
156 }
157 self.exact.insert(name.to_owned(), Entry { source: Source::File { file, path: path.to_owned() }, from_dir: false });
158 Ok(())
159 }
160
161 pub fn add_dir(&mut self, prefix: Option<&str>, dir: &Path) -> Result<usize, String> {
165 let meta = std::fs::metadata(dir).map_err(|e| format!("{}: {e}", dir.display()))?;
166 if !meta.is_dir() {
167 return Err(format!("{}: not a directory", dir.display()));
168 }
169 let mut files = Vec::new();
170 walk(dir, &mut Vec::new(), &mut files)?;
171 let mut added = 0;
172 for (segments, path, len) in files {
173 let rel = segments.join("/");
174 let name = match prefix {
175 Some(p) if !p.is_empty() => format!("{}/{rel}", p.trim_end_matches('/')),
176 _ => rel,
177 };
178 if self.exact.contains_key(&name) {
179 continue;
180 }
181 self.exact.insert(name, Entry { source: Source::Lazy { path, len }, from_dir: true });
182 added += 1;
183 }
184 Ok(added)
185 }
186
187 pub fn finish(&mut self) {
189 self.folded.clear();
190 for (name, entry) in &self.exact {
191 if entry.from_dir {
192 self.folded.entry(name.to_ascii_lowercase()).or_default().push(name.clone());
193 }
194 }
195 for names in self.folded.values_mut() {
196 names.sort();
197 if names.len() > 1 {
198 eprintln!(
199 "[gasm] assets: {} differ only in case; case-insensitive lookups use {:?}",
200 names.join(", "),
201 names[0]
202 );
203 }
204 }
205 self.sorted = self.exact.keys().cloned().collect();
206 self.sorted.sort();
207 }
208
209 pub fn get(&self, name: &str) -> Option<Asset<'_>> {
211 let entry = match self.exact.get(name) {
212 Some(e) => e,
213 None => self.exact.get(self.folded.get(&name.to_ascii_lowercase())?.first()?)?,
214 };
215 Some(Asset { assets: self, source: &entry.source })
216 }
217
218 pub fn size(&self, name: &str) -> Option<u64> {
220 self.get(name).map(|a| a.size())
221 }
222
223 pub fn read_at(&self, name: &str, offset: u64, dst: &mut [u8]) -> Option<usize> {
226 self.get(name).map(|a| a.read_at(offset, dst))
227 }
228
229 pub fn names(&self) -> &[String] {
231 &self.sorted
232 }
233
234 fn open_lazy(&self, path: &Path) -> Option<Arc<File>> {
237 let mut open = self.open.borrow_mut();
238 if let Some(i) = open.iter().position(|(p, _)| p == path) {
239 let hit = open.remove(i)?;
240 let file = hit.1.clone();
241 open.push_back(hit);
242 return Some(file);
243 }
244 let ok = std::fs::symlink_metadata(path).is_ok_and(|m| m.file_type().is_file());
245 let file = match ok.then(|| File::open(path)) {
246 Some(Ok(f)) => Arc::new(f),
247 Some(Err(e)) => {
248 eprintln!("[gasm] assets: {}: {e}", path.display());
249 return None;
250 }
251 None => {
252 eprintln!("[gasm] assets: {}: no longer a regular file", path.display());
253 return None;
254 }
255 };
256 if open.len() >= OPEN_FILES {
257 open.pop_front();
258 }
259 open.push_back((path.to_owned(), file.clone()));
260 Some(file)
261 }
262}
263
264fn walk(dir: &Path, prefix: &mut Vec<String>, out: &mut Vec<(Vec<String>, PathBuf, u64)>) -> Result<(), String> {
268 let mut entries: Vec<_> = std::fs::read_dir(dir)
269 .map_err(|e| format!("{}: {e}", dir.display()))?
270 .filter_map(Result::ok)
271 .collect();
272 entries.sort_by_key(|e| e.file_name());
273 for entry in entries {
274 let Some(name) = entry.file_name().to_str().map(str::to_owned) else {
275 eprintln!("[gasm] assets: skipping non-UTF-8 name {:?}", entry.path());
276 continue;
277 };
278 if name.starts_with('.') {
279 continue;
280 }
281 let Ok(ft) = entry.file_type() else { continue }; if ft.is_symlink() {
283 continue;
284 }
285 prefix.push(name);
286 if ft.is_dir() {
287 walk(&entry.path(), prefix, out)?;
288 } else if ft.is_file() {
289 match entry.metadata() {
290 Ok(m) => out.push((prefix.clone(), entry.path(), m.len())),
291 Err(e) => eprintln!("[gasm] assets: skipping {}: {e}", entry.path().display()),
292 }
293 }
294 prefix.pop();
295 }
296 Ok(())
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302
303 fn tree() -> PathBuf {
304 let d = std::env::temp_dir().join(format!("gasm-assets-test-{}", std::process::id()));
305 let _ = std::fs::remove_dir_all(&d);
306 std::fs::create_dir_all(d.join("ART")).unwrap();
307 std::fs::create_dir_all(d.join("WORLDS/1PLAYER")).unwrap();
308 std::fs::write(d.join("ART/ART.CAR"), b"car").unwrap();
309 std::fs::write(d.join("WORLDS/1PLAYER/MAP.RFM"), b"map-data").unwrap();
310 std::fs::write(d.join(".DS_Store"), b"x").unwrap();
311 std::fs::write(d.join("dup.txt"), b"lower").unwrap();
312 std::fs::write(d.join("DUP.TXT"), b"upper").unwrap();
313 #[cfg(unix)]
314 std::os::unix::fs::symlink("/etc/hosts", d.join("escape")).unwrap();
315 d
316 }
317
318 #[test]
319 fn folders_case_and_precedence() {
320 let d = tree();
321 let mut a = Assets::new();
322 a.insert_memory("ART/ART.CAR", b"explicit".to_vec());
323 a.add_dir(None, &d).unwrap();
324 a.add_dir(Some("cd"), &d).unwrap();
325 a.finish();
326 let read = |a: &Assets, n: &str| {
327 let mut buf = vec![0u8; 64];
328 a.read_at(n, 0, &mut buf).map(|k| String::from_utf8_lossy(&buf[..k]).into_owned())
329 };
330 assert_eq!(read(&a, "ART/ART.CAR").as_deref(), Some("explicit")); assert_eq!(read(&a, "cd/art/art.car").as_deref(), Some("car")); assert_eq!(read(&a, "Worlds/1player/map.rfm").as_deref(), Some("map-data"));
333 let case_sensitive = std::fs::read_dir(&d).unwrap().filter_map(Result::ok)
335 .filter(|e| e.file_name().to_string_lossy().eq_ignore_ascii_case("dup.txt")).count() == 2;
336 if case_sensitive {
337 assert_eq!(read(&a, "dup.txt").as_deref(), Some("lower")); assert_eq!(read(&a, "Dup.Txt").as_deref(), Some("upper")); }
340 assert!(a.size(".DS_Store").is_none()); assert!(a.size("escape").is_none()); assert_eq!(a.size("worlds/1player/MAP.RFM"), Some(8));
343 let mut buf = [0u8; 4];
344 assert_eq!(a.read_at("WORLDS/1PLAYER/MAP.RFM", 4, &mut buf), Some(4));
345 assert_eq!(&buf, b"data");
346 assert_eq!(a.read_at("WORLDS/1PLAYER/MAP.RFM", 100, &mut buf), Some(0)); assert!(a.read_at("missing", 0, &mut buf).is_none());
348 assert!(a.names().windows(2).all(|w| w[0] < w[1]));
349 let _ = std::fs::remove_dir_all(&d);
350 }
351
352 #[test]
353 fn many_folder_files_stay_under_the_open_file_limit() {
354 let d = std::env::temp_dir().join(format!("gasm-assets-many-{}", std::process::id()));
355 let _ = std::fs::remove_dir_all(&d);
356 std::fs::create_dir_all(&d).unwrap();
357 for i in 0..(OPEN_FILES * 3) {
358 std::fs::write(d.join(format!("F{i:04}")), format!("{i}")).unwrap();
359 }
360 let mut a = Assets::new();
361 a.add_dir(None, &d).unwrap();
362 a.finish();
363 let mut buf = [0u8; 8];
364 for round in 0..2 {
365 for i in 0..(OPEN_FILES * 3) {
366 let n = a.read_at(&format!("f{i:04}"), 0, &mut buf).unwrap();
367 assert_eq!(&buf[..n], format!("{i}").as_bytes(), "round {round}");
368 }
369 }
370 assert!(a.open.borrow().len() <= OPEN_FILES);
371 let _ = std::fs::remove_dir_all(&d);
372 }
373
374 #[test]
375 fn shrinking_file_never_panics() {
376 let p = std::env::temp_dir().join(format!("gasm-shrink-{}", std::process::id()));
377 std::fs::write(&p, vec![7u8; 1000]).unwrap();
378 let mut a = Assets::new();
379 a.insert_file("f", &p).unwrap();
380 a.finish();
381 std::fs::write(&p, vec![7u8; 10]).unwrap(); let mut buf = vec![0u8; 100];
383 assert_eq!(a.size("f"), Some(10));
384 assert_eq!(a.read_at("f", 0, &mut buf), Some(10));
385 assert_eq!(a.read_at("f", 500, &mut buf), Some(0));
386 std::fs::remove_file(&p).unwrap(); let _ = a.read_at("f", 0, &mut buf); let _ = std::fs::remove_file(&p);
389 }
390}