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        && (type_str == TYPE_BYTES || type_str == TYPE_BUFFER)
27    {
28        return general_purpose::STANDARD.decode(data).ok();
29    }
30    None
31}
32
33/// Encode bytes as a JSON object with type marker.
34///
35/// This creates a portable representation that can be decoded on any platform.
36pub fn encode_bytes_as_json(bytes: &[u8], type_marker: &str) -> Value {
37    let base64_str = general_purpose::STANDARD.encode(bytes);
38    let mut map = JsonMap::new();
39    map.insert(
40        TYPE_MARKER_KEY.to_string(),
41        Value::String(type_marker.to_string()),
42    );
43    map.insert(DATA_KEY.to_string(), Value::String(base64_str));
44    Value::Object(map)
45}
46
47/// Encode bytes using the Python-style marker ("bytes").
48pub fn encode_bytes_python(bytes: &[u8]) -> Value {
49    encode_bytes_as_json(bytes, TYPE_BYTES)
50}
51
52/// Encode bytes using the Node.js-style marker ("buffer").
53pub fn encode_bytes_nodejs(bytes: &[u8]) -> Value {
54    encode_bytes_as_json(bytes, TYPE_BUFFER)
55}
56
57/// Base64 encode bytes to a string.
58pub fn bytes_to_base64(bytes: &[u8]) -> String {
59    general_purpose::STANDARD.encode(bytes)
60}
61
62/// Base64 decode a string to bytes.
63pub fn base64_to_bytes(encoded: &str) -> Result<Vec<u8>, base64::DecodeError> {
64    general_purpose::STANDARD.decode(encoded)
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn test_encode_decode_bytes_roundtrip() {
73        let original = vec![1u8, 2, 3, 4, 5, 255, 0, 128];
74
75        // Test Python-style encoding
76        let encoded = encode_bytes_python(&original);
77        if let Value::Object(obj) = &encoded {
78            let decoded = try_decode_bytes_object(obj).expect("Should decode");
79            assert_eq!(decoded, original);
80        } else {
81            panic!("Expected object");
82        }
83
84        // Test Node.js-style encoding
85        let encoded_node = encode_bytes_nodejs(&original);
86        if let Value::Object(obj) = &encoded_node {
87            let decoded = try_decode_bytes_object(obj).expect("Should decode");
88            assert_eq!(decoded, original);
89        } else {
90            panic!("Expected object");
91        }
92    }
93
94    #[test]
95    fn test_regular_object_not_decoded() {
96        let mut obj = JsonMap::new();
97        obj.insert("key".to_string(), Value::String("value".to_string()));
98
99        assert!(try_decode_bytes_object(&obj).is_none());
100    }
101
102    #[test]
103    fn test_base64_roundtrip() {
104        let original = b"Hello, World!";
105        let encoded = bytes_to_base64(original);
106        let decoded = base64_to_bytes(&encoded).expect("Should decode");
107        assert_eq!(decoded, original);
108    }
109}