use std::path::{Path, PathBuf};
pub const EXPORT_ROOT_ENV: &str = "KHIVE_SAVE_TO_ROOT";
pub fn export_root() -> anyhow::Result<PathBuf> {
let root = match std::env::var(EXPORT_ROOT_ENV) {
Ok(v) if !v.trim().is_empty() => PathBuf::from(v),
_ => {
let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
PathBuf::from(home).join(".khive").join("exports")
}
};
std::fs::create_dir_all(&root)
.map_err(|e| anyhow::anyhow!("create export root {}: {e}", root.display()))?;
root.canonicalize()
.map_err(|e| anyhow::anyhow!("canonicalize export root {}: {e}", root.display()))
}
pub fn validate_destination(root: &Path, requested: &Path) -> anyhow::Result<PathBuf> {
if requested.as_os_str().is_empty() {
anyhow::bail!("save_to path must not be empty");
}
if requested
.components()
.any(|c| matches!(c, std::path::Component::ParentDir))
{
anyhow::bail!(
"save_to path must not contain '..' traversal components: {}",
requested.display()
);
}
let joined = if requested.is_absolute() {
requested.to_path_buf()
} else {
root.join(requested)
};
let parent = joined.parent().filter(|p| !p.as_os_str().is_empty());
let parent = match parent {
Some(p) => p,
None => anyhow::bail!("save_to path has no parent directory: {}", joined.display()),
};
let mut existing = parent;
while !existing.exists() {
existing = match existing.parent().filter(|p| !p.as_os_str().is_empty()) {
Some(p) => p,
None => anyhow::bail!(
"save_to path has no existing ancestor: {}",
joined.display()
),
};
}
let canonical_existing = existing.canonicalize().map_err(|e| {
anyhow::anyhow!("canonicalize save_to ancestor {}: {e}", existing.display())
})?;
if !canonical_existing.starts_with(root) {
anyhow::bail!(
"save_to path escapes the allowed export root ({}): {}",
root.display(),
joined.display()
);
}
std::fs::create_dir_all(parent)
.map_err(|e| anyhow::anyhow!("create save_to parent dir {}: {e}", parent.display()))?;
let canonical_parent = parent
.canonicalize()
.map_err(|e| anyhow::anyhow!("canonicalize save_to parent {}: {e}", parent.display()))?;
if !canonical_parent.starts_with(root) {
anyhow::bail!(
"save_to path escapes the allowed export root ({}): {}",
root.display(),
joined.display()
);
}
let file_name = joined
.file_name()
.ok_or_else(|| anyhow::anyhow!("save_to path has no file name: {}", joined.display()))?;
let dest = canonical_parent.join(file_name);
if let Ok(meta) = std::fs::symlink_metadata(&dest) {
if meta.file_type().is_symlink() {
anyhow::bail!(
"save_to destination must not be a symlink: {}",
dest.display()
);
}
}
Ok(dest)
}
pub fn resolve_destination(path: &Path, restrict_to_export_root: bool) -> anyhow::Result<PathBuf> {
if path.as_os_str().is_empty() {
anyhow::bail!("save_to path must not be empty");
}
let destination = if restrict_to_export_root {
let root = export_root()?;
validate_destination(&root, path)
} else {
if let Some(parent) = path.parent() {
if !parent.as_os_str().is_empty() {
std::fs::create_dir_all(parent)
.map_err(|e| anyhow::anyhow!("create parent dir {}: {e}", parent.display()))?;
}
}
Ok(path.to_path_buf())
}?;
match std::fs::symlink_metadata(&destination) {
Ok(metadata) if !metadata.file_type().is_file() => anyhow::bail!(
"save_to destination must be absent or an existing regular file: {}",
destination.display()
),
Ok(_) => {}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => anyhow::bail!(
"inspect save_to destination {}: {error}",
destination.display()
),
}
Ok(destination)
}
pub const IMPORT_ROOT_ENV: &str = "KHIVE_IMPORT_FROM_ROOT";
pub fn import_root() -> anyhow::Result<PathBuf> {
let root = match std::env::var(IMPORT_ROOT_ENV) {
Ok(value) if !value.trim().is_empty() => PathBuf::from(value),
_ => {
let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
PathBuf::from(home).join(".khive").join("imports")
}
};
std::fs::create_dir_all(&root)
.map_err(|error| anyhow::anyhow!("create import root {}: {error}", root.display()))?;
root.canonicalize()
.map_err(|error| anyhow::anyhow!("canonicalize import root {}: {error}", root.display()))
}
pub fn confined_file_roots() -> anyhow::Result<(PathBuf, PathBuf)> {
let imports = import_root()?;
let exports = export_root()?;
if imports.starts_with(&exports) || exports.starts_with(&imports) {
anyhow::bail!("import and export roots must not be equal or nested");
}
Ok((imports, exports))
}
pub fn validate_import(root: &Path, requested: &Path) -> anyhow::Result<PathBuf> {
if requested.as_os_str().is_empty() {
anyhow::bail!("import path must not be empty");
}
if requested
.components()
.any(|component| matches!(component, std::path::Component::ParentDir))
{
anyhow::bail!("import path must not contain '..' traversal components");
}
let joined = if requested.is_absolute() {
requested.to_path_buf()
} else {
root.join(requested)
};
if !joined.starts_with(root) {
anyhow::bail!(
"import path escapes the allowed import root ({})",
root.display()
);
}
let mut current = root.to_path_buf();
for component in joined.strip_prefix(root)?.components() {
current.push(component);
let metadata = std::fs::symlink_metadata(¤t).map_err(|error| {
anyhow::anyhow!("inspect import path {}: {error}", current.display())
})?;
if metadata.file_type().is_symlink() {
anyhow::bail!(
"import path must not contain a symlink: {}",
current.display()
);
}
}
let canonical = joined.canonicalize().map_err(|error| {
anyhow::anyhow!("canonicalize import file {}: {error}", joined.display())
})?;
if !canonical.starts_with(root) {
anyhow::bail!(
"import path escapes the allowed import root ({})",
root.display()
);
}
if !std::fs::metadata(&canonical)?.is_file() {
anyhow::bail!("import source must be a regular file: {}", joined.display());
}
Ok(canonical)
}
pub fn open_import(root: &Path, requested: &Path) -> anyhow::Result<std::fs::File> {
let path = validate_import(root, requested)?;
let mut options = std::fs::OpenOptions::new();
options.read(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt as _;
options.custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK);
}
let file = options
.open(&path)
.map_err(|error| anyhow::anyhow!("open import source {}: {error}", path.display()))?;
if !file.metadata()?.is_file() {
anyhow::bail!("import source must be a regular file: {}", path.display());
}
Ok(file)
}