use std::any::Any;
use std::borrow::Cow;
use std::path::PathBuf;
use std::sync::OnceLock;
macro_rules! resource_name {
($(#[$m:meta])* $name:ident, $what:literal) => {
$(#[$m])*
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct $name(Cow<'static, str>);
impl $name {
#[doc = concat!("A ", $what, " known at compile time — the form the generated `res::…` constants use.")]
pub const fn from_static(name: &'static str) -> Self {
$name(Cow::Borrowed(name))
}
#[doc = concat!("Escape hatch for a ", $what, " computed at runtime (no build-time presence guarantee).")]
pub fn dynamic(name: impl Into<String>) -> Self {
$name(Cow::Owned(name.into()))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl From<String> for $name {
fn from(s: String) -> Self {
$name(Cow::Owned(s))
}
}
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
};
}
resource_name!(
ImageName,
"bundled-image name"
);
resource_name!(
AssetName,
"bundled-asset name"
);
resource_name!(
AssetDir,
"bundled-asset directory"
);
impl AssetDir {
pub fn join(&self, rel: &str) -> AssetName {
AssetName::dynamic(format!("{}/{}", self.as_str(), rel.trim_start_matches('/')))
}
}
resource_name!(
VectorName,
"bundled-vector name"
);
impl From<VectorName> for ImageName {
fn from(v: VectorName) -> ImageName {
ImageName::dynamic(v.as_str().to_owned())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct FontFamily(&'static str);
impl FontFamily {
pub const fn from_static(family: &'static str) -> Self {
FontFamily(family)
}
pub const fn as_str(&self) -> &'static str {
self.0
}
}
impl std::fmt::Display for FontFamily {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.0)
}
}
pub struct Resource {
ptr: *const u8,
len: usize,
_guard: Box<dyn Any>,
}
impl Resource {
pub unsafe fn from_raw(ptr: *const u8, len: usize, guard: Box<dyn Any>) -> Resource {
Resource {
ptr,
len,
_guard: guard,
}
}
pub fn from_vec(bytes: Vec<u8>) -> Resource {
let boxed: Box<[u8]> = bytes.into_boxed_slice();
let ptr = boxed.as_ptr();
let len = boxed.len();
Resource {
ptr,
len,
_guard: Box::new(boxed),
}
}
pub fn len(&self) -> usize {
self.len
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
pub fn as_slice(&self) -> &[u8] {
unsafe { std::slice::from_raw_parts(self.ptr, self.len) }
}
pub fn read_at(&self, offset: usize, buf: &mut [u8]) -> usize {
let data = self.as_slice();
if offset >= data.len() {
return 0;
}
let n = buf.len().min(data.len() - offset);
buf[..n].copy_from_slice(&data[offset..offset + n]);
n
}
pub fn to_vec(&self) -> Vec<u8> {
self.as_slice().to_vec()
}
}
pub type ResourceOpener = fn(&str) -> Option<Resource>;
static OPENER: OnceLock<ResourceOpener> = OnceLock::new();
pub fn set_resource_opener(opener: ResourceOpener) {
let _ = OPENER.set(opener);
}
pub fn resource(name: impl Into<AssetName>) -> Option<Resource> {
let name = name.into();
let name = name.as_str();
match OPENER.get() {
Some(open) => open(name),
None => default_open(name),
}
}
fn default_open(name: &str) -> Option<Resource> {
let path = resolve_resource_path(name)?;
let file = std::fs::File::open(&path).ok()?;
let mmap = unsafe { memmap2::Mmap::map(&file).ok()? };
let ptr = mmap.as_ptr();
let len = mmap.len();
Some(unsafe { Resource::from_raw(ptr, len, Box::new(mmap)) })
}
fn resolve_resource_path(name: &str) -> Option<PathBuf> {
for var in ASSET_ROOT_VARS {
if let Ok(root) = std::env::var(var) {
let p = PathBuf::from(root).join(name);
if p.exists() {
return Some(p);
}
}
}
if let Ok(exe) = std::env::current_exe()
&& let Some(dir) = exe.parent()
{
for rel in ["../Resources/assets", "Resources/assets", "assets"] {
let p = dir.join(rel).join(name);
if p.exists() {
return Some(p);
}
}
}
None
}
const ASSET_ROOT_VARS: [&str; 2] = ["DAY_ASSET_ROOT", "DAY_PIECE_ASSET_ROOT"];
pub fn resolve_asset_file(name: &str) -> Option<PathBuf> {
resolve_resource_path(name)
}
pub fn resolve_asset_dir(rel: &str) -> Option<PathBuf> {
let canon = |p: PathBuf| p.canonicalize().ok().filter(|c| c.is_dir());
for var in ASSET_ROOT_VARS {
if let Ok(root) = std::env::var(var)
&& let Some(p) = canon(PathBuf::from(root).join(rel))
{
return Some(p);
}
}
if let Ok(exe) = std::env::current_exe()
&& let Some(dir) = exe.parent()
{
for base in ["../Resources/assets", "Resources/assets", "assets"] {
if let Some(p) = canon(dir.join(base).join(rel)) {
return Some(p);
}
}
}
None
}
const IMAGE_EXTS: [&str; 8] = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "pdf", "svg"];
pub const WEIGHT_SUFFIXES: [&str; 2] = ["__light", "__bold"];
pub fn weight_alias(name: &str) -> Option<&str> {
WEIGHT_SUFFIXES
.iter()
.find_map(|s| name.strip_suffix(s))
.filter(|base| !base.is_empty())
}
pub fn resolve_image_file(name: &str) -> Option<PathBuf> {
let mut roots: Vec<PathBuf> = Vec::new();
for var in ["DAY_IMAGE_ROOT", "DAY_VECTOR_RASTER_ROOT", "DAY_ASSET_ROOT"] {
if let Ok(v) = std::env::var(var) {
roots.push(PathBuf::from(v));
}
}
if let Ok(exe) = std::env::current_exe()
&& let Some(dir) = exe.parent()
{
for rel in [
"../Resources/images",
"../Resources/vectors/raster",
"../Resources/assets",
"images",
"vectors/raster",
"assets",
] {
roots.push(dir.join(rel));
}
}
let names: Vec<&str> = match weight_alias(name) {
Some(base) => vec![name, base],
None => vec![name],
};
let has_ext = std::path::Path::new(name).extension().is_some();
for (name, root) in names
.iter()
.flat_map(|n| roots.iter().map(move |r| (*n, r.clone())))
{
let exact = root.join(name);
if exact.is_file() {
return Some(exact);
}
if !has_ext {
for ext in IMAGE_EXTS {
let p = root.join(format!("{name}.{ext}"));
if p.is_file() {
return Some(p);
}
}
}
}
None
}
pub fn resolve_vector_svg(name: &str) -> Option<PathBuf> {
let mut roots: Vec<PathBuf> = Vec::new();
if let Ok(v) = std::env::var("DAY_VECTOR_SVG_ROOT") {
roots.push(PathBuf::from(v));
}
if let Ok(exe) = std::env::current_exe()
&& let Some(dir) = exe.parent()
{
roots.push(dir.join("../Resources/vectors/svg"));
roots.push(dir.join("vectors/svg"));
}
if let Some(found) = roots
.iter()
.map(|r| r.join(format!("{name}.svg")))
.find(|p| p.is_file())
{
return Some(found);
}
let base = weight_alias(name)?;
roots
.into_iter()
.map(|r| r.join(format!("{base}.svg")))
.find(|p| p.is_file())
}
pub fn resolve_vector_xaml(name: &str) -> Option<PathBuf> {
let mut roots: Vec<PathBuf> = Vec::new();
if let Ok(v) = std::env::var("DAY_VECTOR_XAML_ROOT") {
roots.push(PathBuf::from(v));
}
if let Ok(exe) = std::env::current_exe()
&& let Some(dir) = exe.parent()
{
roots.push(dir.join("vectors/xaml"));
}
if let Some(found) = roots
.iter()
.map(|r| r.join(format!("{name}.xamlgeom")))
.find(|p| p.is_file())
{
return Some(found);
}
let base = weight_alias(name)?;
roots
.into_iter()
.map(|r| r.join(format!("{base}.xamlgeom")))
.find(|p| p.is_file())
}
pub fn stage_symbol_svg(sym: crate::Symbol) -> Option<PathBuf> {
let svg = sym.outline_svg()?;
let dir = std::env::temp_dir().join("day-symbols");
let file = dir.join(format!("{sym:?}.svg"));
if file.is_file() {
return Some(file);
}
std::fs::create_dir_all(&dir).ok()?;
std::fs::write(&file, svg).ok()?;
Some(file)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn env_backed_resource_access() {
let dir = std::env::temp_dir().join(format!("day-res-test-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let payload: Vec<u8> = (0u8..=255).cycle().take(1000).collect();
std::fs::write(dir.join("blob.bin"), &payload).unwrap();
std::fs::write(dir.join("logo.png"), b"x").unwrap();
unsafe {
std::env::set_var("DAY_ASSET_ROOT", &dir);
std::env::set_var("DAY_IMAGE_ROOT", &dir);
}
let res = resource(AssetName::from_static("blob.bin")).expect("resource present");
assert_eq!(res.len(), 1000);
assert!(!res.is_empty());
assert_eq!(res.as_slice(), &payload[..]);
let mut buf = [0u8; 16];
assert_eq!(res.read_at(500, &mut buf), 16);
assert_eq!(&buf, &payload[500..516]);
assert_eq!(res.read_at(995, &mut buf), 5);
assert_eq!(&buf[..5], &payload[995..1000]);
assert_eq!(res.read_at(2000, &mut buf), 0);
assert!(resource(AssetName::from_static("does-not-exist.bin")).is_none());
assert!(resource(AssetName::dynamic(String::from("nope.bin"))).is_none());
assert_eq!(resolve_image_file("logo"), Some(dir.join("logo.png")));
assert_eq!(resolve_image_file("logo.png"), Some(dir.join("logo.png")));
assert_eq!(resolve_image_file("missing"), None);
assert_eq!(weight_alias("logo__bold"), Some("logo"));
assert_eq!(weight_alias("logo"), None);
assert_eq!(weight_alias("__bold"), None); assert_eq!(resolve_image_file("logo__bold"), Some(dir.join("logo.png")));
assert_eq!(
resolve_image_file("logo__light"),
Some(dir.join("logo.png"))
);
assert_eq!(resolve_image_file("missing__bold"), None);
let svgs = dir.join("svg");
std::fs::create_dir_all(&svgs).unwrap();
std::fs::write(svgs.join("plain.svg"), b"<svg/>").unwrap();
std::fs::write(svgs.join("tmpl.svg"), b"<svg/>").unwrap();
std::fs::write(svgs.join("tmpl__bold.svg"), b"<svg/>").unwrap();
unsafe { std::env::set_var("DAY_VECTOR_SVG_ROOT", &svgs) };
assert_eq!(
resolve_vector_svg("plain__bold"),
Some(svgs.join("plain.svg"))
);
assert_eq!(
resolve_vector_svg("tmpl__bold"),
Some(svgs.join("tmpl__bold.svg")),
"a staged variant must not be shadowed by its base"
);
assert_eq!(resolve_vector_svg("absent__bold"), None);
unsafe { std::env::remove_var("DAY_VECTOR_SVG_ROOT") };
unsafe {
std::env::remove_var("DAY_ASSET_ROOT");
std::env::remove_var("DAY_IMAGE_ROOT");
}
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn from_vec_view_is_stable() {
let r = Resource::from_vec(vec![1, 2, 3, 4]);
assert_eq!(r.as_slice(), &[1, 2, 3, 4]);
assert_eq!(r.to_vec(), vec![1, 2, 3, 4]);
let mut b = [0u8; 2];
assert_eq!(r.read_at(2, &mut b), 2);
assert_eq!(b, [3, 4]);
}
}