Skip to main content

bitwarden_ffi/
wire.rs

1//! How a type crosses the wasm ABI.
2//!
3//! A proc macro cannot resolve types: seeing the token `CipherView`, `#[wasm_export]` has no way to
4//! know whether it is a serde DTO, a `#[wasm_bindgen]` handle, or a type that crosses as a string.
5//! So each type declares its own wire form instead. [`FromWasm::Wire`] and [`ToWasm::Wire`] are
6//! what a generated shim puts in its signature, with the conversion in the shim body, where a
7//! failure is an ordinary `Err` and destructors run.
8
9use std::fmt::Display;
10
11use wasm_bindgen::JsValue;
12
13/// A value could not be converted across the wasm ABI.
14#[derive(Debug, thiserror::Error)]
15pub enum WireError {
16    /// A string did not parse as the type it crosses as.
17    #[error("Failed to parse `{type_name}`: {message}")]
18    Parse {
19        /// The type the string was parsed as.
20        type_name: &'static str,
21        /// The parser's error message.
22        message: String,
23    },
24}
25
26/// A type that can be received from JavaScript, crossing the ABI as [`Self::Wire`].
27pub trait FromWasm: Sized {
28    /// The type JavaScript actually passes.
29    type Wire;
30
31    /// Recovers the Rust value from the wire value.
32    fn from_wire(wire: Self::Wire) -> Result<Self, WireError>;
33}
34
35/// A type that can be handed to JavaScript, crossing the ABI as [`Self::Wire`].
36pub trait ToWasm {
37    /// The type JavaScript actually receives.
38    type Wire;
39
40    /// Converts the Rust value into the wire value.
41    fn to_wire(self) -> Result<Self::Wire, WireError>;
42}
43
44/// Runs an `impl_wire_string!` parser. A function rather than inline macro code so the closure's
45/// argument type is inferred.
46#[doc(hidden)]
47pub fn parse_wire_string<T, E: Display>(
48    type_name: &'static str,
49    wire: String,
50    parse: impl FnOnce(String) -> Result<T, E>,
51) -> Result<T, WireError> {
52    parse(wire).map_err(|err| WireError::Parse {
53        type_name,
54        message: err.to_string(),
55    })
56}
57
58/// Runs an `impl_wire_string!` formatter; see [`parse_wire_string`].
59#[doc(hidden)]
60pub fn format_wire_string<T>(value: T, format: impl FnOnce(T) -> String) -> String {
61    format(value)
62}
63
64// The closed set of types wasm_bindgen carries natively. Being absent from it is a missing-impl
65// compile error at the use site.
66crate::impl_wire_object!(
67    bool,
68    char,
69    f32,
70    f64,
71    i8,
72    i16,
73    i32,
74    i64,
75    i128,
76    isize,
77    u8,
78    u16,
79    u32,
80    u64,
81    u128,
82    usize,
83    (),
84    String,
85    JsValue,
86);
87
88/// `Vec<T>` crosses as a vector of `T`'s wire type, so `Vec<u8>` stays `Vec<u8>` and reaches
89/// JavaScript as a `Uint8Array`.
90impl<T: FromWasm> FromWasm for Vec<T> {
91    type Wire = Vec<T::Wire>;
92
93    fn from_wire(wire: Self::Wire) -> Result<Self, WireError> {
94        wire.into_iter().map(T::from_wire).collect()
95    }
96}
97
98impl<T: ToWasm> ToWasm for Vec<T> {
99    type Wire = Vec<T::Wire>;
100
101    fn to_wire(self) -> Result<Self::Wire, WireError> {
102        self.into_iter().map(T::to_wire).collect()
103    }
104}
105
106impl<T: FromWasm> FromWasm for Option<T> {
107    type Wire = Option<T::Wire>;
108
109    fn from_wire(wire: Self::Wire) -> Result<Self, WireError> {
110        wire.map(T::from_wire).transpose()
111    }
112}
113
114impl<T: ToWasm> ToWasm for Option<T> {
115    type Wire = Option<T::Wire>;
116
117    fn to_wire(self) -> Result<Self::Wire, WireError> {
118        self.map(T::to_wire).transpose()
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use std::convert::Infallible;
125
126    use super::*;
127
128    #[derive(Debug, PartialEq)]
129    struct Digit(u8);
130
131    impl std::str::FromStr for Digit {
132        type Err = String;
133
134        fn from_str(s: &str) -> Result<Self, Self::Err> {
135            s.parse()
136                .map(Digit)
137                .map_err(|_| format!("not a digit: {s}"))
138        }
139    }
140
141    impl Display for Digit {
142        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143            write!(f, "{}", self.0)
144        }
145    }
146
147    crate::impl_wire_string!(Digit);
148
149    struct Shout(String);
150
151    crate::impl_wire_string!(
152        Shout,
153        parse = |wire: String| Ok::<_, Infallible>(Shout(wire.to_uppercase())),
154        format = |value: Shout| value.0,
155    );
156
157    struct InputOnly(String);
158
159    crate::impl_wire_string!(
160        InputOnly,
161        parse = |wire| Ok::<_, Infallible>(InputOnly(wire))
162    );
163
164    #[test]
165    fn default_form_round_trips() {
166        let digit = Digit::from_wire("7".to_owned()).unwrap();
167        assert_eq!(digit, Digit(7));
168        assert_eq!(digit.to_wire().unwrap(), "7");
169    }
170
171    #[test]
172    fn malformed_string_is_a_parse_error() {
173        let err = Digit::from_wire("x".to_owned()).unwrap_err();
174        assert_eq!(err.to_string(), "Failed to parse `Digit`: not a digit: x");
175    }
176
177    #[test]
178    fn custom_form_uses_the_given_functions() {
179        let shout = Shout::from_wire("hi".to_owned()).unwrap();
180        assert_eq!(shout.to_wire().unwrap(), "HI");
181        assert_eq!(InputOnly::from_wire("hi".to_owned()).unwrap().0, "hi");
182    }
183
184    #[test]
185    fn a_bad_element_fails_the_whole_vec() {
186        let wire = vec!["1".to_owned(), "x".to_owned()];
187        assert!(Vec::<Digit>::from_wire(wire).is_err());
188        assert_eq!(Option::<Digit>::from_wire(None).unwrap(), None);
189    }
190}