Skip to main content

hara_native/
native_link.rs

1//! Publication-time native crate composition for verified HARP package roots.
2
3use hara_abi::NativeIdentity;
4use std::collections::BTreeMap;
5use std::path::{Path, PathBuf};
6
7#[derive(Clone, Debug, PartialEq, Eq)]
8pub struct NativeArtifact {
9    pub identity: NativeIdentity,
10    pub version: String,
11    pub archive_sha256: String,
12    pub package_root: PathBuf,
13    pub crate_path: PathBuf,
14}
15
16impl NativeArtifact {
17    pub fn verified(
18        identity: NativeIdentity,
19        version: impl Into<String>,
20        archive_sha256: impl Into<String>,
21        package_root: impl Into<PathBuf>,
22        crate_relative: impl AsRef<Path>,
23    ) -> Result<Self, String> {
24        let version = version.into();
25        let archive_sha256 = archive_sha256.into();
26        if version.is_empty() {
27            return Err("native artifact version must not be empty".into());
28        }
29        let digest = archive_sha256
30            .strip_prefix("sha256:")
31            .ok_or("native artifact digest must start with sha256:")?;
32        if digest.len() != 64 || !digest.chars().all(|value| value.is_ascii_hexdigit()) {
33            return Err("native artifact digest must contain 64 hexadecimal characters".into());
34        }
35        let package_root = package_root.into();
36        let relative = crate_relative.as_ref();
37        if relative.is_absolute()
38            || relative
39                .components()
40                .any(|component| matches!(component, std::path::Component::ParentDir))
41        {
42            return Err("native crate path must remain beneath the package root".into());
43        }
44        let crate_path = package_root.join(relative);
45        if !crate_path.join("Cargo.toml").is_file() {
46            return Err(format!(
47                "native crate has no Cargo.toml: {}",
48                crate_path.display()
49            ));
50        }
51        Ok(Self {
52            identity,
53            version,
54            archive_sha256,
55            package_root,
56            crate_path,
57        })
58    }
59}
60
61#[derive(Clone, Debug, PartialEq, Eq)]
62pub struct LinkPlan {
63    pub artifacts: Vec<NativeArtifact>,
64}
65
66impl LinkPlan {
67    pub fn new(mut artifacts: Vec<NativeArtifact>) -> Result<Self, String> {
68        artifacts.sort_by(|left, right| left.identity.cmp(&right.identity));
69        for pair in artifacts.windows(2) {
70            if pair[0].identity == pair[1].identity {
71                return Err(format!(
72                    "duplicate native adapter {} {}",
73                    pair[0].identity.package, pair[0].identity.export
74                ));
75            }
76        }
77        Ok(Self { artifacts })
78    }
79
80    pub fn cargo_dependencies(&self) -> String {
81        let mut dependencies = BTreeMap::new();
82        for artifact in &self.artifacts {
83            dependencies.insert(
84                artifact.identity.crate_name.as_str(),
85                artifact.crate_path.to_string_lossy(),
86            );
87        }
88        let mut output = String::from("[dependencies]\n");
89        for (crate_name, path) in dependencies {
90            output.push_str(&format!(
91                "{crate_name} = {{ path = {:?} }}\n",
92                toml_string(&path)
93            ));
94        }
95        output
96    }
97
98    /// Generates deterministic installation statements for a composed host.
99    pub fn registration_source(&self, runtime_expression: &str) -> String {
100        let mut output = String::new();
101        for artifact in &self.artifacts {
102            let crate_name = artifact.identity.crate_name.replace('-', "_");
103            output.push_str(&format!(
104                "{runtime_expression}.install_native_module({crate_name}::module());\n"
105            ));
106        }
107        output
108    }
109}
110
111fn toml_string(value: &str) -> String {
112    value.replace('\\', "\\\\").replace('"', "\\\"")
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    use std::fs;
119    use std::time::{SystemTime, UNIX_EPOCH};
120
121    #[test]
122    fn verified_artifacts_generate_deterministic_cargo_dependencies() {
123        let root = std::env::temp_dir().join(format!(
124            "hara-native-link-{}",
125            SystemTime::now()
126                .duration_since(UNIX_EPOCH)
127                .unwrap()
128                .as_nanos()
129        ));
130        fs::create_dir_all(root.join("crate")).unwrap();
131        fs::write(
132            root.join("crate/Cargo.toml"),
133            "[package]\nname='adapter'\nversion='0.1.0'\n",
134        )
135        .unwrap();
136        let identity = NativeIdentity::new(
137            "gh:example:adapter",
138            "service/store",
139            "adapter",
140            "service/1",
141        )
142        .unwrap();
143        let artifact = NativeArtifact::verified(
144            identity,
145            "0.1.0",
146            format!("sha256:{}", "a".repeat(64)),
147            &root,
148            "crate",
149        )
150        .unwrap();
151        let plan = LinkPlan::new(vec![artifact]).unwrap();
152        assert!(plan.cargo_dependencies().contains("adapter = { path ="));
153        assert_eq!(
154            plan.registration_source("runtime"),
155            "runtime.install_native_module(adapter::module());\n"
156        );
157        fs::remove_dir_all(root).unwrap();
158    }
159}