mod security;
mod tests;
mod validation;
use crate::{Error, Result, Runtime};
use std::path::{Path, PathBuf};
pub use validation::validate_asset_size;
use security::canonicalize_path;
use security::validate_asset_security;
pub async fn resolve_asset(
specifier: &str,
referrer: &Path,
cwd: &Path,
runtime: &dyn Runtime,
) -> Result<PathBuf> {
eprintln!("[ASSET_RESOLVE] Resolving asset:");
eprintln!("[ASSET_RESOLVE] specifier: '{}'", specifier);
eprintln!("[ASSET_RESOLVE] referrer: {}", referrer.display());
eprintln!("[ASSET_RESOLVE] cwd: {}", cwd.display());
if specifier.starts_with('.') {
eprintln!("[ASSET_RESOLVE] Type: relative path");
return resolve_relative(specifier, referrer, cwd, runtime).await;
}
if specifier.starts_with('/') {
eprintln!("[ASSET_RESOLVE] Type: absolute path");
let path = PathBuf::from(specifier);
return validate_and_canonicalize(&path, cwd, runtime).await;
}
eprintln!("[ASSET_RESOLVE] Type: bare specifier");
let is_simple_filename = !specifier.contains('/')
|| (specifier.starts_with('@') && specifier.matches('/').count() == 1);
if !is_simple_filename {
eprintln!("[ASSET_RESOLVE] Looks like package path, trying node_modules");
return resolve_from_node_modules(specifier, referrer, cwd, runtime).await;
}
eprintln!("[ASSET_RESOLVE] Trying as relative filename");
match resolve_relative(specifier, referrer, cwd, runtime).await {
Ok(resolved) => {
eprintln!("[ASSET_RESOLVE] ✓ Resolved relative to referrer");
Ok(resolved)
}
Err(_) => {
eprintln!("[ASSET_RESOLVE] Failed relative, trying node_modules");
resolve_from_node_modules(specifier, referrer, cwd, runtime).await
}
}
}
async fn resolve_relative(
specifier: &str,
referrer: &Path,
cwd: &Path,
runtime: &dyn Runtime,
) -> Result<PathBuf> {
let looks_like_file = referrer.extension().is_some();
let base_dir = if looks_like_file {
referrer.parent().unwrap_or(cwd)
} else {
referrer
};
eprintln!("[ASSET_RESOLVE] base_dir: {}", base_dir.display());
let resolved = base_dir.join(specifier);
eprintln!("[ASSET_RESOLVE] joined path: {}", resolved.display());
validate_and_canonicalize(&resolved, cwd, runtime).await
}
async fn resolve_from_node_modules(
specifier: &str,
referrer: &Path,
cwd: &Path,
runtime: &dyn Runtime,
) -> Result<PathBuf> {
let looks_like_file = referrer.extension().is_some();
let mut current = if looks_like_file {
referrer.parent().unwrap_or(cwd)
} else {
referrer
};
eprintln!(
"[ASSET_RESOLVE] Starting search from: {}",
current.display()
);
let mut attempts = 0;
loop {
let candidate = current.join("node_modules").join(specifier);
eprintln!(
"[ASSET_RESOLVE] Attempt {}: checking {}",
attempts,
candidate.display()
);
if runtime.exists(&candidate) {
eprintln!("[ASSET_RESOLVE] ✓ Found!");
return validate_and_canonicalize(&candidate, cwd, runtime).await;
}
match current.parent() {
Some(parent) => {
current = parent;
attempts += 1;
}
None => {
eprintln!("[ASSET_RESOLVE] ✗ Not found in any node_modules");
break;
}
}
}
Err(Error::AssetNotFound {
specifier: specifier.to_string(),
searched_from: referrer.display().to_string(),
})
}
async fn validate_and_canonicalize(
path: &Path,
cwd: &Path,
runtime: &dyn Runtime,
) -> Result<PathBuf> {
eprintln!("[ASSET_RESOLVE] Validating: {}", path.display());
if !runtime.exists(path) {
eprintln!("[ASSET_RESOLVE] ✗ File does not exist");
return Err(Error::AssetNotFound {
specifier: path.display().to_string(),
searched_from: cwd.display().to_string(),
});
}
eprintln!("[ASSET_RESOLVE] ✓ File exists");
let canonical = canonicalize_path(path, runtime).await?;
eprintln!("[ASSET_RESOLVE] Canonical: {}", canonical.display());
validate_asset_security(&canonical, cwd, runtime).await?;
eprintln!("[ASSET_RESOLVE] ✓ Security validation passed");
eprintln!(
"[ASSET_RESOLVE] Final resolved path: {}",
canonical.display()
);
Ok(canonical)
}