use std::collections::HashMap;
use std::fs;
use std::marker::PhantomData;
use std::path::{Path, PathBuf};
use rust_embed::RustEmbed;
pub trait AssetSource: Send + Sync + 'static {
fn read(&self, path: &str) -> Option<Vec<u8>>;
fn list(&self, dir: &str) -> Vec<String>;
fn name(&self) -> &str;
}
pub struct EmbeddedSource<E: RustEmbed + Send + Sync + 'static> {
name: &'static str,
_marker: PhantomData<fn() -> E>,
}
impl<E: RustEmbed + Send + Sync + 'static> EmbeddedSource<E> {
#[must_use]
pub const fn new(name: &'static str) -> Self {
Self { name, _marker: PhantomData }
}
}
impl<E: RustEmbed + Send + Sync + 'static> AssetSource for EmbeddedSource<E> {
fn read(&self, path: &str) -> Option<Vec<u8>> {
E::get(path).map(|file| file.data.to_vec())
}
fn list(&self, dir: &str) -> Vec<String> {
let prefix = if dir.is_empty() || dir == "." {
String::new()
} else if dir.ends_with('/') {
dir.to_string()
} else {
format!("{dir}/")
};
let mut seen = std::collections::BTreeSet::new();
for raw in E::iter() {
let Some(rest) = raw.strip_prefix(prefix.as_str()) else { continue };
if rest.is_empty() {
continue;
}
let head = rest.find('/').map_or(rest, |idx| &rest[..idx]);
seen.insert(head.to_string());
}
seen.into_iter().collect()
}
fn name(&self) -> &str {
self.name
}
}
pub struct DirectorySource {
root: PathBuf,
name: String,
}
impl DirectorySource {
#[must_use]
pub fn new(root: impl Into<PathBuf>, name: impl Into<String>) -> Self {
Self { root: root.into(), name: name.into() }
}
fn resolve(&self, path: &str) -> Option<PathBuf> {
safe_join(&self.root, path)
}
}
impl AssetSource for DirectorySource {
fn read(&self, path: &str) -> Option<Vec<u8>> {
let resolved = self.resolve(path)?;
fs::read(resolved).ok()
}
fn list(&self, dir: &str) -> Vec<String> {
if dir.is_empty() || dir == "." {
return list_owned(self.root.as_path());
}
let Some(resolved) = self.resolve(dir) else {
return Vec::new();
};
list_owned(&resolved)
}
fn name(&self) -> &str {
&self.name
}
}
fn safe_join(root: &Path, rel: &str) -> Option<PathBuf> {
use std::path::Component;
let rel_path = Path::new(rel);
if rel_path.is_absolute() {
return None;
}
let mut out = root.to_path_buf();
for component in rel_path.components() {
match component {
Component::Normal(part) => out.push(part),
Component::CurDir => {}
Component::ParentDir | Component::RootDir | Component::Prefix(_) => return None,
}
}
Some(out)
}
fn list_owned(dir: &Path) -> Vec<String> {
let Ok(iter) = fs::read_dir(dir) else {
return Vec::new();
};
let mut out = Vec::new();
for entry in iter.flatten() {
if let Some(name) = entry.file_name().to_str() {
out.push(name.to_string());
}
}
out.sort();
out
}
pub struct MemorySource {
name: String,
files: HashMap<String, Vec<u8>>,
}
impl MemorySource {
#[must_use]
pub fn new(name: impl Into<String>, files: HashMap<String, Vec<u8>>) -> Self {
Self { name: name.into(), files }
}
}
impl AssetSource for MemorySource {
fn read(&self, path: &str) -> Option<Vec<u8>> {
self.files.get(path).cloned()
}
fn list(&self, dir: &str) -> Vec<String> {
let prefix = if dir.is_empty() || dir == "." {
String::new()
} else if dir.ends_with('/') {
dir.to_string()
} else {
format!("{dir}/")
};
let mut seen = std::collections::BTreeSet::new();
for key in self.files.keys() {
let Some(rest) = key.strip_prefix(prefix.as_str()) else { continue };
if rest.is_empty() {
continue;
}
let head = rest.find('/').map_or(rest, |idx| &rest[..idx]);
seen.insert(head.to_string());
}
seen.into_iter().collect()
}
fn name(&self) -> &str {
&self.name
}
}