use http::{status::StatusCode, Response};
use std::path::{Path, PathBuf};
use crate::{AssetPathError, NativeAssetResolveError};
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
pub enum AssetServeError {
#[error("Failed to resolve asset: {0}")]
ResolveError(#[from] NativeAssetResolveError),
#[error("Failed to construct response: {0}")]
ResponseError(#[from] http::Error),
}
pub(crate) fn resolve_native_asset_path(path: &str) -> Result<PathBuf, AssetPathError> {
#[allow(clippy::unnecessary_lazy_evaluations)]
resolve_asset_path_from_filesystem(path).ok_or_else(|| {
#[cfg(target_os = "android")]
{
if to_java_load_asset(path).is_some() {
return AssetPathError::CannotRepresentAsPath;
}
}
AssetPathError::NotFound
})
}
fn resolve_asset_path_from_filesystem(path: &str) -> Option<PathBuf> {
let mut uri_path = PathBuf::from(
percent_encoding::percent_decode_str(path)
.decode_utf8()
.expect("expected URL to be UTF-8 encoded")
.as_ref(),
);
if !uri_path.exists() || uri_path.starts_with("/assets/") {
let bundle_root = get_asset_root();
let relative_path = uri_path.strip_prefix("/").unwrap();
uri_path = bundle_root.join(relative_path);
}
uri_path.exists().then_some(uri_path)
}
struct ResolvedAsset {
mime_type: &'static str,
body: Vec<u8>,
}
impl ResolvedAsset {
fn new(mime_type: &'static str, body: Vec<u8>) -> Self {
Self { mime_type, body }
}
fn into_response(self) -> Result<Response<Vec<u8>>, AssetServeError> {
Ok(Response::builder()
.header("Content-Type", self.mime_type)
.header("Access-Control-Allow-Origin", "*")
.body(self.body)?)
}
}
pub(crate) fn resolve_native_asset(path: &str) -> Result<Vec<u8>, NativeAssetResolveError> {
#[cfg(target_os = "android")]
{
if let Some(asset) = to_java_load_asset(path) {
return Ok(asset);
}
}
let Some(uri_path) = resolve_asset_path_from_filesystem(path) else {
return Err(NativeAssetResolveError::IoError(std::io::Error::new(
std::io::ErrorKind::NotFound,
"Asset not found",
)));
};
Ok(std::fs::read(uri_path)?)
}
fn resolve_asset(path: &str) -> Result<Option<ResolvedAsset>, NativeAssetResolveError> {
#[cfg(target_os = "android")]
{
if let Some(asset) = to_java_load_asset(path) {
let extension = path.rsplit_once('.').and_then(|(_, ext)| Some(ext));
let mime_type = get_mime_from_ext(extension);
return Ok(Some(ResolvedAsset::new(mime_type, asset)));
}
}
let Some(uri_path) = resolve_asset_path_from_filesystem(path) else {
return Ok(None);
};
let mime_type = get_mime_from_path(&uri_path)?;
let body = std::fs::read(uri_path)?;
Ok(Some(ResolvedAsset::new(mime_type, body)))
}
pub fn serve_asset(path: &str) -> Result<Response<Vec<u8>>, AssetServeError> {
match resolve_asset(path)? {
Some(asset) => asset.into_response(),
None => Ok(Response::builder()
.status(StatusCode::NOT_FOUND)
.body(String::from("Not Found").into_bytes())?),
}
}
#[allow(unreachable_code)]
fn get_asset_root() -> PathBuf {
let cur_exe = std::env::current_exe().unwrap();
if cfg!(target_os = "macos") {
return cur_exe
.parent()
.unwrap()
.parent()
.unwrap()
.join("Resources");
}
if cfg!(target_os = "linux") {
if let Some(lib_dir) = cur_exe
.parent()
.and_then(|parent| parent.parent())
.map(|root| root.join("lib"))
{
if let Some(product_name) = dioxus_cli_config::product_name() {
let path = lib_dir.join(&product_name);
if path.exists() {
return path;
}
}
if let Ok(entries) = std::fs::read_dir(&lib_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() && path.join("assets").is_dir() {
return path;
}
}
}
}
}
cur_exe.parent().unwrap().to_path_buf()
}
fn get_mime_from_path(asset: &Path) -> std::io::Result<&'static str> {
if asset.extension().is_some_and(|ext| ext == "svg") {
return Ok("image/svg+xml");
}
match infer::get_from_path(asset)?.map(|f| f.mime_type()) {
Some(f) if f != "text/plain" => Ok(f),
_other => Ok(get_mime_by_ext(asset)),
}
}
fn get_mime_by_ext(trimmed: &Path) -> &'static str {
let ext = trimmed.extension().as_ref().and_then(|ext| ext.to_str());
get_mime_from_ext(ext)
}
pub fn get_mime_from_ext(ext: Option<&str>) -> &'static str {
match ext {
Some("js") => "text/javascript; charset=utf-8",
Some("css") => "text/css; charset=utf-8",
Some("json") => "application/json; charset=utf-8",
Some("svg") => "image/svg+xml; charset=utf-8",
Some("html") => "text/html; charset=utf-8",
Some("mjs") => "text/javascript; charset=utf-8",
Some("bin") => "application/octet-stream",
Some("csv") => "text/csv",
Some("ico") => "image/vnd.microsoft.icon",
Some("jsonld") => "application/ld+json",
Some("rtf") => "application/rtf",
Some("mp4") => "video/mp4",
Some(_) => "text/html; charset=utf-8",
None => "application/octet-stream",
}
}
#[cfg(target_os = "android")]
pub(crate) fn to_java_load_asset(filepath: &str) -> Option<Vec<u8>> {
let normalized = filepath
.trim_start_matches("/assets/")
.trim_start_matches('/');
#[cfg(debug_assertions)]
{
let path = dioxus_cli_config::android_session_cache_dir().join(normalized);
if path.exists() {
return std::fs::read(path).ok();
}
}
use std::ptr::NonNull;
let ctx = ndk_context::android_context();
let vm = unsafe { jni::JavaVM::from_raw(ctx.vm().cast()) }.unwrap();
let mut env = vm.attach_current_thread().unwrap();
let asset_manager_ptr = env
.call_method(
unsafe { jni::objects::JObject::from_raw(ctx.context().cast()) },
"getAssets",
"()Landroid/content/res/AssetManager;",
&[],
)
.expect("Failed to get asset manager")
.l()
.expect("Failed to get asset manager as object");
unsafe {
let asset_manager =
ndk_sys::AAssetManager_fromJava(env.get_native_interface(), *asset_manager_ptr);
let asset_manager = ndk::asset::AssetManager::from_ptr(
NonNull::new(asset_manager).expect("Invalid asset manager"),
);
let cstr = std::ffi::CString::new(normalized).unwrap();
let mut asset = asset_manager.open(&cstr)?;
Some(asset.buffer().unwrap().to_vec())
}
}