Skip to main content

alef_component_runtime/
error.rs

1use std::path::PathBuf;
2
3#[derive(Debug, thiserror::Error)]
4pub enum ComponentError {
5    #[error("component `{component_id}` has no artifact for target `{target}`")]
6    ArtifactNotFound { component_id: String, target: String },
7    #[error(
8        "component `{component_id}` is not locked; the embedded components.lock.json is still \
9         the empty bootstrap lock `alef generate` writes before any artifact exists -- run \
10         `alef component lock` to populate it"
11    )]
12    ComponentsNotLocked { component_id: String },
13    #[error("component `{component}` is bundled for this target and cannot be downloaded")]
14    BundledComponentNotLoadable { component: String },
15    #[error("component `{component_id}` does not provide contract `{contract}`")]
16    ContractNotProvided { component_id: String, contract: String },
17    #[error("invalid SHA-256 digest `{0}`")]
18    InvalidDigest(String),
19    #[error("artifact digest mismatch: expected {expected}, got {actual}")]
20    DigestMismatch { expected: String, actual: String },
21    #[error("artifact size mismatch: expected {expected} bytes, got {actual} bytes")]
22    SizeMismatch { expected: u64, actual: u64 },
23    #[error("artifact signature is required")]
24    SignatureRequired,
25    #[error("invalid Ed25519 signature encoding")]
26    InvalidSignatureEncoding,
27    #[error("artifact signature verification failed")]
28    SignatureVerification,
29    #[error("component manifest is not in canonical JSON form")]
30    NonCanonicalManifest,
31    #[error("component manifest digest does not match the lock")]
32    ManifestDigestMismatch,
33    #[error("component manifest identity does not match the lock")]
34    ManifestIdentityMismatch,
35    #[error("component manifest schema or ABI version is unsupported")]
36    UnsupportedManifest,
37    #[error("component signature uses unsupported algorithm `{0}`")]
38    UnsupportedSignatureAlgorithm(String),
39    #[error("component signature key `{0}` is not embedded in the lock")]
40    UnknownSignatureKey(String),
41    #[error("component signature key does not match the lock entry")]
42    SignatureKeyMismatch,
43    #[error("unsafe archive entry `{0}`")]
44    UnsafeArchiveEntry(String),
45    #[error("component archive has more than {max} entries (found at least {actual})")]
46    TooManyArchiveEntries { max: usize, actual: usize },
47    #[error("extracted component archive entry exceeds the {limit}-byte size limit ({actual} bytes)")]
48    ExtractedSizeExceeded { limit: u64, actual: u64 },
49    #[error("artifact library path `{0}` is not a safe relative path")]
50    UnsafeLibraryPath(String),
51    #[error("component library is missing at `{0}`")]
52    MissingLibrary(PathBuf),
53    #[error("failed to download `{url}`: {message}")]
54    Download { url: String, message: String },
55    #[error("component `{component}` is not cached and network access is disabled (would fetch `{url}`)")]
56    Offline { component: String, url: String },
57    #[error("failed to load component library `{path}`: {message}")]
58    LibraryLoad { path: PathBuf, message: String },
59    #[error("component library does not export the requested entry point: {0}")]
60    MissingEntrypoint(String),
61    #[error("component entrypoint failed with status {0}")]
62    EntrypointFailed(i32),
63    #[error("component returned an invalid descriptor: {0}")]
64    InvalidDescriptor(&'static str),
65    #[error(
66        "component ABI {actual_major}.{actual_minor} is incompatible with host ABI {expected_major}.{expected_minor}"
67    )]
68    IncompatibleAbi {
69        expected_major: u32,
70        expected_minor: u32,
71        actual_major: u32,
72        actual_minor: u32,
73    },
74    #[error("component identity mismatch: expected `{expected}`, got `{actual}`")]
75    IdentityMismatch { expected: String, actual: String },
76    #[error("component contract hash does not match the requested contract")]
77    ContractHashMismatch,
78    #[error("component feature-set hash does not match the requested feature set")]
79    FeatureSetHashMismatch,
80    #[error("component lock contains duplicate entry `{component_id}` for target `{target}`")]
81    DuplicateLockEntry { component_id: String, target: String },
82    #[error("component lock schema version {0} is not supported")]
83    UnsupportedLockSchema(u32),
84    #[error("component lock public key is not a valid Ed25519 public key")]
85    InvalidPublicKey,
86    #[error("component contract table is too small or incorrectly aligned")]
87    InvalidContractTable,
88    #[error("component descriptor does not provide an instance factory")]
89    MissingInstanceFactory,
90    #[error("component descriptor does not provide an instance destructor")]
91    MissingInstanceDestructor,
92    #[error("component instance creation failed with status {status}: {message}")]
93    InstanceCreation { status: i32, message: String },
94    #[error("component instance factory returned a null handle")]
95    NullInstance,
96    #[error("component string is not valid UTF-8")]
97    InvalidUtf8,
98    #[error("component string exceeds the maximum supported length")]
99    StringTooLong,
100    #[error("JSON error: {0}")]
101    Json(#[from] serde_json::Error),
102    #[error("I/O error: {0}")]
103    Io(#[from] std::io::Error),
104}
105
106impl ComponentError {
107    /// A stable, language-agnostic identifier for this error's kind, independent of its
108    /// human-readable message and never changing across releases for the same variant.
109    /// Generated bindings prefix the message with this (see
110    /// `alef::backends::native_components`) so callers in any host language can distinguish,
111    /// for example, an offline failure from an invalid signature from an unsupported host
112    /// without parsing free-form text.
113    #[must_use]
114    pub fn code(&self) -> &'static str {
115        match self {
116            Self::ArtifactNotFound { .. } => "artifact_not_found",
117            Self::ComponentsNotLocked { .. } => "components_not_locked",
118            Self::BundledComponentNotLoadable { .. } => "bundled_component_not_loadable",
119            Self::ContractNotProvided { .. } => "contract_not_provided",
120            Self::InvalidDigest(_) => "invalid_digest",
121            Self::DigestMismatch { .. } => "digest_mismatch",
122            Self::SizeMismatch { .. } => "size_mismatch",
123            Self::SignatureRequired => "signature_required",
124            Self::InvalidSignatureEncoding => "invalid_signature_encoding",
125            Self::SignatureVerification => "signature_verification_failed",
126            Self::NonCanonicalManifest => "non_canonical_manifest",
127            Self::ManifestDigestMismatch => "manifest_digest_mismatch",
128            Self::ManifestIdentityMismatch => "manifest_identity_mismatch",
129            Self::UnsupportedManifest => "unsupported_manifest",
130            Self::UnsupportedSignatureAlgorithm(_) => "unsupported_signature_algorithm",
131            Self::UnknownSignatureKey(_) => "unknown_signature_key",
132            Self::SignatureKeyMismatch => "signature_key_mismatch",
133            Self::UnsafeArchiveEntry(_) => "unsafe_archive_entry",
134            Self::TooManyArchiveEntries { .. } => "too_many_archive_entries",
135            Self::ExtractedSizeExceeded { .. } => "extracted_size_exceeded",
136            Self::UnsafeLibraryPath(_) => "unsafe_library_path",
137            Self::MissingLibrary(_) => "missing_library",
138            Self::Download { .. } => "download_failed",
139            Self::Offline { .. } => "offline",
140            Self::LibraryLoad { .. } => "library_load_failed",
141            Self::MissingEntrypoint(_) => "missing_entrypoint",
142            Self::EntrypointFailed(_) => "entrypoint_failed",
143            Self::InvalidDescriptor(_) => "invalid_descriptor",
144            Self::IncompatibleAbi { .. } => "incompatible_abi",
145            Self::IdentityMismatch { .. } => "identity_mismatch",
146            Self::ContractHashMismatch => "contract_hash_mismatch",
147            Self::FeatureSetHashMismatch => "feature_set_hash_mismatch",
148            Self::DuplicateLockEntry { .. } => "duplicate_lock_entry",
149            Self::UnsupportedLockSchema(_) => "unsupported_lock_schema",
150            Self::InvalidPublicKey => "invalid_public_key",
151            Self::InvalidContractTable => "invalid_contract_table",
152            Self::MissingInstanceFactory => "missing_instance_factory",
153            Self::MissingInstanceDestructor => "missing_instance_destructor",
154            Self::InstanceCreation { .. } => "instance_creation_failed",
155            Self::NullInstance => "null_instance",
156            Self::InvalidUtf8 => "invalid_utf8",
157            Self::StringTooLong => "string_too_long",
158            Self::Json(_) => "json_error",
159            Self::Io(_) => "io_error",
160        }
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    #[test]
169    fn code_is_stable_and_distinct_for_representative_variants() {
170        assert_eq!(
171            ComponentError::Offline {
172                component: "demo".into(),
173                url: "https://example.invalid".into(),
174            }
175            .code(),
176            "offline"
177        );
178        assert_eq!(
179            ComponentError::SignatureVerification.code(),
180            "signature_verification_failed"
181        );
182        assert_eq!(
183            ComponentError::BundledComponentNotLoadable {
184                component: "demo".into()
185            }
186            .code(),
187            "bundled_component_not_loadable"
188        );
189    }
190
191    #[test]
192    fn code_never_appears_inside_its_own_display_message_by_accident() {
193        // Not a hard requirement, just documents that `code()` and `Display` are independent
194        // surfaces: generated bindings prefix the message with the code themselves rather
195        // than relying on it already being embedded.
196        let error = ComponentError::SignatureVerification;
197        assert!(!error.to_string().contains(error.code()));
198    }
199}