nautalid 0.1.0

Scratch container substrate — TLS 1.3 HTTP/2+3 kernel, LID/AetherDB, optional filter bus (GPL-3.0-or-later).
use std::path::{Component, Path, PathBuf};

use super::ArtifactVerifyError;

/// Root directory for artifact `path` bindings (**`IMAGE_TRUST_FILE_ROOT`**, default **`/`**).
pub fn file_root_from_env() -> PathBuf {
    std::env::var("IMAGE_TRUST_FILE_ROOT")
        .ok()
        .filter(|s| !s.is_empty())
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from("/"))
}

/// Resolve a manifest `path` under **`root`**, rejecting `..` and paths that escape the jail.
pub fn resolve_under_root(rel: &str, root: &Path) -> Result<PathBuf, ArtifactVerifyError> {
    if rel.is_empty() {
        return Err(ArtifactVerifyError::UnsafePath {
            path: rel.to_string(),
            reason: "empty path".into(),
        });
    }
    if rel.contains('\0') {
        return Err(ArtifactVerifyError::UnsafePath {
            path: rel.to_string(),
            reason: "NUL byte in path".into(),
        });
    }

    let rel_path = Path::new(rel);
    if rel_path.is_absolute() {
        return Err(ArtifactVerifyError::UnsafePath {
            path: rel.to_string(),
            reason: "absolute paths are not allowed in manifest bindings".into(),
        });
    }

    for component in rel_path.components() {
        if matches!(component, Component::ParentDir) {
            return Err(ArtifactVerifyError::UnsafePath {
                path: rel.to_string(),
                reason: "parent-dir (..) components are not allowed".into(),
            });
        }
    }

    let joined = root.join(rel_path);
    let canonical_root = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
    let canonical = joined.canonicalize().map_err(|e| ArtifactVerifyError::Io {
        path: rel.to_string(),
        source: e,
    })?;

    if !canonical.starts_with(&canonical_root) {
        return Err(ArtifactVerifyError::UnsafePath {
            path: rel.to_string(),
            reason: format!("path escapes IMAGE_TRUST_FILE_ROOT ({})", root.display()),
        });
    }

    Ok(canonical)
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use tempfile::TempDir;

    #[test]
    fn rejects_parent_dir() {
        let root = PathBuf::from("/app");
        assert!(resolve_under_root("../etc/passwd", &root).is_err());
    }

    #[test]
    fn resolves_under_root() {
        let dir = TempDir::new().unwrap();
        let file = dir.path().join("bundle.wasm");
        fs::write(&file, b"test").unwrap();
        let resolved = resolve_under_root("bundle.wasm", dir.path()).unwrap();
        assert_eq!(resolved, file.canonicalize().unwrap());
    }
}