Skip to main content

redevplugin_worker_sdk/
lib.rs

1use base64::Engine as _;
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4use std::ptr;
5
6mod hostcalls;
7
8pub use hostcalls::{network, storage};
9
10pub const WORKER_ABI_VERSION: &str = "redevplugin-wasm-worker-v2";
11pub const WORKER_REQUEST_SCHEMA_VERSION: &str = "redevplugin.worker_request.v2";
12const MAX_HOSTCALL_RESPONSE_BYTES: usize = 512 * 1024;
13
14#[derive(Debug, Clone, Deserialize)]
15#[serde(deny_unknown_fields)]
16pub struct WorkerRequest {
17    pub schema_version: String,
18    pub method: String,
19    pub params: Value,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct WorkerError {
24    pub code: String,
25    pub message: String,
26}
27
28impl WorkerError {
29    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
30        Self {
31            code: code.into(),
32            message: message.into(),
33        }
34    }
35
36    pub fn invalid_request(message: impl Into<String>) -> Self {
37        Self::new("INVALID_REQUEST", message)
38    }
39
40    pub fn hostcall(message: impl Into<String>) -> Self {
41        Self::new("HOSTCALL_FAILED", message)
42    }
43}
44
45pub type WorkerResult = Result<Value, WorkerError>;
46
47#[derive(Serialize)]
48#[serde(untagged)]
49enum WorkerResponse<'a> {
50    Success {
51        ok: bool,
52        data: &'a Value,
53    },
54    Failure {
55        ok: bool,
56        error_code: &'a str,
57        message: &'a str,
58    },
59}
60
61pub fn decode_base64(value: &str) -> Result<Vec<u8>, WorkerError> {
62    base64::engine::general_purpose::STANDARD
63        .decode(value)
64        .map_err(|err| WorkerError::hostcall(format!("decode base64 response: {err}")))
65}
66
67pub fn decode_base64_text(value: &str) -> Result<String, WorkerError> {
68    String::from_utf8(decode_base64(value)?)
69        .map_err(|_| WorkerError::hostcall("decoded response is not UTF-8"))
70}
71
72#[doc(hidden)]
73pub fn allocate(length: u32) -> u32 {
74    if length == 0 {
75        return 0;
76    }
77    let (pointer, _) = leak_buffer(vec![0_u8; length as usize].into_boxed_slice());
78    pointer as u32
79}
80
81#[doc(hidden)]
82pub unsafe fn deallocate(pointer: u32, length: u32) {
83    if pointer == 0 || length == 0 {
84        return;
85    }
86    unsafe { drop_buffer(pointer as *mut u8, length as usize) }
87}
88
89#[doc(hidden)]
90pub unsafe fn invoke(pointer: u32, length: u32, handler: fn(WorkerRequest) -> WorkerResult) -> u64 {
91    let request_bytes = if pointer == 0 || length == 0 {
92        &[][..]
93    } else {
94        unsafe { std::slice::from_raw_parts(pointer as *const u8, length as usize) }
95    };
96    let result = serde_json::from_slice::<WorkerRequest>(request_bytes)
97        .map_err(|err| WorkerError::invalid_request(format!("decode worker request: {err}")))
98        .and_then(|request| {
99            if request.schema_version != WORKER_REQUEST_SCHEMA_VERSION {
100                return Err(WorkerError::invalid_request(
101                    "worker request schema version is unsupported",
102                ));
103            }
104            if request.method.trim().is_empty() || !request.params.is_object() {
105                return Err(WorkerError::invalid_request(
106                    "worker method and object params are required",
107                ));
108            }
109            handler(request)
110        });
111    let response = match &result {
112        Ok(data) => WorkerResponse::Success { ok: true, data },
113        Err(error) => WorkerResponse::Failure {
114            ok: false,
115            error_code: &error.code,
116            message: &error.message,
117        },
118    };
119    let bytes = serde_json::to_vec(&response).unwrap_or_else(|_| {
120        br#"{"ok":false,"error_code":"WORKER_SERIALIZATION_FAILED","message":"worker response serialization failed"}"#.to_vec()
121    });
122    let (pointer, length) = leak_buffer(bytes.into_boxed_slice());
123    let pointer = pointer as u32;
124    let length = length as u32;
125    ((pointer as u64) << 32) | length as u64
126}
127
128fn leak_buffer(buffer: Box<[u8]>) -> (*mut u8, usize) {
129    let length = buffer.len();
130    let pointer = Box::into_raw(buffer) as *mut u8;
131    (pointer, length)
132}
133
134unsafe fn drop_buffer(pointer: *mut u8, length: usize) {
135    let slice = ptr::slice_from_raw_parts_mut(pointer, length);
136    unsafe { drop(Box::from_raw(slice)) }
137}
138
139#[macro_export]
140macro_rules! export_worker {
141    ($handler:path) => {
142        #[unsafe(no_mangle)]
143        pub extern "C" fn redevplugin_worker_alloc(length: u32) -> u32 {
144            $crate::allocate(length)
145        }
146
147        #[unsafe(no_mangle)]
148        pub unsafe extern "C" fn redevplugin_worker_dealloc(pointer: u32, length: u32) {
149            unsafe { $crate::deallocate(pointer, length) }
150        }
151
152        #[unsafe(no_mangle)]
153        pub unsafe extern "C" fn redevplugin_worker_invoke(pointer: u32, length: u32) -> u64 {
154            unsafe { $crate::invoke(pointer, length, $handler) }
155        }
156    };
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162    use serde_json::json;
163
164    #[test]
165    fn worker_error_helpers_are_stable() {
166        assert_eq!(
167            WorkerError::invalid_request("missing title"),
168            WorkerError::new("INVALID_REQUEST", "missing title")
169        );
170    }
171
172    #[test]
173    fn base64_helpers_decode_network_payloads() {
174        assert_eq!(decode_base64_text("SGVsbG8=").unwrap(), "Hello");
175        assert!(decode_base64_text("not-base64").is_err());
176    }
177
178    #[test]
179    fn request_shape_is_closed() {
180        let valid: WorkerRequest = serde_json::from_value(json!({
181            "schema_version": WORKER_REQUEST_SCHEMA_VERSION,
182            "method": "notes.list",
183            "params": {}
184        }))
185        .unwrap();
186        assert_eq!(valid.method, "notes.list");
187        assert!(
188            serde_json::from_value::<WorkerRequest>(json!({
189                "schema_version": WORKER_REQUEST_SCHEMA_VERSION,
190                "method": "notes.list",
191                "params": {},
192                "gateway_token": "secret"
193            }))
194            .is_err()
195        );
196    }
197
198    #[test]
199    fn exact_layout_buffers_round_trip_across_response_sizes() {
200        for length in [1_usize, 7, 255, 4096, 65_537] {
201            let expected = (0..length)
202                .map(|index| (index % 251) as u8)
203                .collect::<Vec<_>>();
204            let (pointer, actual_length) = leak_buffer(expected.clone().into_boxed_slice());
205            assert_eq!(actual_length, length);
206            let actual = unsafe { std::slice::from_raw_parts(pointer, actual_length) };
207            assert_eq!(actual, expected);
208            unsafe { drop_buffer(pointer, actual_length) };
209        }
210    }
211}