Skip to main content

actr_pack/
manifest.rs

1use serde::{Deserialize, Serialize};
2
3/// Package manifest, parsed from manifest.toml inside .actr package.
4#[derive(Debug, Clone, Serialize, Deserialize)]
5pub struct PackageManifest {
6    pub manufacturer: String,
7    pub name: String,
8    pub version: String,
9    pub binary: BinaryEntry,
10    #[serde(default = "default_sig_algorithm")]
11    pub signature_algorithm: String,
12    /// ID of the public key used for signing (allows key rotation and lookup of historical keys).
13    #[serde(skip_serializing_if = "Option::is_none")]
14    pub signing_key_id: Option<String>,
15    #[serde(default)]
16    pub resources: Vec<ResourceEntry>,
17    /// Proto files included in the package for service API definition.
18    #[serde(default)]
19    pub proto_files: Vec<ProtoFileEntry>,
20    /// Optional workload dependency lock file packaged with the workload.
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub lock_file: Option<LockFileEntry>,
23    #[serde(default)]
24    pub metadata: ManifestMetadata,
25}
26
27fn default_sig_algorithm() -> String {
28    "ed25519".to_string()
29}
30
31/// Shape of the binary carried by an .actr package.
32///
33/// Phase 1 introduces the Component Model as the only supported wasm shape;
34/// older `core-module` packages were valid before the host rewrite in
35/// Phase 1 Commit 2 and fail to load against the new wasmtime
36/// `Component::from_binary` path. We keep the enum open to future variants
37/// (native cdylib, etc.) by encoding it as a lowercase kebab-case string
38/// rather than a closed numeric code.
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
40#[serde(rename_all = "kebab-case")]
41pub enum BinaryKind {
42    /// Legacy core wasm module (`wasm_core_module`). No longer loadable —
43    /// retained in the enum so the verifier can produce a helpful
44    /// migration error rather than a generic "unknown kind".
45    CoreModule,
46    /// Component Model component (wasip2 canonical ABI).
47    Component,
48    /// Native shared library (cdylib); loaded via the dynclib engine.
49    NativeCdylib,
50}
51
52impl BinaryKind {
53    /// Default binary kind for manifests that predate the Phase-1
54    /// `binary.kind` field. Old packages were always core-modules on the
55    /// wasm side; assuming that keeps the error path crisp ("this is a
56    /// pre-Phase-1 package") rather than silently upgrading.
57    pub(crate) fn legacy_default_for(target: &str) -> Self {
58        if target.starts_with("wasm32-") {
59            BinaryKind::CoreModule
60        } else {
61            BinaryKind::NativeCdylib
62        }
63    }
64}
65
66#[derive(Debug, Clone, Default, Serialize, Deserialize)]
67pub struct BinaryEntry {
68    pub path: String,
69    pub target: String,
70    /// SHA-256 hash hex string (64 chars)
71    pub hash: String,
72    pub size: Option<u64>,
73    /// Binary shape marker.
74    ///
75    /// Added in Phase 1 alongside the Component Model rewrite. Old
76    /// packages lack the field; [`BinaryEntry::resolved_kind`] applies
77    /// the legacy default so the verifier can produce a clear
78    /// migration-pointing error without introducing a second
79    /// pre-Phase-1 code path.
80    #[serde(default, skip_serializing_if = "Option::is_none")]
81    pub kind: Option<BinaryKind>,
82}
83
84impl BinaryEntry {
85    /// Decode the hex-encoded SHA-256 hash string into a 32-byte array.
86    ///
87    /// Returns [`PackError::ManifestParseError`] if the stored `hash` is not a
88    /// 64-character hex string.
89    pub fn hash_bytes(&self) -> Result<[u8; 32], crate::error::PackError> {
90        let hex = &self.hash;
91        if hex.len() != 64 {
92            return Err(crate::error::PackError::ManifestParseError(
93                "binary.hash must be a 64-character hex string (32 bytes)".to_string(),
94            ));
95        }
96        let mut out = [0u8; 32];
97        for (i, chunk) in hex.as_bytes().chunks(2).enumerate() {
98            let s = std::str::from_utf8(chunk).map_err(|_| {
99                crate::error::PackError::ManifestParseError(
100                    "binary.hash contains non-UTF-8 characters".to_string(),
101                )
102            })?;
103            out[i] = u8::from_str_radix(s, 16).map_err(|_| {
104                crate::error::PackError::ManifestParseError(
105                    "binary.hash contains invalid hex characters".to_string(),
106                )
107            })?;
108        }
109        Ok(out)
110    }
111
112    /// Returns `true` when this binary targets a WASM runtime (e.g.
113    /// `wasm32-wasip2`, `wasm32-wasip1`, `wasm32-unknown-unknown`).
114    pub fn is_wasm_target(&self) -> bool {
115        self.target.starts_with("wasm32-")
116    }
117
118    /// Return the declared kind, falling back to the legacy default for
119    /// manifests packaged before Phase 1 introduced the `kind` field.
120    pub fn resolved_kind(&self) -> BinaryKind {
121        self.kind
122            .unwrap_or_else(|| BinaryKind::legacy_default_for(&self.target))
123    }
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct ResourceEntry {
128    pub path: String,
129    /// SHA-256 hash hex string (64 chars)
130    pub hash: String,
131}
132
133/// Entry for a proto file included in the .actr package.
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct ProtoFileEntry {
136    /// File name (e.g. "echo.proto")
137    pub name: String,
138    /// Path inside the ZIP (e.g. "proto/echo.proto")
139    pub path: String,
140    /// SHA-256 hash hex string (64 chars)
141    pub hash: String,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct LockFileEntry {
146    /// Path inside the ZIP (always `manifest.lock.toml` for packaged workloads).
147    pub path: String,
148    /// SHA-256 hash hex string (64 chars)
149    pub hash: String,
150}
151
152#[derive(Debug, Clone, Default, Serialize, Deserialize)]
153pub struct ManifestMetadata {
154    pub description: Option<String>,
155    pub license: Option<String>,
156}
157
158impl PackageManifest {
159    /// Full type string: manufacturer:name:version
160    pub fn actr_type_str(&self) -> String {
161        format!("{}:{}:{}", self.manufacturer, self.name, self.version)
162    }
163
164    /// Parse from TOML string
165    pub fn from_toml(s: &str) -> Result<Self, crate::error::PackError> {
166        toml::from_str(s).map_err(|e| crate::error::PackError::ManifestParseError(e.to_string()))
167    }
168
169    /// Serialize to TOML string
170    pub fn to_toml(&self) -> Result<String, crate::error::PackError> {
171        toml::to_string_pretty(self)
172            .map_err(|e| crate::error::PackError::ManifestParseError(e.to_string()))
173    }
174}