Skip to main content

freenet_stdlib/
delegate_host.rs

1//! Host function API for delegates.
2//!
3//! This module provides synchronous access to delegate context, secrets, and
4//! contract state via host functions, eliminating the need for message round-trips.
5//!
6//! # Example
7//!
8//! ```ignore
9//! use freenet_stdlib::prelude::*;
10//!
11//! #[delegate]
12//! impl DelegateInterface for MyDelegate {
13//!     fn process(
14//!         ctx: &mut DelegateCtx,
15//!         _params: Parameters<'static>,
16//!         _attested: Option<&'static [u8]>,
17//!         message: InboundDelegateMsg,
18//!     ) -> Result<Vec<OutboundDelegateMsg>, DelegateError> {
19//!         // Read/write temporary context
20//!         let data = ctx.read();
21//!         ctx.write(b"new state");
22//!
23//!         // Access persistent secrets
24//!         if let Some(key) = ctx.get_secret(b"private_key") {
25//!             // use key...
26//!         }
27//!         ctx.set_secret(b"new_secret", b"value");
28//!
29//!         // V2: Direct contract access (no round-trips!)
30//!         let contract_id = [0u8; 32]; // your contract instance ID
31//!         if let Some(state) = ctx.get_contract_state(&contract_id) {
32//!             // process state...
33//!         }
34//!         ctx.put_contract_state(&contract_id, b"new state");
35//!
36//!         Ok(vec![])
37//!     }
38//! }
39//! ```
40//!
41//! # Context vs Secrets vs Contracts
42//!
43//! - **Context** (`read`/`write`): Temporary state within a single message batch.
44//!   Reset between separate runtime calls. Use for intermediate processing state.
45//!
46//! - **Secrets** (`get_secret`/`set_secret`): Persistent encrypted storage.
47//!   Survives across all delegate invocations. Use for private keys, tokens, etc.
48//!
49//! - **Contracts** (`get_contract_state`/`put_contract_state`/`update_contract_state`/
50//!   `subscribe_contract`): V2 host functions for direct contract state access.
51//!   Synchronous local reads/writes — no request/response round-trips.
52//!
53//! # Error Codes
54//!
55//! Host functions return negative values to indicate errors:
56//!
57//! | Code | Meaning |
58//! |------|---------|
59//! | 0    | Success |
60//! | -1   | Called outside process() context |
61//! | -2   | Secret not found |
62//! | -3   | Storage operation failed |
63//! | -4   | Invalid parameter (e.g., negative length) |
64//! | -5   | Context too large (exceeds i32::MAX) |
65//! | -6   | Buffer too small |
66//! | -7   | Contract not found in local store |
67//! | -8   | Internal state store error |
68//! | -9   | WASM memory bounds violation |
69//! | -10  | Contract code not registered |
70//!
71//! The wrapper methods in [`DelegateCtx`] handle these error codes and present
72//! a more ergonomic API.
73
74/// Error codes returned by host functions.
75///
76/// Negative values indicate errors, non-negative values indicate success
77/// (usually the number of bytes read/written).
78pub mod error_codes {
79    /// Operation succeeded.
80    pub const SUCCESS: i32 = 0;
81    /// Called outside of a process() context.
82    pub const ERR_NOT_IN_PROCESS: i32 = -1;
83    /// Secret not found.
84    pub const ERR_SECRET_NOT_FOUND: i32 = -2;
85    /// Storage operation failed.
86    pub const ERR_STORAGE_FAILED: i32 = -3;
87    /// Invalid parameter (e.g., negative length).
88    pub const ERR_INVALID_PARAM: i32 = -4;
89    /// Context too large (exceeds i32::MAX).
90    pub const ERR_CONTEXT_TOO_LARGE: i32 = -5;
91    /// Buffer too small to hold the data.
92    pub const ERR_BUFFER_TOO_SMALL: i32 = -6;
93    /// Contract not found in local store.
94    pub const ERR_CONTRACT_NOT_FOUND: i32 = -7;
95    /// Internal state store error.
96    pub const ERR_STORE_ERROR: i32 = -8;
97    /// WASM memory bounds violation (pointer/length out of range).
98    pub const ERR_MEMORY_BOUNDS: i32 = -9;
99    /// Contract code not registered in the index.
100    pub const ERR_CONTRACT_CODE_NOT_REGISTERED: i32 = -10;
101    /// Delegate creation depth limit exceeded.
102    pub const ERR_DEPTH_EXCEEDED: i32 = -20;
103    /// Per-call delegate creation limit exceeded.
104    pub const ERR_CREATIONS_EXCEEDED: i32 = -21;
105    /// Invalid WASM module (failed to construct DelegateContainer).
106    pub const ERR_INVALID_WASM: i32 = -23;
107    /// Failed to register delegate in secret/delegate store.
108    pub const ERR_STORE_FAILED: i32 = -24;
109}
110
111// ============================================================================
112// Host function declarations (WASM only)
113// ============================================================================
114
115#[cfg(target_family = "wasm")]
116#[link(wasm_import_module = "freenet_delegate_ctx")]
117extern "C" {
118    /// Returns the current context length in bytes, or negative error code.
119    fn __frnt__delegate__ctx_len() -> i32;
120    /// Reads context into the buffer at `ptr` (max `len` bytes). Returns bytes written, or negative error code.
121    fn __frnt__delegate__ctx_read(ptr: i64, len: i32) -> i32;
122    /// Writes `len` bytes from `ptr` into the context, replacing existing content. Returns 0 on success, or negative error code.
123    fn __frnt__delegate__ctx_write(ptr: i64, len: i32) -> i32;
124}
125
126#[cfg(target_family = "wasm")]
127#[link(wasm_import_module = "freenet_delegate_secrets")]
128extern "C" {
129    /// Get a secret. Returns bytes written to `out_ptr`, or negative error code.
130    fn __frnt__delegate__get_secret(key_ptr: i64, key_len: i32, out_ptr: i64, out_len: i32) -> i32;
131    /// Get secret length without fetching value. Returns length, or negative error code.
132    fn __frnt__delegate__get_secret_len(key_ptr: i64, key_len: i32) -> i32;
133    /// Store a secret. Returns 0 on success, or negative error code.
134    fn __frnt__delegate__set_secret(key_ptr: i64, key_len: i32, val_ptr: i64, val_len: i32) -> i32;
135    /// Check if a secret exists. Returns 1 if yes, 0 if no, or negative error code.
136    fn __frnt__delegate__has_secret(key_ptr: i64, key_len: i32) -> i32;
137    /// Remove a secret. Returns 0 on success, or negative error code.
138    fn __frnt__delegate__remove_secret(key_ptr: i64, key_len: i32) -> i32;
139    /// Length (in bytes) of the serialized key list for all stored secret keys
140    /// whose raw key starts with the `prefix_len`-byte prefix at `prefix_ptr`
141    /// (an empty prefix matches every key). Returns the byte count to allocate
142    /// before calling `__frnt__delegate__list_secrets`, or a negative error code.
143    fn __frnt__delegate__list_secrets_len(prefix_ptr: i64, prefix_len: i32) -> i32;
144    /// Enumerate stored secret keys matching the prefix. Writes a length-prefixed
145    /// list to `out_ptr` (max `out_len` bytes): each record is a 4-byte
146    /// little-endian length followed by that many key bytes. Returns the number
147    /// of bytes written, or a negative error code.
148    fn __frnt__delegate__list_secrets(
149        prefix_ptr: i64,
150        prefix_len: i32,
151        out_ptr: i64,
152        out_len: i32,
153    ) -> i32;
154}
155
156#[cfg(target_family = "wasm")]
157#[link(wasm_import_module = "freenet_delegate_contracts")]
158extern "C" {
159    /// Get contract state length. Returns byte count, or negative error code (i64).
160    fn __frnt__delegate__get_contract_state_len(id_ptr: i64, id_len: i32) -> i64;
161    /// Get contract state. Returns byte count written, or negative error code (i64).
162    fn __frnt__delegate__get_contract_state(
163        id_ptr: i64,
164        id_len: i32,
165        out_ptr: i64,
166        out_len: i64,
167    ) -> i64;
168    /// Put (store) contract state. Returns 0 on success, or negative error code (i64).
169    fn __frnt__delegate__put_contract_state(
170        id_ptr: i64,
171        id_len: i32,
172        state_ptr: i64,
173        state_len: i64,
174    ) -> i64;
175    /// Update contract state (requires existing state). Returns 0 on success, or negative error code (i64).
176    fn __frnt__delegate__update_contract_state(
177        id_ptr: i64,
178        id_len: i32,
179        state_ptr: i64,
180        state_len: i64,
181    ) -> i64;
182    /// Subscribe to contract updates. Returns 0 on success, or negative error code (i64).
183    fn __frnt__delegate__subscribe_contract(id_ptr: i64, id_len: i32) -> i64;
184}
185
186#[cfg(target_family = "wasm")]
187#[link(wasm_import_module = "freenet_delegate_management")]
188extern "C" {
189    /// Create a new delegate from WASM code + parameters.
190    /// Returns 0 on success, negative error code on failure.
191    /// On success, writes 32 bytes to out_key_ptr and 32 bytes to out_hash_ptr.
192    fn __frnt__delegate__create_delegate(
193        wasm_ptr: i64,
194        wasm_len: i64,
195        params_ptr: i64,
196        params_len: i64,
197        cipher_ptr: i64,
198        nonce_ptr: i64,
199        out_key_ptr: i64,
200        out_hash_ptr: i64,
201    ) -> i32;
202}
203
204// ============================================================================
205// DelegateCtx - Unified handle to context, secrets, and contracts
206// ============================================================================
207
208/// Opaque handle to the delegate's execution environment.
209///
210/// Provides access to:
211/// - **Temporary context**: State shared within a single message batch (reset between calls)
212/// - **Persistent secrets**: Encrypted storage that survives across all invocations
213/// - **Contract state** (V2): Direct synchronous access to local contract state
214///
215/// # Context Methods
216/// - [`read`](Self::read), [`write`](Self::write), [`len`](Self::len), [`clear`](Self::clear)
217///
218/// # Secret Methods
219/// - [`get_secret`](Self::get_secret), [`set_secret`](Self::set_secret),
220///   [`has_secret`](Self::has_secret), [`remove_secret`](Self::remove_secret)
221///
222/// # Contract Methods (V2)
223/// - [`get_contract_state`](Self::get_contract_state),
224///   [`put_contract_state`](Self::put_contract_state),
225///   [`update_contract_state`](Self::update_contract_state),
226///   [`subscribe_contract`](Self::subscribe_contract)
227///
228/// # Delegate Management Methods (V2)
229/// - [`create_delegate`](Self::create_delegate)
230#[derive(Default)]
231#[repr(transparent)]
232pub struct DelegateCtx {
233    _private: (),
234}
235
236impl DelegateCtx {
237    /// Creates the context handle.
238    ///
239    /// # Safety
240    ///
241    /// This should only be called by macro-generated code when the runtime
242    /// has set up the delegate execution environment.
243    #[doc(hidden)]
244    pub unsafe fn __new() -> Self {
245        Self { _private: () }
246    }
247
248    // ========================================================================
249    // Context methods (temporary state within a batch)
250    // ========================================================================
251
252    /// Returns the current context length in bytes.
253    #[inline]
254    pub fn len(&self) -> usize {
255        #[cfg(target_family = "wasm")]
256        {
257            let len = unsafe { __frnt__delegate__ctx_len() };
258            if len < 0 {
259                0
260            } else {
261                len as usize
262            }
263        }
264        #[cfg(not(target_family = "wasm"))]
265        {
266            0
267        }
268    }
269
270    /// Returns `true` if the context is empty.
271    #[inline]
272    pub fn is_empty(&self) -> bool {
273        self.len() == 0
274    }
275
276    /// Read the current context bytes.
277    ///
278    /// Returns an empty `Vec` if no context has been written.
279    pub fn read(&self) -> Vec<u8> {
280        #[cfg(target_family = "wasm")]
281        {
282            let len = unsafe { __frnt__delegate__ctx_len() };
283            if len <= 0 {
284                return Vec::new();
285            }
286            let mut buf = vec![0u8; len as usize];
287            let read = unsafe { __frnt__delegate__ctx_read(buf.as_mut_ptr() as i64, len) };
288            buf.truncate(read.max(0) as usize);
289            buf
290        }
291        #[cfg(not(target_family = "wasm"))]
292        {
293            Vec::new()
294        }
295    }
296
297    /// Read context into a provided buffer.
298    ///
299    /// Returns the number of bytes actually read.
300    pub fn read_into(&self, buf: &mut [u8]) -> usize {
301        #[cfg(target_family = "wasm")]
302        {
303            let read =
304                unsafe { __frnt__delegate__ctx_read(buf.as_mut_ptr() as i64, buf.len() as i32) };
305            read.max(0) as usize
306        }
307        #[cfg(not(target_family = "wasm"))]
308        {
309            let _ = buf;
310            0
311        }
312    }
313
314    /// Write new context bytes, replacing any existing content.
315    ///
316    /// Returns `true` on success, `false` on error.
317    pub fn write(&mut self, data: &[u8]) -> bool {
318        #[cfg(target_family = "wasm")]
319        {
320            let result =
321                unsafe { __frnt__delegate__ctx_write(data.as_ptr() as i64, data.len() as i32) };
322            result == 0
323        }
324        #[cfg(not(target_family = "wasm"))]
325        {
326            let _ = data;
327            false
328        }
329    }
330
331    /// Clear the context.
332    #[inline]
333    pub fn clear(&mut self) {
334        self.write(&[]);
335    }
336
337    // ========================================================================
338    // Secret methods (persistent encrypted storage)
339    // ========================================================================
340
341    /// Get the length of a secret without retrieving its value.
342    ///
343    /// Returns `None` if the secret does not exist.
344    pub fn get_secret_len(&self, key: &[u8]) -> Option<usize> {
345        #[cfg(target_family = "wasm")]
346        {
347            let result =
348                unsafe { __frnt__delegate__get_secret_len(key.as_ptr() as i64, key.len() as i32) };
349            if result < 0 {
350                None
351            } else {
352                Some(result as usize)
353            }
354        }
355        #[cfg(not(target_family = "wasm"))]
356        {
357            let _ = key;
358            None
359        }
360    }
361
362    /// Get a secret by key.
363    ///
364    /// Returns `None` if the secret does not exist.
365    pub fn get_secret(&self, key: &[u8]) -> Option<Vec<u8>> {
366        #[cfg(target_family = "wasm")]
367        {
368            // First get the length to allocate the right buffer size
369            let len = self.get_secret_len(key)?;
370
371            if len == 0 {
372                return Some(Vec::new());
373            }
374
375            let mut out = vec![0u8; len];
376            let result = unsafe {
377                __frnt__delegate__get_secret(
378                    key.as_ptr() as i64,
379                    key.len() as i32,
380                    out.as_mut_ptr() as i64,
381                    out.len() as i32,
382                )
383            };
384            if result < 0 {
385                None
386            } else {
387                out.truncate(result as usize);
388                Some(out)
389            }
390        }
391        #[cfg(not(target_family = "wasm"))]
392        {
393            let _ = key;
394            None
395        }
396    }
397
398    /// Store a secret.
399    ///
400    /// Returns `true` on success, `false` on error.
401    pub fn set_secret(&mut self, key: &[u8], value: &[u8]) -> bool {
402        #[cfg(target_family = "wasm")]
403        {
404            let result = unsafe {
405                __frnt__delegate__set_secret(
406                    key.as_ptr() as i64,
407                    key.len() as i32,
408                    value.as_ptr() as i64,
409                    value.len() as i32,
410                )
411            };
412            result == 0
413        }
414        #[cfg(not(target_family = "wasm"))]
415        {
416            let _ = (key, value);
417            false
418        }
419    }
420
421    /// Check if a secret exists.
422    pub fn has_secret(&self, key: &[u8]) -> bool {
423        #[cfg(target_family = "wasm")]
424        {
425            let result =
426                unsafe { __frnt__delegate__has_secret(key.as_ptr() as i64, key.len() as i32) };
427            result == 1
428        }
429        #[cfg(not(target_family = "wasm"))]
430        {
431            let _ = key;
432            false
433        }
434    }
435
436    /// Remove a secret.
437    ///
438    /// Returns `true` if the secret was removed, `false` if it didn't exist.
439    pub fn remove_secret(&mut self, key: &[u8]) -> bool {
440        #[cfg(target_family = "wasm")]
441        {
442            let result =
443                unsafe { __frnt__delegate__remove_secret(key.as_ptr() as i64, key.len() as i32) };
444            result == 0
445        }
446        #[cfg(not(target_family = "wasm"))]
447        {
448            let _ = key;
449            false
450        }
451    }
452
453    /// Enumerate the keys of every secret this delegate has stored whose raw
454    /// key begins with `prefix` (pass an empty slice to list all keys).
455    ///
456    /// Returns the matching raw keys (the same byte strings originally passed
457    /// to [`set_secret`](Self::set_secret)). Order is unspecified. The host
458    /// caps the number of keys returned; if storage holds more matching keys
459    /// than the cap, the list is truncated (callers needing exhaustive
460    /// enumeration should narrow the prefix).
461    ///
462    /// This closes the gap that previously forced apps storing an open-ended
463    /// key family (e.g. `room:<owner_vk>`) to maintain their own key registry:
464    /// after a delegate-WASM rebuild the delegate can now rediscover what it
465    /// has stored instead of probing a hardcoded key set.
466    pub fn list_secrets(&self, prefix: &[u8]) -> Vec<Vec<u8>> {
467        #[cfg(target_family = "wasm")]
468        {
469            let len = unsafe {
470                __frnt__delegate__list_secrets_len(prefix.as_ptr() as i64, prefix.len() as i32)
471            };
472            if len <= 0 {
473                // Negative => error; zero => no matching keys. Either way the
474                // caller gets an empty list (errors are non-fatal: enumeration
475                // is advisory).
476                return Vec::new();
477            }
478            let mut out = vec![0u8; len as usize];
479            let written = unsafe {
480                __frnt__delegate__list_secrets(
481                    prefix.as_ptr() as i64,
482                    prefix.len() as i32,
483                    out.as_mut_ptr() as i64,
484                    out.len() as i32,
485                )
486            };
487            if written < 0 {
488                return Vec::new();
489            }
490            out.truncate(written as usize);
491            decode_secret_key_list(&out)
492        }
493        #[cfg(not(target_family = "wasm"))]
494        {
495            let _ = prefix;
496            Vec::new()
497        }
498    }
499
500    // ========================================================================
501    // Contract methods (V2 — direct synchronous access)
502    // ========================================================================
503
504    /// Get contract state by instance ID.
505    ///
506    /// Returns `Some(state_bytes)` if the contract exists locally,
507    /// `None` if not found or on error.
508    ///
509    /// Uses a two-step protocol: first queries the state length, then reads
510    /// the state bytes into an allocated buffer.
511    pub fn get_contract_state(&self, instance_id: &[u8; 32]) -> Option<Vec<u8>> {
512        #[cfg(target_family = "wasm")]
513        {
514            // Step 1: Get the state length
515            let len = unsafe {
516                __frnt__delegate__get_contract_state_len(instance_id.as_ptr() as i64, 32)
517            };
518            if len < 0 {
519                return None;
520            }
521            let len = len as usize;
522            if len == 0 {
523                return Some(Vec::new());
524            }
525
526            // Step 2: Read the state bytes
527            let mut buf = vec![0u8; len];
528            let read = unsafe {
529                __frnt__delegate__get_contract_state(
530                    instance_id.as_ptr() as i64,
531                    32,
532                    buf.as_mut_ptr() as i64,
533                    buf.len() as i64,
534                )
535            };
536            if read < 0 {
537                None
538            } else {
539                buf.truncate(read as usize);
540                Some(buf)
541            }
542        }
543        #[cfg(not(target_family = "wasm"))]
544        {
545            let _ = instance_id;
546            None
547        }
548    }
549
550    /// Store (PUT) contract state by instance ID.
551    ///
552    /// The contract's code must already be registered in the runtime's contract
553    /// store. Returns `true` on success, `false` on error.
554    pub fn put_contract_state(&mut self, instance_id: &[u8; 32], state: &[u8]) -> bool {
555        #[cfg(target_family = "wasm")]
556        {
557            let result = unsafe {
558                __frnt__delegate__put_contract_state(
559                    instance_id.as_ptr() as i64,
560                    32,
561                    state.as_ptr() as i64,
562                    state.len() as i64,
563                )
564            };
565            result == 0
566        }
567        #[cfg(not(target_family = "wasm"))]
568        {
569            let _ = (instance_id, state);
570            false
571        }
572    }
573
574    /// Update contract state by instance ID.
575    ///
576    /// Like `put_contract_state`, but only succeeds if the contract already has
577    /// stored state. This performs a full state replacement (not a delta-based
578    /// update through the contract's `update_state` logic). Returns `true` on
579    /// success, `false` if no prior state exists or on other errors.
580    pub fn update_contract_state(&mut self, instance_id: &[u8; 32], state: &[u8]) -> bool {
581        #[cfg(target_family = "wasm")]
582        {
583            let result = unsafe {
584                __frnt__delegate__update_contract_state(
585                    instance_id.as_ptr() as i64,
586                    32,
587                    state.as_ptr() as i64,
588                    state.len() as i64,
589                )
590            };
591            result == 0
592        }
593        #[cfg(not(target_family = "wasm"))]
594        {
595            let _ = (instance_id, state);
596            false
597        }
598    }
599
600    /// Subscribe to contract updates by instance ID.
601    ///
602    /// Registers interest in receiving notifications when the contract's state
603    /// changes. Currently validates that the contract is known and returns success;
604    /// actual notification delivery is a follow-up.
605    ///
606    /// Returns `true` on success, `false` if the contract is unknown or on error.
607    pub fn subscribe_contract(&mut self, instance_id: &[u8; 32]) -> bool {
608        #[cfg(target_family = "wasm")]
609        {
610            let result =
611                unsafe { __frnt__delegate__subscribe_contract(instance_id.as_ptr() as i64, 32) };
612            result == 0
613        }
614        #[cfg(not(target_family = "wasm"))]
615        {
616            let _ = instance_id;
617            false
618        }
619    }
620
621    /// Create a new child delegate from WASM bytecode and parameters.
622    ///
623    /// This V2 host function allows a delegate to spawn new delegates at runtime.
624    /// The child delegate is registered in the node's delegate store and secret store
625    /// with the provided cipher and nonce.
626    ///
627    /// Returns `Ok((key_hash, code_hash))` where both are 32-byte arrays identifying
628    /// the newly created delegate. Returns `Err(error_code)` on failure.
629    ///
630    /// # Resource Limits
631    /// - Maximum creation depth: 4 (prevents fork bombs)
632    /// - Maximum creations per process() call: 8
633    ///
634    /// # Error Codes
635    /// - `-1`: Called outside process() context
636    /// - `-4`: Invalid parameter
637    /// - `-9`: WASM memory bounds violation
638    /// - `-20`: Depth limit exceeded
639    /// - `-21`: Per-call creation limit exceeded
640    /// - `-23`: Invalid WASM module
641    /// - `-24`: Store registration failed
642    pub fn create_delegate(
643        &mut self,
644        wasm_code: &[u8],
645        params: &[u8],
646        cipher: &[u8; 32],
647        nonce: &[u8; 24],
648    ) -> Result<([u8; 32], [u8; 32]), i32> {
649        #[cfg(target_family = "wasm")]
650        {
651            let mut key_buf = [0u8; 32];
652            let mut hash_buf = [0u8; 32];
653            let result = unsafe {
654                __frnt__delegate__create_delegate(
655                    wasm_code.as_ptr() as i64,
656                    wasm_code.len() as i64,
657                    params.as_ptr() as i64,
658                    params.len() as i64,
659                    cipher.as_ptr() as i64,
660                    nonce.as_ptr() as i64,
661                    key_buf.as_mut_ptr() as i64,
662                    hash_buf.as_mut_ptr() as i64,
663                )
664            };
665            if result == 0 {
666                Ok((key_buf, hash_buf))
667            } else {
668                Err(result)
669            }
670        }
671        #[cfg(not(target_family = "wasm"))]
672        {
673            let _ = (wasm_code, params, cipher, nonce);
674            Err(error_codes::ERR_NOT_IN_PROCESS)
675        }
676    }
677}
678
679impl std::fmt::Debug for DelegateCtx {
680    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
681        f.debug_struct("DelegateCtx")
682            .field("context_len", &self.len())
683            .finish_non_exhaustive()
684    }
685}
686
687// ============================================================================
688// Secret-key-list wire codec (shared host↔delegate contract for list_secrets)
689// ============================================================================
690
691/// Serialize a list of raw secret keys into the wire format read back by
692/// [`decode_secret_key_list`]: for each key, a 4-byte little-endian length
693/// followed by that many key bytes. This is the encoding the host
694/// (`__frnt__delegate__list_secrets`) writes into the delegate's output buffer.
695///
696/// Kept in stdlib (rather than only in the host) so the format has exactly one
697/// authoritative definition that both sides — and the round-trip tests — share.
698pub fn encode_secret_key_list<'a, I>(keys: I) -> Vec<u8>
699where
700    I: IntoIterator<Item = &'a [u8]>,
701{
702    let mut buf = Vec::new();
703    for key in keys {
704        buf.extend_from_slice(&(key.len() as u32).to_le_bytes());
705        buf.extend_from_slice(key);
706    }
707    buf
708}
709
710/// Inverse of [`encode_secret_key_list`]. A truncated trailing record (which can
711/// only happen if the buffer was clipped mid-record) is dropped rather than
712/// panicking, so a short read degrades to "fewer keys" instead of a trap.
713pub fn decode_secret_key_list(buf: &[u8]) -> Vec<Vec<u8>> {
714    let mut keys = Vec::new();
715    let mut pos = 0usize;
716    while pos + 4 <= buf.len() {
717        let len = u32::from_le_bytes([buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3]]) as usize;
718        pos += 4;
719        if pos + len > buf.len() {
720            // Truncated record: stop here rather than over-read.
721            break;
722        }
723        keys.push(buf[pos..pos + len].to_vec());
724        pos += len;
725    }
726    keys
727}
728
729#[cfg(test)]
730mod secret_key_list_codec_tests {
731    use super::{decode_secret_key_list, encode_secret_key_list};
732
733    #[test]
734    fn round_trip_multiple_keys() {
735        let keys: Vec<&[u8]> = vec![b"room:alice", b"room:bob", b"private_key"];
736        let encoded = encode_secret_key_list(keys.iter().copied());
737        let decoded = decode_secret_key_list(&encoded);
738        assert_eq!(
739            decoded,
740            vec![
741                b"room:alice".to_vec(),
742                b"room:bob".to_vec(),
743                b"private_key".to_vec()
744            ]
745        );
746    }
747
748    #[test]
749    fn round_trip_empty_list() {
750        let encoded = encode_secret_key_list(std::iter::empty::<&[u8]>());
751        assert!(encoded.is_empty());
752        assert!(decode_secret_key_list(&encoded).is_empty());
753    }
754
755    #[test]
756    fn round_trip_empty_key() {
757        // A zero-length key is a legal (if unusual) record.
758        let encoded = encode_secret_key_list([b"".as_slice()]);
759        assert_eq!(encoded, vec![0, 0, 0, 0]);
760        assert_eq!(decode_secret_key_list(&encoded), vec![Vec::<u8>::new()]);
761    }
762
763    #[test]
764    fn truncated_trailing_record_is_dropped() {
765        let mut encoded = encode_secret_key_list([b"abc".as_slice(), b"defgh".as_slice()]);
766        // Clip mid-way through the second record's payload.
767        encoded.truncate(encoded.len() - 2);
768        assert_eq!(decode_secret_key_list(&encoded), vec![b"abc".to_vec()]);
769    }
770}