confium_api/plugin/hash.rs
1//! `HashPlugin` trait — the Rust-side counterpart of the hash v0 wire
2//! protocol.
3//!
4//! Plugin authors implement this trait on their hash state type, then
5//! apply `#[plugin_interface(name = "hash", version = 0)]` to the impl
6//! block. The macro emits the eight canonical `cfmp_hash_*` FFI symbols,
7//! one per trait method.
8//!
9//! Method → symbol mapping (see `crates/confium-core/src/ffi/hash.rs`
10//! for the loader-side wire types):
11//!
12//! | trait method | FFI symbol | purpose |
13//! |--------------|------------|---------|
14//! | [`HashPlugin::create_with_opts`] | `cfmp_hash_create` | construct a new instance |
15//! | [`HashPlugin::output_size`] | `cfmp_hash_output_size` | output length in bytes |
16//! | [`HashPlugin::block_size`] | `cfmp_hash_block_size` | internal block size in bytes |
17//! | [`HashPlugin::update`] | `cfmp_hash_update` | absorb input bytes |
18//! | [`HashPlugin::reset`] | `cfmp_hash_reset` | reset to initial state |
19//! | [`HashPlugin::try_clone`] | `cfmp_hash_clone` | duplicate the state |
20//! | [`HashPlugin::finalize`] | `cfmp_hash_finalize` | emit digest into caller buffer |
21//! | `Drop` | `cfmp_hash_destroy` | reclaim the boxed state |
22
23use crate::error::{PluginError, PluginResult};
24use crate::options::OptionView;
25
26/// Trait implemented by hash plugins. The macro-generated
27/// `cfmp_hash_create` calls [`HashPlugin::create_with_opts`]; all other
28/// symbols dispatch through `OpaqueHandle::<Self>::borrow_raw` and the
29/// corresponding trait method.
30///
31/// `update`, `reset`, `try_clone`, and `finalize` may return a
32/// [`PluginError`]; the macro maps the error into the wire status code
33/// (non-zero) that the loader surfaces via its `Error::PluginInternalError`
34/// variant.
35pub trait HashPlugin: Sized {
36 /// Construct a new hash instance for the named algorithm.
37 ///
38 /// `name` is the algorithm name the caller passed to
39 /// `cfm_hash_create` (e.g. `"sha-256"`). `opts` is the caller's
40 /// option map (or `None` if the caller passed NULL). Plugins that
41 /// don't take options can ignore both.
42 fn create_with_opts(name: &str, opts: Option<OptionView<'_>>) -> PluginResult<Self>;
43
44 /// Output length in bytes for this instance. The caller allocates a
45 /// buffer of this size before calling `finalize`.
46 fn output_size(&self) -> u32;
47
48 /// Internal block size in bytes. Used by callers that want to feed
49 /// input in aligned chunks; safe to return a constant (e.g. 64) if
50 /// the algorithm doesn't have a meaningful block size.
51 fn block_size(&self) -> u32;
52
53 /// Absorb `data` into the hash state.
54 fn update(&mut self, data: &[u8]) -> PluginResult<()>;
55
56 /// Reset to the initial state (post-`create`).
57 fn reset(&mut self) -> PluginResult<()>;
58
59 /// Duplicate the hash state. The clone must be independent: updates
60 /// to one must not affect the other.
61 fn try_clone(&self) -> PluginResult<Self>;
62
63 /// Write the finalized digest into `out`. The caller guarantees
64 /// `out.len() == self.output_size()`. Plugins that need to pad or
65 /// finalize internally should do so here.
66 fn finalize(&mut self, out: &mut [u8]) -> PluginResult<()>;
67}
68
69/// Trivial constructor entry point for plugin authors who don't need
70/// `opts`. Wraps [`HashPlugin::create_with_opts`] with `None`.
71///
72/// Plugin authors do not normally call this — the macro-generated
73/// `cfmp_hash_create` calls `create_with_opts` directly. It's provided
74/// as a convenience for tests and hand-written plugins.
75pub fn create_simple<T: HashPlugin>(name: &str) -> PluginResult<T> {
76 T::create_with_opts(name, None)
77}
78
79/// Default `block_size` implementation for hash plugins that don't have
80/// a meaningful block size. Returns 64, the common block size for
81/// SHA-2 family hashes.
82pub fn default_block_size() -> u32 {
83 64
84}
85
86#[doc(hidden)]
87/// Convenience used by macro-generated code to convert a `Result<T, E>`
88/// where `E: ToString` into a [`PluginResult<T>`].
89pub fn err_with_message<E: std::fmt::Display>(code: crate::ErrorCode, e: E) -> PluginError {
90 PluginError::new(code, e.to_string())
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 /// A trivial XOR-fold hash used by the test suite. Each byte of
98 /// input is XOR-folded into a single u8 state; `finalize` writes
99 /// that single byte to the first byte of the output buffer.
100 struct XorHash {
101 acc: u8,
102 }
103
104 impl HashPlugin for XorHash {
105 fn create_with_opts(_name: &str, _opts: Option<OptionView<'_>>) -> PluginResult<Self> {
106 Ok(XorHash { acc: 0 })
107 }
108
109 fn output_size(&self) -> u32 {
110 1
111 }
112
113 fn block_size(&self) -> u32 {
114 default_block_size()
115 }
116
117 fn update(&mut self, data: &[u8]) -> PluginResult<()> {
118 for &b in data {
119 self.acc ^= b;
120 }
121 Ok(())
122 }
123
124 fn reset(&mut self) -> PluginResult<()> {
125 self.acc = 0;
126 Ok(())
127 }
128
129 fn try_clone(&self) -> PluginResult<Self> {
130 Ok(XorHash { acc: self.acc })
131 }
132
133 fn finalize(&mut self, out: &mut [u8]) -> PluginResult<()> {
134 if out.is_empty() {
135 return Err(crate::error::PluginError::new(
136 crate::ErrorCode::INSUFFICIENT_BUFFER,
137 "output buffer too small",
138 ));
139 }
140 out[0] = self.acc;
141 Ok(())
142 }
143 }
144
145 #[test]
146 fn xor_hash_produces_xor_of_inputs() {
147 let mut h = XorHash::create_with_opts("xor", None).unwrap();
148 h.update(&[1, 2, 4]).unwrap();
149 let mut out = [0u8];
150 h.finalize(&mut out).unwrap();
151 assert_eq!(out[0], 1 ^ 2 ^ 4);
152 }
153
154 #[test]
155 fn xor_hash_clone_is_independent() {
156 let mut a = XorHash::create_with_opts("xor", None).unwrap();
157 a.update(&[0xFF]).unwrap();
158 let mut b = a.try_clone().unwrap();
159 a.update(&[0x01]).unwrap();
160 b.update(&[0x02]).unwrap();
161 let mut oa = [0u8];
162 let mut ob = [0u8];
163 a.finalize(&mut oa).unwrap();
164 b.finalize(&mut ob).unwrap();
165 assert_eq!(oa[0], 0xFF ^ 0x01);
166 assert_eq!(ob[0], 0xFF ^ 0x02);
167 }
168
169 #[test]
170 fn xor_hash_reset_clears_state() {
171 let mut h = XorHash::create_with_opts("xor", None).unwrap();
172 h.update(&[0xFF]).unwrap();
173 h.reset().unwrap();
174 let mut out = [0u8];
175 h.finalize(&mut out).unwrap();
176 assert_eq!(out[0], 0);
177 }
178}