Skip to main content

hara_native/
package_hta_loader.rs

1//! Trusted native-host loader for resolver-selected generated HTA Wasm packages.
2//!
3//! HTA artifacts are selected only through the package `:require` route. The
4//! loader verifies the complete package tree and the selected artifact before
5//! Wasmtime sees any bytes.
6
7#![cfg(not(target_arch = "wasm32"))]
8
9use std::collections::BTreeSet;
10use std::fs;
11use std::path::Path;
12use std::rc::Rc;
13
14use crate::extension::{ExtensionManifest, Value, WasmAbi, WasmExtension};
15use crate::package_manifest::{
16    PackageArtifactType, PackageManifest, PackageRuntimeRequirements, PackageSelection,
17};
18use crate::wasmtime_provider::WasmtimeExtensionProvider;
19
20pub struct LoadedPackageHta {
21    pub identity: String,
22    pub entry_point: String,
23    pub extension: WasmExtension,
24}
25
26pub fn load_hta_package(
27    manifest: &PackageManifest,
28    package_root: &Path,
29    requirements: &PackageRuntimeRequirements,
30    extension_manifest_source: &str,
31) -> Result<LoadedPackageHta, String> {
32    let module = match manifest.wasm_imports.len() {
33        0 => {
34            return Err(
35                "package/missing-require-artifact: package declares no HTA artifacts".into(),
36            )
37        }
38        1 => manifest
39            .wasm_imports
40            .keys()
41            .next()
42            .cloned()
43            .expect("one HTA artifact"),
44        _ => {
45            return Err(
46                "package/ambiguous-require-artifact: package declares multiple HTA artifacts"
47                    .into(),
48            )
49        }
50    };
51    load_hta_require_package(
52        manifest,
53        package_root,
54        &module,
55        requirements,
56        extension_manifest_source,
57        None,
58    )
59}
60
61pub fn load_hta_require_package(
62    manifest: &PackageManifest,
63    package_root: &Path,
64    module: &str,
65    requirements: &PackageRuntimeRequirements,
66    extension_manifest_source: &str,
67    host_handler: Option<Rc<dyn Fn(String, String, Vec<Value>) -> Result<Value, String>>>,
68) -> Result<LoadedPackageHta, String> {
69    manifest
70        .verify_files_at(package_root)
71        .map_err(|error| error.to_string())?;
72    let selection = manifest
73        .select_hta_require(module, requirements)
74        .map_err(|error| error.to_string())?;
75    let PackageSelection::Variant(variant) = &selection else {
76        return Err("package/missing-artifact: portable package has no HTA artifact".into());
77    };
78    if variant.artifact.artifact_type != PackageArtifactType::Hta {
79        return Err(format!(
80            "package/artifact-type-mismatch: expected :hta, got :{}",
81            variant.artifact.artifact_type.keyword()
82        ));
83    }
84    if variant.artifact.abi != "hta.v1" {
85        return Err(format!(
86            "package/abi-mismatch: HTA loader does not support {}",
87            variant.artifact.abi
88        ));
89    }
90
91    let artifact_path = package_root.join(&variant.artifact.path);
92    let bytes = fs::read(&artifact_path).map_err(|error| {
93        format!(
94            "package/missing-artifact: cannot read {}: {error}",
95            variant.artifact.path.display()
96        )
97    })?;
98    manifest
99        .verify_artifact_bytes(&selection, &bytes)
100        .map_err(|error| error.to_string())?;
101
102    let extension_manifest = ExtensionManifest::parse(extension_manifest_source, "package")?;
103    if extension_manifest.identity.as_deref() != Some(manifest.identity.as_str()) {
104        return Err("package/identity-mismatch: extension identity differs from package".into());
105    }
106    if extension_manifest.provider != "wasm" {
107        return Err("package/provider-mismatch: HTA artifact requires :provider :wasm".into());
108    }
109    if extension_manifest.abi != WasmAbi::HtaV1 {
110        return Err(
111            "package/abi-mismatch: extension manifest differs from selected variant".into(),
112        );
113    }
114    let required_capabilities = extension_manifest
115        .capabilities
116        .iter()
117        .cloned()
118        .collect::<BTreeSet<_>>();
119    if required_capabilities != variant.required_capabilities {
120        return Err(
121            "package/manifest-mismatch: required capabilities differ from extension".into(),
122        );
123    }
124    let declared_host_calls = extension_manifest
125        .host_calls
126        .iter()
127        .flat_map(|(service, methods)| {
128            methods
129                .iter()
130                .map(move |method| format!("{service}/{method}"))
131        })
132        .collect::<BTreeSet<_>>();
133    if declared_host_calls != variant.host_calls {
134        return Err("package/manifest-mismatch: declared host calls differ from extension".into());
135    }
136    if !variant.exports.iter().all(|export| {
137        extension_manifest
138            .exports
139            .iter()
140            .any(|(declared, _)| declared == export)
141    }) {
142        return Err(
143            "package/manifest-mismatch: selected exports are not declared by extension".into(),
144        );
145    }
146    let artifact_path = variant.artifact.path.to_string_lossy();
147    let library_path = extension_manifest
148        .assets
149        .iter()
150        .find(|path| path.ends_with(".wasm") && path.as_str() != artifact_path)
151        .cloned();
152    let provider = if let Some(library_path) = library_path {
153        let library_bytes = fs::read(package_root.join(&library_path)).map_err(|error| {
154            format!(
155                "package/missing-library: cannot read {}: {error}",
156                library_path
157            )
158        })?;
159        WasmtimeExtensionProvider::compile_hta_with_library(&bytes, &library_bytes, host_handler)?
160    } else {
161        WasmtimeExtensionProvider::compile_hta_with_host_handler(&bytes, host_handler)?
162    };
163    let extension = WasmExtension::new(extension_manifest, provider)?;
164    Ok(LoadedPackageHta {
165        identity: manifest.identity.clone(),
166        entry_point: variant.artifact.entry_point.clone(),
167        extension,
168    })
169}