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