use std::path::{Component, Path, PathBuf};
const ASSET_PREFIX: char = '@';
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AssetRef<'a> {
ProjectRoot { rel: &'a str },
Alias { alias: &'a str, rel: &'a str },
}
pub fn is_asset_path(path: &str) -> bool {
path.starts_with(ASSET_PREFIX)
}
pub fn parse(path: &str) -> Option<AssetRef<'_>> {
let stripped = path.strip_prefix(ASSET_PREFIX)?;
if let Some(rel) = stripped.strip_prefix('/') {
return Some(AssetRef::ProjectRoot { rel });
}
let (alias, rel) = stripped.split_once('/')?;
Some(AssetRef::Alias { alias, rel })
}
pub fn find_project_root(base: &Path) -> Option<PathBuf> {
let mut dir = base.to_path_buf();
loop {
if dir.join("harn.toml").exists() {
return Some(dir);
}
if !dir.pop() {
return None;
}
}
}
pub fn resolve(asset_ref: &AssetRef<'_>, anchor: &Path) -> Result<PathBuf, String> {
let project_root = find_project_root(anchor).ok_or_else(|| {
format!(
"package-root prompt path '{}' has no project root: no harn.toml found above {}",
display_asset(asset_ref),
anchor.display()
)
})?;
match asset_ref {
AssetRef::ProjectRoot { rel } => {
let safe = safe_relative(rel)
.ok_or_else(|| format!("invalid project-root asset path '@/{rel}'"))?;
Ok(project_root.join(safe))
}
AssetRef::Alias { alias, rel } => {
let safe =
safe_relative(rel).ok_or_else(|| format!("invalid asset path '@{alias}/{rel}'"))?;
let asset_root = lookup_alias(&project_root, alias).ok_or_else(|| {
format!(
"asset alias '{alias}' is not defined in [asset_roots] of {}",
project_root.join("harn.toml").display()
)
})?;
let safe_root = safe_relative(&asset_root).ok_or_else(|| {
format!(
"asset alias '{alias}' resolves to an unsafe path '{asset_root}' \
(must be a project-relative directory without `..` segments)"
)
})?;
Ok(project_root.join(safe_root).join(safe))
}
}
}
pub fn resolve_or<F>(path: &str, anchor: &Path, fallback: F) -> Result<PathBuf, String>
where
F: FnOnce(&str) -> PathBuf,
{
if let Some(asset_ref) = parse(path) {
return resolve(&asset_ref, anchor);
}
Ok(fallback(path))
}
fn safe_relative(raw: &str) -> Option<PathBuf> {
if raw.is_empty() || raw.contains('\\') {
return None;
}
let mut out = PathBuf::new();
let mut saw_component = false;
for component in Path::new(raw).components() {
match component {
Component::Normal(part) => {
saw_component = true;
out.push(part);
}
Component::CurDir => {}
Component::ParentDir | Component::RootDir | Component::Prefix(_) => return None,
}
}
saw_component.then_some(out)
}
fn display_asset(asset_ref: &AssetRef<'_>) -> String {
match asset_ref {
AssetRef::ProjectRoot { rel } => format!("@/{rel}"),
AssetRef::Alias { alias, rel } => format!("@{alias}/{rel}"),
}
}
fn lookup_alias(project_root: &Path, alias: &str) -> Option<String> {
let manifest = std::fs::read_to_string(project_root.join("harn.toml")).ok()?;
let parsed: toml::Value = toml::from_str(&manifest).ok()?;
let table = parsed.get("asset_roots")?.as_table()?;
table.get(alias)?.as_str().map(str::to_string)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn parses_project_root_form() {
assert_eq!(
parse("@/partials/foo.harn.prompt"),
Some(AssetRef::ProjectRoot {
rel: "partials/foo.harn.prompt"
})
);
}
#[test]
fn parses_alias_form() {
assert_eq!(
parse("@partials/foo.harn.prompt"),
Some(AssetRef::Alias {
alias: "partials",
rel: "foo.harn.prompt"
})
);
}
#[test]
fn plain_paths_pass_through() {
assert!(parse("relative/path").is_none());
assert!(parse("/absolute/path").is_none());
assert!(parse("../sibling").is_none());
}
#[test]
fn parent_traversal_rejected() {
assert!(safe_relative("foo/../bar").is_none());
assert!(safe_relative("/abs").is_none());
assert!(safe_relative("").is_none());
}
#[test]
fn resolves_project_root_path_anchored_at_caller_root() {
let temp = TempDir::new().unwrap();
let root = temp.path();
fs::write(root.join("harn.toml"), "[package]\nname = \"x\"\n").unwrap();
fs::create_dir_all(root.join("a/b/c")).unwrap();
let resolved = resolve(
&parse("@/prompts/foo.harn.prompt").unwrap(),
&root.join("a/b/c"),
)
.unwrap();
assert_eq!(resolved, root.join("prompts/foo.harn.prompt"));
}
#[test]
fn resolves_alias_path_via_asset_roots() {
let temp = TempDir::new().unwrap();
let root = temp.path();
fs::write(
root.join("harn.toml"),
"[package]\nname = \"x\"\n[asset_roots]\npartials = \"src/prompts\"\n",
)
.unwrap();
fs::create_dir_all(root.join("a/b")).unwrap();
let resolved = resolve(
&parse("@partials/foo.harn.prompt").unwrap(),
&root.join("a/b"),
)
.unwrap();
assert_eq!(resolved, root.join("src/prompts/foo.harn.prompt"));
}
#[test]
fn missing_alias_produces_clear_error() {
let temp = TempDir::new().unwrap();
let root = temp.path();
fs::write(root.join("harn.toml"), "[package]\nname = \"x\"\n").unwrap();
let err = resolve(&parse("@unknown/foo.harn.prompt").unwrap(), root).unwrap_err();
assert!(err.contains("[asset_roots]"));
assert!(err.contains("unknown"));
}
#[test]
fn no_project_root_produces_error() {
let temp = TempDir::new().unwrap();
let err = resolve(&parse("@/foo.harn.prompt").unwrap(), temp.path()).unwrap_err();
assert!(err.contains("no harn.toml"));
}
}