Skip to main content

mx_core/
encoding.rs

1//! Encoding and decoding utilities for base64 and hex.
2//!
3//! Provides helper functions commonly used throughout the codebase.
4
5use base64::Engine;
6use prost::bytes::Bytes;
7
8use crate::error::CoreError;
9
10/// Decodes a base64-encoded string, trying standard encoding first.
11///
12/// # Arguments
13/// * `input` - The base64-encoded string
14///
15/// # Returns
16/// The decoded bytes, or an error message if decoding fails.
17pub fn decode_base64(input: &str) -> Result<Vec<u8>, CoreError> {
18    base64::engine::general_purpose::STANDARD
19        .decode(input.trim())
20        .map_err(|e| CoreError::InvalidBase64(e.to_string()))
21}
22
23/// Decodes a base64-encoded string to Bytes.
24///
25/// # Arguments
26/// * `input` - The base64-encoded string
27///
28/// # Returns
29/// The decoded Bytes, or an error message if decoding fails.
30pub fn decode_base64_bytes(input: &str) -> Result<Bytes, CoreError> {
31    decode_base64(input).map(Bytes::from)
32}
33
34/// Decodes an optional base64-encoded string.
35///
36/// # Arguments
37/// * `input` - Optional base64-encoded string
38///
39/// # Returns
40/// None if input is None or empty, otherwise the decoded Bytes or an error.
41pub fn decode_optional_base64(input: Option<&str>) -> Result<Option<Bytes>, CoreError> {
42    match input {
43        Some(s) if !s.trim().is_empty() => decode_base64_bytes(s).map(Some),
44        _ => Ok(None),
45    }
46}
47
48/// Decodes a hex-encoded string.
49///
50/// # Arguments
51/// * `input` - The hex-encoded string
52///
53/// # Returns
54/// The decoded bytes as Bytes, or an error message if decoding fails.
55pub fn decode_hex(input: &str) -> Result<Bytes, CoreError> {
56    hex::decode(input.trim())
57        .map(Bytes::from)
58        .map_err(|e| CoreError::InvalidHex(e.to_string()))
59}
60
61/// Decodes an optional hex-encoded string.
62///
63/// # Arguments
64/// * `input` - Optional hex-encoded string
65///
66/// # Returns
67/// None if input is None or empty, otherwise the decoded Bytes or an error.
68pub fn decode_optional_hex(input: Option<&str>) -> Result<Option<Bytes>, CoreError> {
69    match input {
70        Some(s) if !s.trim().is_empty() => decode_hex(s).map(Some),
71        _ => Ok(None),
72    }
73}
74
75/// Decodes data that may be either base64 or hex encoded.
76/// Tries base64 first, then falls back to hex.
77///
78/// This matches the Go implementation's permissive decoding behavior.
79///
80/// # Warning: Ambiguous inputs
81///
82/// Strings that are valid in **both** base64 and hex will always be decoded as
83/// base64. For example, `"AABB"` is valid hex (`[0xAA, 0xBB]`) *and* valid
84/// base64 (`[0x00, 0x00, 0x1B]`). Because base64 is tried first, the base64
85/// interpretation wins. This is **intentional** — the Go SDK uses the same
86/// heuristic. Callers who know the encoding should use [`decode_base64`] or
87/// [`decode_hex`] directly.
88///
89/// # Arguments
90/// * `input` - The encoded string (base64 or hex)
91///
92/// # Returns
93/// The decoded bytes as Bytes.
94pub fn decode_base64_or_hex(input: &str) -> Result<Bytes, CoreError> {
95    // Try base64 first; if it fails, fall back to hex decode
96    match base64::engine::general_purpose::STANDARD.decode(input.trim()) {
97        Ok(bytes) => Ok(Bytes::from(bytes)),
98        Err(_) => decode_hex(input),
99    }
100}
101
102/// Filters and trims a username field.
103///
104/// Returns None if the username is empty or only whitespace after trimming.
105///
106/// # Arguments
107/// * `username` - Optional username string
108///
109/// # Returns
110/// The trimmed username if non-empty, otherwise None.
111pub fn filter_username(username: Option<&str>) -> Option<&str> {
112    username.map(str::trim).filter(|s| !s.is_empty())
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn test_decode_base64() {
121        let encoded = "SGVsbG8gV29ybGQ="; // "Hello World"
122        let decoded = decode_base64(encoded).unwrap();
123        assert_eq!(decoded, b"Hello World");
124    }
125
126    #[test]
127    fn test_decode_base64_invalid() {
128        let result = decode_base64("not-valid-base64!!!");
129        assert!(result.is_err());
130    }
131
132    #[test]
133    fn test_decode_hex() {
134        let encoded = "48656c6c6f"; // "Hello"
135        let decoded = decode_hex(encoded).unwrap();
136        assert_eq!(decoded.as_ref(), b"Hello");
137    }
138
139    #[test]
140    fn test_decode_hex_invalid() {
141        let result = decode_hex("not-valid-hex");
142        assert!(result.is_err());
143    }
144
145    #[test]
146    fn test_decode_base64_or_hex_base64() {
147        let encoded = "SGVsbG8="; // "Hello" in base64
148        let decoded = decode_base64_or_hex(encoded).unwrap();
149        assert_eq!(decoded.as_ref(), b"Hello");
150    }
151
152    #[test]
153    fn test_decode_base64_or_hex_hex() {
154        let encoded = "48656c6c6f"; // "Hello" in hex
155        let decoded = decode_base64_or_hex(encoded).unwrap();
156        assert_eq!(decoded.as_ref(), b"Hello");
157    }
158
159    #[test]
160    fn test_decode_optional_base64_none() {
161        let result = decode_optional_base64(None).unwrap();
162        assert!(result.is_none());
163    }
164
165    #[test]
166    fn test_decode_optional_base64_empty() {
167        let result = decode_optional_base64(Some("")).unwrap();
168        assert!(result.is_none());
169    }
170
171    #[test]
172    fn test_decode_optional_hex_none() {
173        let result = decode_optional_hex(None).unwrap();
174        assert!(result.is_none());
175    }
176
177    #[test]
178    fn test_decode_base64_or_hex_ambiguity() {
179        // "AABB" is valid both as hex ([0xAA, 0xBB]) and base64 ([0x00, 0x00, 0x1B]).
180        // Because base64 is tried first, the base64 interpretation wins.
181        // This is intentional behavior matching the Go SDK.
182        let decoded = decode_base64_or_hex("AABB").unwrap();
183        // base64("AABB") = [0x00, 0x00, 0x41], NOT hex [0xAA, 0xBB]
184        assert_eq!(decoded.as_ref(), &[0x00, 0x00, 0x41]);
185        assert_ne!(decoded.as_ref(), &[0xAA, 0xBB]);
186    }
187
188    #[test]
189    fn test_filter_username_valid() {
190        let result = filter_username(Some("user123"));
191        assert_eq!(result, Some("user123"));
192    }
193
194    #[test]
195    fn test_filter_username_empty() {
196        let result = filter_username(Some(""));
197        assert_eq!(result, None);
198    }
199
200    #[test]
201    fn test_filter_username_whitespace() {
202        let result = filter_username(Some("  "));
203        assert_eq!(result, None);
204    }
205
206    #[test]
207    fn test_filter_username_none() {
208        let result = filter_username(None);
209        assert_eq!(result, None);
210    }
211}