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