use std::path::{Path, PathBuf};
#[cfg(not(target_arch = "wasm32"))]
fn asset_roots() -> Vec<PathBuf> {
let mut roots = Vec::new();
if let Some(dir) = std::env::var_os("CARGO_MANIFEST_DIR") {
roots.push(PathBuf::from(dir).join("assets"));
}
if let Some(dir) = std::env::current_exe()
.ok()
.and_then(|exe| exe.parent().map(Path::to_path_buf))
{
roots.push(dir.join("assets"));
}
if let Ok(dir) = std::env::current_dir() {
roots.push(dir.join("assets"));
}
roots.dedup();
roots
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn resolve(name: &str) -> Result<PathBuf, String> {
let roots = asset_roots();
for root in &roots {
let candidate = root.join(name);
if candidate.is_file() {
return Ok(candidate);
}
}
let searched: Vec<String> = roots.iter().map(|r| format!(" {}", r.display())).collect();
Err(format!(
"\"{name}\" not found in assets/. Looked in:\n{}",
searched.join("\n")
))
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn read(path: &Path) -> std::io::Result<Vec<u8>> {
std::fs::read(path)
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn write(name: &str, bytes: &[u8]) -> Result<PathBuf, String> {
let path = match resolve(name) {
Ok(existing) => existing,
Err(_) => {
let roots = asset_roots();
let root = roots
.iter()
.find(|r| r.is_dir())
.or(roots.first())
.ok_or("no assets/ folder")?;
root.join(name)
}
};
if let Some(dir) = path.parent() {
std::fs::create_dir_all(dir).map_err(|e| format!("could not create {}: {e}", dir.display()))?;
}
std::fs::write(&path, bytes).map_err(|e| format!("could not write {}: {e}", path.display()))?;
Ok(path)
}
#[cfg(all(not(target_arch = "wasm32"), feature = "inspector"))]
pub(crate) fn list(dir: &str, extension: &str) -> Vec<String> {
let mut names = Vec::new();
for root in asset_roots() {
let Ok(entries) = std::fs::read_dir(root.join(dir)) else {
continue;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_file()
&& path.extension().is_some_and(|e| e == extension)
&& let Some(file) = path.file_name().and_then(|f| f.to_str())
{
names.push(format!("{dir}/{file}"));
}
}
}
names.sort();
names.dedup();
names
}
#[cfg(all(target_arch = "wasm32", feature = "inspector"))]
pub(crate) fn list(_dir: &str, _extension: &str) -> Vec<String> {
Vec::new()
}
#[cfg(target_arch = "wasm32")]
pub(crate) fn write(name: &str, _: &[u8]) -> Result<PathBuf, String> {
Err(format!("can't save \"{name}\": web builds have no files"))
}
pub(crate) fn read_to_string(path: &Path) -> Result<String, String> {
let bytes = read(path).map_err(|e| format!("could not read {}: {e}", path.display()))?;
String::from_utf8(bytes).map_err(|e| format!("{} is not UTF-8 text: {e}", path.display()))
}
#[cfg(target_arch = "wasm32")]
thread_local! {
static WEB_FILES: std::cell::RefCell<std::collections::HashMap<String, std::rc::Rc<[u8]>>> =
Default::default();
}
#[cfg(target_arch = "wasm32")]
pub(crate) fn insert_web_file(name: String, bytes: Vec<u8>) {
WEB_FILES.with(|files| files.borrow_mut().insert(name, bytes.into()));
}
#[cfg(target_arch = "wasm32")]
pub(crate) fn resolve(name: &str) -> Result<PathBuf, String> {
if WEB_FILES.with(|files| files.borrow().contains_key(name)) {
Ok(PathBuf::from(name))
} else {
Err(format!(
"\"{name}\" not found in assets/. Web builds load the files listed in assets/index.json; \
is it listed there?"
))
}
}
#[cfg(target_arch = "wasm32")]
pub(crate) fn read(path: &Path) -> std::io::Result<Vec<u8>> {
let name = path.to_string_lossy();
WEB_FILES
.with(|files| files.borrow().get(name.as_ref()).map(|b| b.to_vec()))
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, format!("{name} was not fetched")))
}
pub(crate) struct Image {
pub width: u32,
pub height: u32,
pub rgba: Vec<u8>,
}
pub(crate) fn load_image_at(path: &Path, name: &str) -> Result<Image, String> {
let bytes = read(path).map_err(|e| format!("could not read {}: {e}", path.display()))?;
decode_image(&bytes).map_err(|e| format!("\"{name}\" is not a valid image: {e}"))
}
pub(crate) fn decode_image(bytes: &[u8]) -> Result<Image, String> {
let img = image::load_from_memory(bytes).map_err(|e| e.to_string())?;
let rgba = img.into_rgba8();
Ok(Image {
width: rgba.width(),
height: rgba.height(),
rgba: rgba.into_raw(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn missing_asset_error_names_file_and_folders() {
let err = resolve("definitely-not-here.png").unwrap_err();
assert!(err.contains("\"definitely-not-here.png\" not found in assets/"));
assert!(err.contains("assets"));
}
#[test]
fn garbage_bytes_are_rejected() {
assert!(decode_image(b"not a png").is_err());
}
#[test]
fn decodes_png() {
let mut png = Vec::new();
image::RgbaImage::from_raw(1, 1, vec![255, 0, 0, 255])
.unwrap()
.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png)
.unwrap();
let img = decode_image(&png).unwrap();
assert_eq!((img.width, img.height), (1, 1));
assert_eq!(img.rgba, [255, 0, 0, 255]);
}
}