Skip to main content

jacs_binding_core/
conversion.rs

1//! Common type conversion utilities for bindings.
2//!
3//! This module provides shared functionality for converting between
4//! language-native types and serde_json::Value. Bindings use these
5//! helpers along with their language-specific conversion code.
6
7use base64::{Engine as _, engine::general_purpose};
8use serde_json::{Map as JsonMap, Value};
9
10/// Marker key for specially encoded types in JSON objects.
11pub const TYPE_MARKER_KEY: &str = "__type__";
12/// Data key for specially encoded types in JSON objects.
13pub const DATA_KEY: &str = "data";
14/// Type marker for bytes/buffer data.
15pub const TYPE_BYTES: &str = "bytes";
16/// Type marker for Node.js Buffer data.
17pub const TYPE_BUFFER: &str = "buffer";
18
19/// Check if a JSON object represents specially encoded bytes.
20///
21/// Returns the decoded bytes if the object has the correct structure,
22/// or None if it's a regular object.
23pub fn try_decode_bytes_object(obj: &serde_json::Map<String, Value>) -> Option<Vec<u8>> {
24    if let (Some(Value::String(type_str)), Some(Value::String(data))) =
25        (obj.get(TYPE_MARKER_KEY), obj.get(DATA_KEY))
26    {
27        if type_str == TYPE_BYTES || type_str == TYPE_BUFFER {
28            return general_purpose::STANDARD.decode(data).ok();
29        }
30    }
31    None
32}
33
34/// Encode bytes as a JSON object with type marker.
35///
36/// This creates a portable representation that can be decoded on any platform.
37pub fn encode_bytes_as_json(bytes: &[u8], type_marker: &str) -> Value {
38    let base64_str = general_purpose::STANDARD.encode(bytes);
39    let mut map = JsonMap::new();
40    map.insert(
41        TYPE_MARKER_KEY.to_string(),
42        Value::String(type_marker.to_string()),
43    );
44    map.insert(DATA_KEY.to_string(), Value::String(base64_str));
45    Value::Object(map)
46}
47
48/// Encode bytes using the Python-style marker ("bytes").
49pub fn encode_bytes_python(bytes: &[u8]) -> Value {
50    encode_bytes_as_json(bytes, TYPE_BYTES)
51}
52
53/// Encode bytes using the Node.js-style marker ("buffer").
54pub fn encode_bytes_nodejs(bytes: &[u8]) -> Value {
55    encode_bytes_as_json(bytes, TYPE_BUFFER)
56}
57
58/// Base64 encode bytes to a string.
59pub fn bytes_to_base64(bytes: &[u8]) -> String {
60    general_purpose::STANDARD.encode(bytes)
61}
62
63/// Base64 decode a string to bytes.
64pub fn base64_to_bytes(encoded: &str) -> Result<Vec<u8>, base64::DecodeError> {
65    general_purpose::STANDARD.decode(encoded)
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn test_encode_decode_bytes_roundtrip() {
74        let original = vec![1u8, 2, 3, 4, 5, 255, 0, 128];
75
76        // Test Python-style encoding
77        let encoded = encode_bytes_python(&original);
78        if let Value::Object(obj) = &encoded {
79            let decoded = try_decode_bytes_object(obj).expect("Should decode");
80            assert_eq!(decoded, original);
81        } else {
82            panic!("Expected object");
83        }
84
85        // Test Node.js-style encoding
86        let encoded_node = encode_bytes_nodejs(&original);
87        if let Value::Object(obj) = &encoded_node {
88            let decoded = try_decode_bytes_object(obj).expect("Should decode");
89            assert_eq!(decoded, original);
90        } else {
91            panic!("Expected object");
92        }
93    }
94
95    #[test]
96    fn test_regular_object_not_decoded() {
97        let mut obj = JsonMap::new();
98        obj.insert("key".to_string(), Value::String("value".to_string()));
99
100        assert!(try_decode_bytes_object(&obj).is_none());
101    }
102
103    #[test]
104    fn test_base64_roundtrip() {
105        let original = b"Hello, World!";
106        let encoded = bytes_to_base64(original);
107        let decoded = base64_to_bytes(&encoded).expect("Should decode");
108        assert_eq!(decoded, original);
109    }
110}