runx-runtime 0.6.19

Native Rust runtime for local runx execution, adapters, harness replay, receipts, and sandboxing.
Documentation
use std::path::Path;

use runx_contracts::sha256_hex;

use super::types::RegistryPackageFile;

pub(crate) fn normalize_registry_package_files(
    files: Vec<RegistryPackageFile>,
) -> Result<Vec<RegistryPackageFile>, String> {
    let mut normalized = Vec::with_capacity(files.len());
    for file in files {
        validate_registry_package_file_path(&file.path)?;
        if normalized
            .iter()
            .any(|entry: &RegistryPackageFile| entry.path == file.path)
        {
            return Err(format!("duplicate package file '{}'", file.path));
        }
        normalized.push(file);
    }
    normalized.sort_by(|left, right| left.path.cmp(&right.path));
    Ok(normalized)
}

pub(crate) fn registry_package_digest(files: &[RegistryPackageFile]) -> Option<String> {
    if files.is_empty() {
        return None;
    }
    let mut sorted = files.to_vec();
    sorted.sort_by(|left, right| left.path.cmp(&right.path));
    let mut canonical = String::from("{\"files\":[");
    for (index, file) in sorted.iter().enumerate() {
        if index > 0 {
            canonical.push(',');
        }
        canonical.push_str("{\"content\":");
        let content = match serde_json::to_string(&file.content) {
            Ok(value) => value,
            Err(_) => return None,
        };
        canonical.push_str(&content);
        canonical.push_str(",\"path\":");
        let path = match serde_json::to_string(&file.path) {
            Ok(value) => value,
            Err(_) => return None,
        };
        canonical.push_str(&path);
        canonical.push('}');
    }
    canonical.push_str("]}");
    Some(sha256_hex(canonical.as_bytes()))
}

pub(crate) fn validate_registry_package_file_path(path: &str) -> Result<(), String> {
    if path.is_empty() {
        return Err("package file path cannot be empty".to_owned());
    }
    if path.contains('\\') {
        return Err(format!("package file path '{path}' must use / separators"));
    }
    let parsed = Path::new(path);
    if parsed.is_absolute() {
        return Err(format!("package file path '{path}' must be relative"));
    }
    let mut depth = 0usize;
    for segment in path.split('/') {
        if segment.is_empty() || segment == "." || segment == ".." || segment.starts_with('.') {
            return Err(format!(
                "package file path '{path}' contains an unsafe segment"
            ));
        }
        depth += 1;
    }
    if depth == 1 && matches!(path, "SKILL.md" | "X.yaml") {
        return Err(format!("{path} is stored in its dedicated registry field"));
    }
    Ok(())
}