Skip to main content

firewood_ffi/value/
results.rs

1// Copyright (C) 2025, Ava Labs, Inc. All rights reserved.
2// See the file LICENSE.md for licensing terms.
3
4use firewood::api;
5use firewood::merkle;
6use firewood_storage::TrieHash;
7use std::fmt;
8
9use crate::revision::{GetRevisionResult, RevisionHandle};
10use crate::{
11    ChangeProofContext, CodeIteratorHandle, CreateIteratorResult, CreateProposalResult, HashKey,
12    IteratorHandle, KeyRange, NextKeyRange, OwnedBytes, OwnedKeyValueBatch, OwnedKeyValuePair,
13    ProposalHandle, ProposedChangeProofContext, RangeProofContext, ReconstructedHandle,
14    VerifiedChangeProofContext,
15};
16
17/// The result type returned from an FFI function that returns no value but may
18/// return an error.
19#[derive(Debug)]
20#[repr(C, usize)]
21pub enum VoidResult {
22    /// The caller provided a null pointer to the input handle.
23    NullHandlePointer,
24
25    /// The operation was successful and no error occurred.
26    Ok,
27
28    /// An error occurred and the message is returned as an [`OwnedBytes`]. Its
29    /// value is guaranteed to contain only valid UTF-8.
30    ///
31    /// The caller must call [`fwd_free_owned_bytes`] to free the memory
32    /// associated with this error.
33    ///
34    /// [`fwd_free_owned_bytes`]: crate::fwd_free_owned_bytes
35    Err(OwnedBytes),
36}
37
38impl From<()> for VoidResult {
39    fn from((): ()) -> Self {
40        VoidResult::Ok
41    }
42}
43
44impl<E: fmt::Display> From<Result<(), E>> for VoidResult {
45    fn from(value: Result<(), E>) -> Self {
46        match value {
47            Ok(()) => VoidResult::Ok,
48            Err(err) => VoidResult::Err(err.to_string().into_bytes().into()),
49        }
50    }
51}
52
53/// The result type returned from the open or create database functions.
54#[derive(Debug)]
55#[repr(C, usize)]
56pub enum HandleResult {
57    /// The database was opened or created successfully and the handle is
58    /// returned as an opaque pointer.
59    ///
60    /// The caller must ensure that [`fwd_close_db`] is called to free resources
61    /// associated with this handle when it is no longer needed.
62    ///
63    /// [`fwd_close_db`]: crate::fwd_close_db
64    Ok(Box<crate::DatabaseHandle>),
65
66    /// An error occurred and the message is returned as an [`OwnedBytes`]. If
67    /// value is guaranteed to contain only valid UTF-8.
68    ///
69    /// The caller must call [`fwd_free_owned_bytes`] to free the memory
70    /// associated with this error.
71    ///
72    /// [`fwd_free_owned_bytes`]: crate::fwd_free_owned_bytes
73    Err(OwnedBytes),
74}
75
76impl<E: fmt::Display> From<Result<crate::DatabaseHandle, E>> for HandleResult {
77    fn from(value: Result<crate::DatabaseHandle, E>) -> Self {
78        match value {
79            Ok(handle) => HandleResult::Ok(Box::new(handle)),
80            Err(err) => HandleResult::Err(err.to_string().into_bytes().into()),
81        }
82    }
83}
84
85/// A result type returned from FFI functions that retrieve a single value.
86#[derive(Debug)]
87#[repr(C, usize)]
88pub enum ValueResult {
89    /// The caller provided a null pointer to a database handle.
90    NullHandlePointer,
91    /// The provided root was not found in the database.
92    RevisionNotFound(HashKey),
93    /// The provided key was not found in the database or proposal.
94    None,
95    /// A value was found and is returned.
96    ///
97    /// The caller must call [`fwd_free_owned_bytes`] to free the memory
98    /// associated with this value.
99    ///
100    /// [`fwd_free_owned_bytes`]: crate::fwd_free_owned_bytes
101    Some(OwnedBytes),
102    /// An error occurred and the message is returned as an [`OwnedBytes`]. If
103    /// value is guaranteed to contain only valid UTF-8.
104    ///
105    /// The caller must call [`fwd_free_owned_bytes`] to free the memory
106    /// associated with this error.
107    ///
108    /// [`fwd_free_owned_bytes`]: crate::fwd_free_owned_bytes
109    Err(OwnedBytes),
110}
111
112impl<E: fmt::Display> From<Result<String, E>> for ValueResult {
113    fn from(value: Result<String, E>) -> Self {
114        match value {
115            Ok(data) => ValueResult::Some(data.into_bytes().into()),
116            Err(err) => ValueResult::Err(err.to_string().into_bytes().into()),
117        }
118    }
119}
120
121impl From<Result<Option<Box<[u8]>>, api::Error>> for ValueResult {
122    fn from(value: Result<Option<Box<[u8]>>, api::Error>) -> Self {
123        match value {
124            Ok(None) => ValueResult::None,
125            Err(api::Error::RevisionNotFound { provided }) => ValueResult::RevisionNotFound(
126                HashKey::from(provided.unwrap_or_else(api::HashKey::empty)),
127            ),
128            Ok(Some(data)) => ValueResult::Some(data.into()),
129            Err(err) => ValueResult::Err(err.to_string().into_bytes().into()),
130        }
131    }
132}
133
134impl From<Result<Option<Box<[u8]>>, firewood::db::DbError>> for ValueResult {
135    fn from(value: Result<Option<Box<[u8]>>, firewood::db::DbError>) -> Self {
136        value.map_err(api::Error::from).into()
137    }
138}
139
140impl From<Vec<u8>> for ValueResult {
141    fn from(value: Vec<u8>) -> Self {
142        value.into_boxed_slice().into()
143    }
144}
145
146impl From<Box<[u8]>> for ValueResult {
147    fn from(value: Box<[u8]>) -> Self {
148        ValueResult::Some(value.into())
149    }
150}
151
152/// A result type returned from FFI functions return the database root hash. This
153/// may or may not be after a mutation.
154#[derive(Debug)]
155#[repr(C, usize)]
156pub enum HashResult {
157    /// The caller provided a null pointer to a database handle.
158    NullHandlePointer,
159    /// The proposal resulted in an empty database or the database currently has
160    /// no root hash.
161    None,
162    /// The mutation was successful and the root hash is returned, if this result
163    /// was from a mutation. Otherwise, this is the current root hash of the
164    /// database.
165    Some(HashKey),
166    /// An error occurred and the message is returned as an [`OwnedBytes`]. If
167    /// value is guaranteed to contain only valid UTF-8.
168    ///
169    /// The caller must call [`fwd_free_owned_bytes`] to free the memory
170    /// associated with this error.
171    ///
172    /// [`fwd_free_owned_bytes`]: crate::fwd_free_owned_bytes
173    Err(OwnedBytes),
174}
175
176impl<E: fmt::Display> From<Result<Option<api::HashKey>, E>> for HashResult {
177    fn from(value: Result<Option<api::HashKey>, E>) -> Self {
178        match value {
179            Ok(hash) => hash.into(),
180            Err(err) => HashResult::Err(err.to_string().into_bytes().into()),
181        }
182    }
183}
184
185impl From<Option<TrieHash>> for HashResult {
186    fn from(value: Option<TrieHash>) -> Self {
187        match value {
188            Some(hash) => HashResult::Some(hash.into()),
189            None => HashResult::None,
190        }
191    }
192}
193
194impl From<Option<Result<HashKey, api::Error>>> for HashResult {
195    fn from(value: Option<Result<HashKey, api::Error>>) -> Self {
196        match value {
197            Some(value) => match value {
198                Ok(hash) => HashResult::Some(hash),
199                Err(err) => HashResult::Err(err.to_string().into_bytes().into()),
200            },
201            None => HashResult::None,
202        }
203    }
204}
205
206/// A result type returned from FFI functions that create or parse range proofs.
207///
208/// The caller must ensure that [`fwd_free_range_proof`] is called to
209/// free the memory associated with the returned context when it is no longer
210/// needed.
211///
212/// [`fwd_free_range_proof`]: crate::fwd_free_range_proof
213#[derive(Debug)]
214#[repr(C, usize)]
215pub enum RangeProofResult<'db> {
216    /// The caller provided a null pointer to the input handle.
217    NullHandlePointer,
218    /// The provided root was not found in the database.
219    RevisionNotFound(HashKey),
220    /// A range proof was requested on an empty trie.
221    EmptyTrie,
222    /// The proof was successfully created or parsed.
223    ///
224    /// If the value was parsed from a serialized proof, this does not imply that
225    /// the proof is valid, only that it is well-formed. The verify method must
226    /// be called to ensure the proof is cryptographically valid.
227    Ok(Box<RangeProofContext<'db>>),
228    /// An error occurred and the message is returned as an [`OwnedBytes`]. If
229    /// value is guaranteed to contain only valid UTF-8.
230    ///
231    /// The caller must call [`fwd_free_owned_bytes`] to free the memory
232    /// associated with this error.
233    ///
234    /// [`fwd_free_owned_bytes`]: crate::fwd_free_owned_bytes
235    Err(OwnedBytes),
236}
237
238impl From<Result<api::FrozenRangeProof, api::Error>> for RangeProofResult<'_> {
239    fn from(value: Result<api::FrozenRangeProof, api::Error>) -> Self {
240        match value {
241            Ok(proof) => RangeProofResult::Ok(Box::new(proof.into())),
242            Err(api::Error::RevisionNotFound { provided }) => RangeProofResult::RevisionNotFound(
243                HashKey::from(provided.unwrap_or_else(api::HashKey::empty)),
244            ),
245            Err(api::Error::RangeProofOnEmptyTrie) => RangeProofResult::EmptyTrie,
246            Err(err) => RangeProofResult::Err(err.to_string().into_bytes().into()),
247        }
248    }
249}
250
251/// A result type returned from FFI functions that create or parse change proofs.
252///
253/// The caller must ensure that [`fwd_free_change_proof`] is called to
254/// free the memory associated with the returned context when it is no longer
255/// needed.
256///
257/// [`fwd_free_change_proof`]: crate::fwd_free_change_proof
258#[derive(Debug)]
259#[repr(C, usize)]
260pub enum ChangeProofResult {
261    /// The caller provided a null pointer to the input handle.
262    NullHandlePointer,
263    /// The provided start root was not found in the database.
264    StartRevisionNotFound(HashKey),
265    /// The provided end root was not found in the database.
266    EndRevisionNotFound(HashKey),
267    /// The proof was successfully created or parsed.
268    ///
269    /// If the value was parsed from a serialized proof, this does not imply that
270    /// the proof is valid, only that it is well-formed. The verify method must
271    /// be called to ensure the proof is cryptographically valid.
272    Ok(Box<ChangeProofContext>),
273    /// An error occurred and the message is returned as an [`OwnedBytes`]. If
274    /// value is guaranteed to contain only valid UTF-8.
275    ///
276    /// The caller must call [`fwd_free_owned_bytes`] to free the memory
277    /// associated with this error.
278    ///
279    /// [`fwd_free_owned_bytes`]: crate::fwd_free_owned_bytes
280    Err(OwnedBytes),
281}
282
283#[derive(Debug)]
284#[repr(C, usize)]
285pub enum VerifiedChangeProofResult {
286    /// The caller provided a null pointer to the input handle.
287    NullHandlePointer,
288    // The proof was successfully verified.
289    Ok(Box<VerifiedChangeProofContext>),
290    /// An error occurred and the message is returned as an [`OwnedBytes`]. If
291    /// value is guaranteed to contain only valid UTF-8.
292    ///
293    /// The caller must call [`fwd_free_owned_bytes`] to free the memory
294    /// associated with this error.
295    ///
296    /// [`fwd_free_owned_bytes`]: crate::fwd_free_owned_bytes
297    Err(OwnedBytes),
298}
299
300#[derive(Debug)]
301#[repr(C, usize)]
302pub enum ProposedChangeProofResult<'db> {
303    /// The caller provided a null pointer to the input handle.
304    NullHandlePointer,
305    /// A proposal was successfully created for this proof.
306    Ok(Box<ProposedChangeProofContext<'db>>),
307    /// An error occurred and the message is returned as an [`OwnedBytes`]. If
308    /// value is guaranteed to contain only valid UTF-8.
309    ///
310    /// The caller must call [`fwd_free_owned_bytes`] to free the memory
311    /// associated with this error.
312    ///
313    /// [`fwd_free_owned_bytes`]: crate::fwd_free_owned_bytes
314    Err(OwnedBytes),
315}
316
317#[derive(Debug)]
318#[repr(C, usize)]
319pub enum NextKeyRangeResult {
320    /// The caller provided a null pointer to the input handle.
321    NullHandlePointer,
322    /// The proof has not prepared into a proposal nor committed to the database.
323    NotPrepared,
324    /// There are no more keys to fetch.
325    None,
326    /// The next key range to fetch is returned.
327    Some(NextKeyRange),
328    /// An error occurred and the message is returned as an [`OwnedBytes`]. If
329    /// value is guaranteed to contain only valid UTF-8.
330    ///
331    /// The caller must call [`fwd_free_owned_bytes`] to free the memory
332    /// associated with this error.
333    ///
334    /// [`fwd_free_owned_bytes`]: crate::fwd_free_owned_bytes
335    Err(OwnedBytes),
336}
337
338impl From<Result<Option<KeyRange>, api::Error>> for NextKeyRangeResult {
339    fn from(value: Result<Option<KeyRange>, api::Error>) -> Self {
340        match value {
341            Ok(None) => NextKeyRangeResult::None,
342            Ok(Some((start_key, end_key))) => NextKeyRangeResult::Some(NextKeyRange {
343                start_key: start_key.into(),
344                end_key: end_key.map(Into::into).into(),
345            }),
346            Err(api::Error::ProofError(firewood::ProofError::Unverified)) => {
347                NextKeyRangeResult::NotPrepared
348            }
349            Err(err) => NextKeyRangeResult::Err(err.to_string().into_bytes().into()),
350        }
351    }
352}
353
354/// A result type returned from FFI functions that create an code hash iterator
355#[derive(Debug)]
356#[repr(C, usize)]
357pub enum CodeIteratorResult<'p> {
358    /// The caller provided a null pointer to a proof handle.
359    NullHandlePointer,
360    /// Building the iterator was successful and the iterator handle is returned
361    Ok {
362        /// An opaque pointer to the [`CodeIteratorHandle`].
363        /// The value should be freed with [`fwd_code_hash_iter_free`]
364        ///
365        /// [`fwd_code_hash_iter_free`]: crate::fwd_code_hash_iter_free
366        handle: Box<CodeIteratorHandle<'p>>,
367    },
368    /// An error occurred and the message is returned as an [`OwnedBytes`].
369    ///
370    /// The caller must call [`fwd_free_owned_bytes`] to free the memory
371    /// associated with this error.
372    ///
373    /// [`fwd_free_owned_bytes`]: crate::fwd_free_owned_bytes
374    Err(OwnedBytes),
375}
376
377impl<'a> From<Result<CodeIteratorHandle<'a>, api::Error>> for CodeIteratorResult<'a> {
378    fn from(value: Result<CodeIteratorHandle<'a>, api::Error>) -> Self {
379        match value {
380            Ok(res) => CodeIteratorResult::Ok {
381                handle: Box::new(res),
382            },
383            Err(err) => CodeIteratorResult::Err(err.to_string().into_bytes().into()),
384        }
385    }
386}
387
388/// A result type returned from FFI functions that create a proposal but do not
389/// commit it to the database.
390#[derive(Debug)]
391#[repr(C, usize)]
392pub enum ProposalResult<'db> {
393    /// The caller provided a null pointer to a database handle.
394    NullHandlePointer,
395    /// Buulding the proposal was successful and the proposal ID and root hash
396    /// are returned.
397    Ok {
398        /// An opaque pointer to the [`ProposalHandle`] that can be use to create
399        /// an additional proposal or later commit. The caller must ensure that this
400        /// pointer is freed with [`fwd_free_proposal`] if it is not committed.
401        ///
402        /// [`fwd_free_proposal`]: crate::fwd_free_proposal
403        // note: opaque pointers mut be boxed because the FFI does not the structure definition.
404        handle: Box<ProposalHandle<'db>>,
405        /// The root hash of the proposal. Zeroed if the proposal resulted in an
406        /// empty database.
407        root_hash: HashKey,
408    },
409    /// An error occurred and the message is returned as an [`OwnedBytes`]. If
410    /// value is guaranteed to contain only valid UTF-8.
411    ///
412    /// The caller must call [`fwd_free_owned_bytes`] to free the memory
413    /// associated with this error.
414    ///
415    /// [`fwd_free_owned_bytes`]: crate::fwd_free_owned_bytes
416    Err(OwnedBytes),
417}
418
419/// A result type returned from FFI functions that create an iterator
420#[derive(Debug)]
421#[repr(C, usize)]
422pub enum IteratorResult<'db> {
423    /// The caller provided a null pointer to a revision/proposal handle.
424    NullHandlePointer,
425    /// Building the iterator was successful and the iterator handle is returned
426    Ok {
427        /// An opaque pointer to the [`IteratorHandle`].
428        /// The value should be freed with [`fwd_free_iterator`]
429        ///
430        /// [`fwd_free_iterator`]: crate::fwd_free_iterator
431        handle: Box<IteratorHandle<'db>>,
432    },
433    /// An error occurred and the message is returned as an [`OwnedBytes`].
434    ///
435    /// The caller must call [`fwd_free_owned_bytes`] to free the memory
436    /// associated with this error.
437    ///
438    /// [`fwd_free_owned_bytes`]: crate::fwd_free_owned_bytes
439    Err(OwnedBytes),
440}
441
442/// A result type returned from iterator FFI functions
443#[derive(Debug)]
444#[repr(C, usize)]
445pub enum KeyValueResult {
446    /// The caller provided a null pointer to an iterator handle.
447    NullHandlePointer,
448    /// The iterator is exhausted
449    None,
450    /// The next item is returned.
451    ///
452    /// The caller must call [`fwd_free_owned_bytes`] to free the memory
453    /// associated with the key and the value of this pair.
454    ///
455    /// [`fwd_free_owned_bytes`]: crate::fwd_free_owned_bytes
456    Some(OwnedKeyValuePair),
457    /// An error occurred and the message is returned as an [`OwnedBytes`]. The
458    /// value is guaranteed to contain only valid UTF-8.
459    ///
460    /// The caller must call [`fwd_free_owned_bytes`] to free the memory
461    /// associated with this error.
462    ///
463    /// [`fwd_free_owned_bytes`]: crate::fwd_free_owned_bytes
464    Err(OwnedBytes),
465}
466
467impl From<Option<Result<(merkle::Key, merkle::Value), api::Error>>> for KeyValueResult {
468    fn from(value: Option<Result<(merkle::Key, merkle::Value), api::Error>>) -> Self {
469        match value {
470            Some(value) => match value {
471                Ok(value) => KeyValueResult::Some(value.into()),
472                Err(err) => KeyValueResult::Err(err.to_string().into_bytes().into()),
473            },
474            None => KeyValueResult::None,
475        }
476    }
477}
478
479/// A result type returned from iterator FFI functions
480#[derive(Debug)]
481#[repr(C, usize)]
482pub enum KeyValueBatchResult {
483    /// The caller provided a null pointer to an iterator handle.
484    NullHandlePointer,
485    /// The next batch of items on iterator are returned.
486    Some(OwnedKeyValueBatch),
487    /// An error occurred and the message is returned as an [`OwnedBytes`]. If
488    /// value is guaranteed to contain only valid UTF-8.
489    ///
490    /// The caller must call [`fwd_free_owned_bytes`] to free the memory
491    /// associated with this error.
492    ///
493    /// [`fwd_free_owned_bytes`]: crate::fwd_free_owned_bytes
494    Err(OwnedBytes),
495}
496
497impl From<Result<Vec<(merkle::Key, merkle::Value)>, api::Error>> for KeyValueBatchResult {
498    fn from(value: Result<Vec<(merkle::Key, merkle::Value)>, api::Error>) -> Self {
499        match value {
500            Ok(pairs) => {
501                let values: Vec<_> = pairs.into_iter().map(Into::into).collect();
502                KeyValueBatchResult::Some(values.into())
503            }
504            Err(err) => KeyValueBatchResult::Err(err.to_string().into_bytes().into()),
505        }
506    }
507}
508
509impl<'db> From<CreateIteratorResult<'db>> for IteratorResult<'db> {
510    fn from(value: CreateIteratorResult<'db>) -> Self {
511        IteratorResult::Ok {
512            handle: Box::new(value.0),
513        }
514    }
515}
516
517impl<'db, E: fmt::Display> From<Result<CreateIteratorResult<'db>, E>> for IteratorResult<'db> {
518    fn from(value: Result<CreateIteratorResult<'db>, E>) -> Self {
519        match value {
520            Ok(res) => res.into(),
521            Err(err) => IteratorResult::Err(err.to_string().into_bytes().into()),
522        }
523    }
524}
525
526/// A result type returned from FFI functions that get a revision
527#[derive(Debug)]
528#[repr(C, usize)]
529pub enum RevisionResult<'db> {
530    /// The caller provided a null pointer to a database handle.
531    NullHandlePointer,
532    /// The provided root was not found in the database.
533    RevisionNotFound(HashKey),
534    /// Getting the revision was successful and the revision handle and root
535    /// hash are returned.
536    Ok {
537        /// An opaque pointer to the [`RevisionHandle`].
538        /// The value should be freed with [`fwd_free_revision`]
539        ///
540        /// [`fwd_free_revision`]: crate::fwd_free_revision
541        handle: Box<RevisionHandle<'db>>,
542        /// The root hash of the revision.
543        root_hash: HashKey,
544    },
545    /// An error occurred and the message is returned as an [`OwnedBytes`]. The
546    /// value is guaranteed to contain only valid UTF-8.
547    ///
548    /// The caller must call [`fwd_free_owned_bytes`] to free the memory
549    /// associated with this error.
550    ///
551    /// [`fwd_free_owned_bytes`]: crate::fwd_free_owned_bytes
552    Err(OwnedBytes),
553}
554
555/// A result type returned from FFI functions that create a reconstructed view.
556#[derive(Debug)]
557#[repr(C, usize)]
558pub enum ReconstructedResult<'db> {
559    /// The caller provided a null pointer to an input handle.
560    NullHandlePointer,
561    /// Building the reconstructed view was successful and the handle is returned.
562    Ok {
563        /// An opaque pointer to the [`ReconstructedHandle`].
564        /// The value should be freed with [`fwd_free_reconstructed`].
565        ///
566        /// [`fwd_free_reconstructed`]: crate::fwd_free_reconstructed
567        handle: Box<ReconstructedHandle<'db>>,
568    },
569    /// An error occurred and the message is returned as an [`OwnedBytes`].
570    Err(OwnedBytes),
571}
572
573impl<'db> From<GetRevisionResult<'db>> for RevisionResult<'db> {
574    fn from(value: GetRevisionResult<'db>) -> Self {
575        RevisionResult::Ok {
576            handle: Box::new(value.handle),
577            root_hash: HashKey::from(value.root_hash),
578        }
579    }
580}
581
582impl<'db> From<Result<GetRevisionResult<'db>, api::Error>> for RevisionResult<'db> {
583    fn from(value: Result<GetRevisionResult<'db>, api::Error>) -> Self {
584        match value {
585            Ok(res) => res.into(),
586            Err(api::Error::RevisionNotFound { provided }) => RevisionResult::RevisionNotFound(
587                HashKey::from(provided.unwrap_or_else(api::HashKey::empty)),
588            ),
589            Err(err) => RevisionResult::Err(err.to_string().into_bytes().into()),
590        }
591    }
592}
593
594impl<'db, E: fmt::Display> From<Result<CreateProposalResult<'db>, E>> for ProposalResult<'db> {
595    fn from(value: Result<CreateProposalResult<'db>, E>) -> Self {
596        match value {
597            Ok(CreateProposalResult { handle, .. }) => ProposalResult::Ok {
598                root_hash: handle.hash_key().unwrap_or_default(),
599                handle: Box::new(handle),
600            },
601            Err(err) => ProposalResult::Err(err.to_string().into_bytes().into()),
602        }
603    }
604}
605
606impl<'db, E: fmt::Display> From<Result<ReconstructedHandle<'db>, E>> for ReconstructedResult<'db> {
607    fn from(value: Result<ReconstructedHandle<'db>, E>) -> Self {
608        match value {
609            Ok(handle) => ReconstructedResult::Ok {
610                handle: Box::new(handle),
611            },
612            Err(err) => ReconstructedResult::Err(err.to_string().into_bytes().into()),
613        }
614    }
615}
616
617impl From<Result<api::FrozenChangeProof, api::Error>> for ChangeProofResult {
618    fn from(value: Result<api::FrozenChangeProof, api::Error>) -> Self {
619        match value {
620            Ok(proof) => ChangeProofResult::Ok(Box::new(proof.into())),
621            Err(api::Error::StartRevisionNotFound { provided }) => {
622                ChangeProofResult::StartRevisionNotFound(HashKey::from(
623                    provided.unwrap_or_else(api::HashKey::empty),
624                ))
625            }
626            Err(api::Error::EndRevisionNotFound { provided }) => {
627                ChangeProofResult::EndRevisionNotFound(HashKey::from(
628                    provided.unwrap_or_else(api::HashKey::empty),
629                ))
630            }
631            Err(err) => ChangeProofResult::Err(err.to_string().into_bytes().into()),
632        }
633    }
634}
635
636impl From<Result<VerifiedChangeProofContext, api::Error>> for VerifiedChangeProofResult {
637    fn from(value: Result<VerifiedChangeProofContext, api::Error>) -> Self {
638        match value {
639            Ok(context) => VerifiedChangeProofResult::Ok(Box::new(context)),
640            Err(err) => VerifiedChangeProofResult::Err(err.to_string().into_bytes().into()),
641        }
642    }
643}
644
645impl<'db> From<Result<ProposedChangeProofContext<'db>, api::Error>>
646    for ProposedChangeProofResult<'db>
647{
648    fn from(value: Result<ProposedChangeProofContext<'db>, api::Error>) -> Self {
649        match value {
650            Ok(context) => ProposedChangeProofResult::Ok(Box::new(context)),
651            Err(err) => ProposedChangeProofResult::Err(err.to_string().into_bytes().into()),
652        }
653    }
654}
655
656/// Helper trait to handle the different result types returned from FFI functions.
657///
658/// Once Try trait is stable, we can use that instead of this trait:
659///
660/// ```ignore
661/// impl std::ops::FromResidual<Option<std::convert::Infallible>> for VoidResult {
662///     #[inline]
663///     fn from_residual(residual: Option<std::convert::Infallible>) -> Self {
664///         match residual {
665///             None => VoidResult::NullHandlePointer,
666///             // no other branches are needed because `std::convert::Infallible` is uninhabited
667///             // this compiles without error because the compiler knows that Some(_) is impossible
668///             // see: https://github.com/rust-lang/rust/blob/3fb1b53a9dbfcdf37a4b67d35cde373316829930/library/core/src/option.rs#L2627-L2631
669///             // and: https://doc.rust-lang.org/nomicon/exotic-sizes.html#empty-types
670///         }
671///     }
672/// }
673/// ```
674pub(crate) trait NullHandleResult: CResult {
675    fn null_handle_pointer_error() -> Self;
676}
677
678pub(crate) trait CResult: Sized {
679    #[cfg(panic = "unwind")]
680    fn from_err(err: impl ToString) -> Self;
681
682    #[cfg(panic = "unwind")]
683    fn from_panic(panic: Box<dyn std::any::Any + Send>) -> Self
684    where
685        Self: Sized,
686    {
687        Self::from_err(Panic::from(panic))
688    }
689}
690
691macro_rules! impl_null_handle_result {
692    ($($Enum:ty),* $(,)?) => {
693        $(
694            impl NullHandleResult for $Enum {
695                fn null_handle_pointer_error() -> Self {
696                    Self::NullHandlePointer
697                }
698            }
699        )*
700    };
701}
702
703macro_rules! impl_cresult {
704    ($($Enum:ty),* $(,)?) => {
705        $(
706            impl CResult for $Enum {
707                #[cfg(panic = "unwind")]
708                fn from_err(err: impl ToString) -> Self {
709                    Self::Err(err.to_string().into_bytes().into())
710                }
711            }
712        )*
713    };
714}
715
716impl_null_handle_result!(
717    VoidResult,
718    ValueResult,
719    HashResult,
720    RangeProofResult<'_>,
721    ChangeProofResult,
722    VerifiedChangeProofResult,
723    ProposedChangeProofResult<'_>,
724    NextKeyRangeResult,
725    CodeIteratorResult<'_>,
726    ProposalResult<'_>,
727    ReconstructedResult<'_>,
728    IteratorResult<'_>,
729    RevisionResult<'_>,
730    KeyValueBatchResult,
731    KeyValueResult,
732);
733
734impl_cresult!(
735    VoidResult,
736    ValueResult,
737    HashResult,
738    HandleResult,
739    RangeProofResult<'_>,
740    ChangeProofResult,
741    VerifiedChangeProofResult,
742    ProposedChangeProofResult<'_>,
743    NextKeyRangeResult,
744    CodeIteratorResult<'_>,
745    ProposalResult<'_>,
746    ReconstructedResult<'_>,
747    IteratorResult<'_>,
748    RevisionResult<'_>,
749    KeyValueBatchResult,
750    KeyValueResult,
751);
752
753#[cfg(panic = "unwind")]
754enum Panic {
755    Static(&'static str),
756    Formatted(String),
757    SendSyncErr(Box<dyn std::error::Error + Send + Sync>),
758    SendErr(Box<dyn std::error::Error + Send>),
759    Unknown(#[expect(unused)] Box<dyn std::any::Any + Send>),
760    // TODO: add variant to capture backtrace with panic hook
761    // https://doc.rust-lang.org/stable/std/panic/fn.set_hook.html
762}
763
764#[cfg(panic = "unwind")]
765impl From<Box<dyn std::any::Any + Send>> for Panic {
766    fn from(panic: Box<dyn std::any::Any + Send>) -> Self {
767        macro_rules! downcast {
768            ($Variant:ident($panic:ident)) => {
769                let $panic = match $panic.downcast() {
770                    Ok(panic) => return Panic::$Variant(*panic),
771                    Err(panic) => panic,
772                };
773            };
774        }
775
776        downcast!(Static(panic));
777        downcast!(Formatted(panic));
778        downcast!(SendSyncErr(panic));
779        downcast!(SendErr(panic));
780
781        Self::Unknown(panic)
782    }
783}
784
785#[cfg(panic = "unwind")]
786impl fmt::Display for Panic {
787    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
788        match self {
789            Panic::Static(msg) => f.pad(msg),
790            Panic::Formatted(msg) => f.pad(msg),
791            Panic::SendSyncErr(err) => err.fmt(f),
792            Panic::SendErr(err) => err.fmt(f),
793            Panic::Unknown(_) => f.pad("unknown panic type recovered"),
794        }
795    }
796}