Skip to main content

flaron_sdk/
encoding.rs

1//! Encoding helpers (base64, hex, URL) backed by the host runtime.
2//!
3//! These are zero-dep wrappers - calling the host avoids pulling a Rust
4//! base64/hex crate into your Wasm binary, which keeps `.wasm` size down.
5
6use crate::{ffi, mem};
7
8/// Base64-encode a byte slice using the standard alphabet (with padding).
9pub fn base64_encode(data: &[u8]) -> String {
10    let (data_ptr, data_len) = mem::host_arg_bytes(data);
11    let result = unsafe { ffi::encoding_base64_encode(data_ptr, data_len) };
12    // SAFETY: host writes a UTF-8 base64 string into the bump arena.
13    unsafe { mem::read_packed_string(result) }.unwrap_or_default()
14}
15
16/// Decode a base64 string into raw bytes.
17///
18/// Returns `None` if the input is not valid base64.
19pub fn base64_decode(input: &str) -> Option<Vec<u8>> {
20    let (data_ptr, data_len) = mem::host_arg_str(input);
21    let result = unsafe { ffi::encoding_base64_decode(data_ptr, data_len) };
22    // SAFETY: host writes the decoded bytes into the bump arena.
23    unsafe { mem::read_packed_bytes(result) }
24}
25
26/// Hex-encode a byte slice (lowercase, no separators).
27pub fn hex_encode(data: &[u8]) -> String {
28    let (data_ptr, data_len) = mem::host_arg_bytes(data);
29    let result = unsafe { ffi::encoding_hex_encode(data_ptr, data_len) };
30    // SAFETY: host writes a UTF-8 hex string into the bump arena.
31    unsafe { mem::read_packed_string(result) }.unwrap_or_default()
32}
33
34/// Decode a hex string into raw bytes. Accepts upper or lower case.
35///
36/// Returns `None` if the input is not valid hex.
37pub fn hex_decode(input: &str) -> Option<Vec<u8>> {
38    let (data_ptr, data_len) = mem::host_arg_str(input);
39    let result = unsafe { ffi::encoding_hex_decode(data_ptr, data_len) };
40    // SAFETY: host writes the decoded bytes into the bump arena.
41    unsafe { mem::read_packed_bytes(result) }
42}
43
44/// URL-encode a string using percent-encoding (`application/x-www-form-urlencoded`
45/// rules).
46pub fn url_encode(input: &str) -> String {
47    let (data_ptr, data_len) = mem::host_arg_str(input);
48    let result = unsafe { ffi::encoding_url_encode(data_ptr, data_len) };
49    // SAFETY: host writes the encoded UTF-8 string into the bump arena.
50    unsafe { mem::read_packed_string(result) }.unwrap_or_default()
51}
52
53/// URL-decode a percent-encoded string.
54///
55/// Returns `None` if the input contains invalid percent escapes.
56pub fn url_decode(input: &str) -> Option<String> {
57    let (data_ptr, data_len) = mem::host_arg_str(input);
58    let result = unsafe { ffi::encoding_url_decode(data_ptr, data_len) };
59    // SAFETY: host writes the decoded UTF-8 string into the bump arena.
60    unsafe { mem::read_packed_string(result) }
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66    use crate::ffi::test_host;
67
68    #[test]
69    fn base64_encode_passes_input_and_returns_response() {
70        test_host::reset();
71        test_host::with_mock(|m| {
72            m.encoding_base64_encode_response = Some("aGVsbG8=".into());
73        });
74        assert_eq!(base64_encode(b"hello"), "aGVsbG8=");
75        assert_eq!(
76            test_host::read_mock(|m| m.last_encoding_base64_encode_input.clone()),
77            Some(b"hello".to_vec())
78        );
79    }
80
81    #[test]
82    fn base64_encode_empty_when_host_silent() {
83        test_host::reset();
84        assert_eq!(base64_encode(b"x"), "");
85    }
86
87    #[test]
88    fn base64_decode_returns_bytes() {
89        test_host::reset();
90        test_host::with_mock(|m| {
91            m.encoding_base64_decode_response = Some(b"hello".to_vec());
92        });
93        assert_eq!(base64_decode("aGVsbG8="), Some(b"hello".to_vec()));
94    }
95
96    #[test]
97    fn base64_decode_none_on_failure() {
98        test_host::reset();
99        assert!(base64_decode("not base64!").is_none());
100    }
101
102    #[test]
103    fn hex_encode_round_trip_via_mock() {
104        test_host::reset();
105        test_host::with_mock(|m| {
106            m.encoding_hex_encode_response = Some("deadbeef".into());
107            m.encoding_hex_decode_response = Some(vec![0xde, 0xad, 0xbe, 0xef]);
108        });
109        let encoded = hex_encode(&[0xde, 0xad, 0xbe, 0xef]);
110        assert_eq!(encoded, "deadbeef");
111        let decoded = hex_decode(&encoded).unwrap();
112        assert_eq!(decoded, vec![0xde, 0xad, 0xbe, 0xef]);
113    }
114
115    #[test]
116    fn hex_decode_none_on_failure() {
117        test_host::reset();
118        assert!(hex_decode("zz").is_none());
119    }
120
121    #[test]
122    fn url_encode_passes_input() {
123        test_host::reset();
124        test_host::with_mock(|m| {
125            m.encoding_url_encode_response = Some("hello%20world".into());
126        });
127        assert_eq!(url_encode("hello world"), "hello%20world");
128        assert_eq!(
129            test_host::read_mock(|m| m.last_encoding_url_encode_input.clone()),
130            Some("hello world".into())
131        );
132    }
133
134    #[test]
135    fn url_decode_returns_some() {
136        test_host::reset();
137        test_host::with_mock(|m| {
138            m.encoding_url_decode_response = Some("hello world".into());
139        });
140        assert_eq!(url_decode("hello%20world").as_deref(), Some("hello world"));
141    }
142
143    #[test]
144    fn url_decode_none_on_failure() {
145        test_host::reset();
146        assert!(url_decode("%ZZ").is_none());
147    }
148}