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