pub mod bindings;
pub mod context;
pub mod types;
pub use bindings::{invoke_capability_raw, log, timestamp_ms};
pub use context::{capabilities, EventSubscription, ExtensionContext};
pub use types::*;
pub const RESULT_OFFSET: usize = 65536;
pub const RESULT_MAX_LEN: usize = 65536;
pub const INPUT_OFFSET: usize = 131072;
pub const INPUT_MAX_LEN: usize = 65536;
pub fn write_result(result: &str) -> i32 {
let bytes = result.as_bytes();
let write_len = bytes.len().min(RESULT_MAX_LEN - 1);
unsafe {
let dest = RESULT_OFFSET as *mut u8;
core::ptr::copy_nonoverlapping(bytes.as_ptr(), dest, write_len);
*dest.add(write_len) = 0; }
write_len as i32
}
pub fn write_bytes_at_offset(offset: usize, data: &[u8]) -> i32 {
let write_len = data.len().min(RESULT_MAX_LEN - 1);
unsafe {
let dest = offset as *mut u8;
core::ptr::copy_nonoverlapping(data.as_ptr(), dest, write_len);
*dest.add(write_len) = 0; }
write_len as i32
}
pub unsafe fn read_bytes_from_memory(ptr: i32, len: i32) -> Vec<u8> {
if ptr == 0 || len <= 0 {
return Vec::new();
}
let slice = core::slice::from_raw_parts(ptr as *const u8, len as usize);
slice.to_vec()
}
pub unsafe fn read_string_from_memory(ptr: i32, len: i32) -> String {
let bytes = read_bytes_from_memory(ptr, len);
String::from_utf8_lossy(&bytes).to_string()
}
pub fn read_result_json() -> Option<serde_json::Value> {
unsafe {
let start = RESULT_OFFSET as *const u8;
let mut end = 0usize;
while end < RESULT_MAX_LEN {
if *start.add(end) == 0 {
break;
}
end += 1;
}
if end == 0 {
return None;
}
let slice = core::slice::from_raw_parts(start, end);
let json_str = core::str::from_utf8(slice).ok()?;
serde_json::from_str(json_str).ok()
}
}
pub fn parse_json(bytes: &[u8]) -> Result<serde_json::Value, String> {
let json_str = core::str::from_utf8(bytes).map_err(|e| format!("Invalid UTF-8: {}", e))?;
serde_json::from_str(json_str).map_err(|e| format!("Invalid JSON: {}", e))
}
pub fn error_response(error: &str) -> String {
serde_json::to_string(&serde_json::json!({
"success": false,
"error": error
}))
.unwrap_or_else(|_| r#"{"success":false,"error":"JSON error"}"#.to_string())
}
pub fn success_response(result: serde_json::Value) -> String {
serde_json::to_string(&serde_json::json!({
"success": true,
"result": result
}))
.unwrap_or_else(|_| r#"{"success":false,"error":"JSON error"}"#.to_string())
}
pub fn encode_base64(data: &[u8]) -> String {
use base64::{engine::general_purpose::STANDARD, Engine};
STANDARD.encode(data)
}
pub fn current_timestamp_ms() -> i64 {
timestamp_ms()
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_memory_layout_constants() {
assert_eq!(RESULT_OFFSET, 65536); assert_eq!(RESULT_MAX_LEN, 65536); assert_eq!(INPUT_OFFSET, 131072); assert_eq!(INPUT_MAX_LEN, 65536);
assert!(RESULT_OFFSET + RESULT_MAX_LEN <= INPUT_OFFSET);
}
#[test]
fn test_write_result() {
let result = r#"{"success":true,"data":42}"#;
let len = write_result(result);
assert_eq!(len as usize, result.len());
}
#[test]
fn test_write_result_truncation() {
let large_string = "x".repeat(RESULT_MAX_LEN + 1000);
let len = write_result(&large_string);
assert_eq!(len as usize, RESULT_MAX_LEN - 1);
}
#[test]
fn test_write_bytes_at_offset() {
let data = b"test data";
let len = write_bytes_at_offset(RESULT_OFFSET, data);
assert_eq!(len as usize, data.len());
}
#[test]
fn test_read_bytes_from_memory_null_ptr() {
unsafe {
let result = read_bytes_from_memory(0, 10);
assert!(result.is_empty());
}
}
#[test]
fn test_read_bytes_from_memory_invalid_len() {
unsafe {
let result = read_bytes_from_memory(100, -1);
assert!(result.is_empty());
let result = read_bytes_from_memory(100, 0);
assert!(result.is_empty());
}
}
#[test]
fn test_read_string_from_memory_null_ptr() {
unsafe {
let result = read_string_from_memory(0, 10);
assert!(result.is_empty());
}
}
#[test]
fn test_parse_json_valid() {
let json_bytes = br#"{"key":"value"}"#;
let result = parse_json(json_bytes);
assert!(result.is_ok());
let value = result.unwrap();
assert_eq!(value["key"], "value");
}
#[test]
fn test_parse_json_invalid() {
let invalid_bytes = b"not valid json";
let result = parse_json(invalid_bytes);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Invalid JSON"));
}
#[test]
fn test_parse_json_invalid_utf8() {
let invalid_utf8 = &[0xFF, 0xFE, 0xFD];
let result = parse_json(invalid_utf8);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Invalid UTF-8"));
}
#[test]
fn test_error_response() {
let error = error_response("Something went wrong");
assert!(error.contains("success"));
assert!(error.contains("false"));
assert!(error.contains("Something went wrong"));
let parsed: serde_json::Value = serde_json::from_str(&error).unwrap();
assert_eq!(parsed["success"], false);
assert_eq!(parsed["error"], "Something went wrong");
}
#[test]
fn test_success_response() {
let result = json!({"value": 42});
let response = success_response(result.clone());
assert!(response.contains("success"));
assert!(response.contains("true"));
let parsed: serde_json::Value = serde_json::from_str(&response).unwrap();
assert_eq!(parsed["success"], true);
assert_eq!(parsed["result"]["value"], 42);
}
#[test]
fn test_encode_base64() {
let data = b"hello world";
let encoded = encode_base64(data);
assert!(!encoded.is_empty());
assert_ne!(encoded, "hello world");
use base64::{engine::general_purpose::STANDARD, Engine};
let decoded = STANDARD.decode(&encoded).unwrap();
assert_eq!(decoded, data);
}
#[test]
fn test_encode_base64_empty() {
let data = b"";
let encoded = encode_base64(data);
assert_eq!(encoded, "");
}
#[test]
fn test_encode_base64_binary() {
let data: &[u8] = &[0x00, 0xFF, 0x80, 0x7F];
let encoded = encode_base64(data);
use base64::{engine::general_purpose::STANDARD, Engine};
let decoded = STANDARD.decode(&encoded).unwrap();
assert_eq!(decoded, data);
}
#[test]
fn test_current_timestamp_ms_returns_value() {
let _ = current_timestamp_ms();
}
#[test]
fn test_result_offset_alignment() {
assert_eq!(
RESULT_OFFSET % 8,
0,
"Result offset should be 8-byte aligned"
);
assert_eq!(INPUT_OFFSET % 8, 0, "Input offset should be 8-byte aligned");
}
#[test]
fn test_json_roundtrip_complex() {
let complex = json!({
"device": {
"id": "device-1",
"metrics": [
{"name": "temp", "value": 25.5},
{"name": "humidity", "value": 65.0},
],
},
"timestamp": 1700000000000i64,
"valid": true,
"notes": null,
});
let error_str = error_response(&serde_json::to_string(&complex).unwrap());
assert!(error_str.contains("success"));
let success_str = success_response(complex.clone());
let parsed: serde_json::Value = serde_json::from_str(&success_str).unwrap();
assert_eq!(parsed["result"]["device"]["id"], "device-1");
}
#[test]
fn test_write_result_empty() {
let len = write_result("");
assert_eq!(len, 0);
}
#[test]
fn test_write_result_unicode() {
let unicode_str = "你好世界 🌍";
let len = write_result(unicode_str);
assert!(len as usize >= unicode_str.chars().count());
}
}