use std::path::{Path, PathBuf};
pub fn expand_placeholders(value: &str, plugin_root: &Path, plugin_data: &Path) -> String {
value
.replace("${PLUGIN_ROOT}", &plugin_root.display().to_string())
.replace("${PLUGIN_DATA}", &plugin_data.display().to_string())
}
pub fn validate_plugin_relative(value: &str, root: &Path) -> Result<PathBuf, crate::PluginError> {
if !value.starts_with("./") {
return Err(crate::PluginError::PathEscape(format!("path must start with ./: {}", value)));
}
let canonical_root = vtcode_commons::canonicalize(root)
.map_err(|e| crate::PluginError::PathEscape(format!("plugin root could not be resolved: {e}")))?;
let candidate = root.join(value.get(2..).unwrap_or_default());
let lexical_ok = candidate
.components()
.all(|component| !matches!(component, std::path::Component::ParentDir));
if !lexical_ok {
return Err(crate::PluginError::PathEscape(format!("path escapes plugin root: {}", value)));
}
let mut probe = candidate.as_path();
let mut suffix: Vec<PathBuf> = Vec::new();
loop {
match vtcode_commons::canonicalize(probe) {
Ok(existing) => {
let mut resolved = existing;
for part in suffix.iter().rev() {
resolved.push(part);
}
if !resolved.starts_with(&canonical_root) {
return Err(crate::PluginError::PathEscape(format!("path escapes plugin root: {}", value)));
}
return Ok(resolved);
}
Err(_) => {
let Some(name) = probe.file_name() else {
return Err(crate::PluginError::PathEscape(format!("path could not be resolved: {}", value)));
};
suffix.push(PathBuf::from(name));
let Some(parent) = probe.parent() else {
return Err(crate::PluginError::PathEscape(format!("path could not be resolved: {}", value)));
};
probe = parent;
}
}
}
}