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 mod api;
9pub mod error;
10pub mod fs;
11pub mod http;
12mod resource;
13pub mod tcp;
14pub mod udp;
15pub mod websocket;
16
17pub use error::{Error, ErrorCode};
18pub type PlatformResult<T> = error::Result<T>;
19pub use hostcalls::{network, storage};
20pub use resource::{
21    IO_FLAG_BINARY, IO_FLAG_DATAGRAM_END, IO_FLAG_EOF, IO_FLAG_MESSAGE_END, IO_FLAG_TEXT,
22    MAX_IO_CHUNK_BYTES,
23};
24
25pub const WORKER_ABI_VERSION: &str = "redevplugin-wasm-worker-v2";
26pub const WORKER_REQUEST_SCHEMA_VERSION: &str = "redevplugin.worker_request.v2";
27const MAX_HOSTCALL_RESPONSE_BYTES: usize = 512 * 1024;
28
29#[derive(Debug, Clone, Deserialize)]
30#[serde(deny_unknown_fields)]
31pub struct WorkerRequest {
32    pub schema_version: String,
33    pub method: String,
34    pub params: Value,
35}
36
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct WorkerError {
39    pub code: String,
40    pub message: String,
41}
42
43impl WorkerError {
44    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
45        Self {
46            code: code.into(),
47            message: message.into(),
48        }
49    }
50
51    pub fn invalid_request(message: impl Into<String>) -> Self {
52        Self::new("INVALID_REQUEST", message)
53    }
54
55    pub fn hostcall(message: impl Into<String>) -> Self {
56        Self::new("HOSTCALL_FAILED", message)
57    }
58}
59
60pub type WorkerResult = Result<Value, WorkerError>;
61
62#[derive(Serialize)]
63#[serde(untagged)]
64enum WorkerResponse<'a> {
65    Success {
66        ok: bool,
67        data: &'a Value,
68    },
69    Failure {
70        ok: bool,
71        error_code: &'a str,
72        message: &'a str,
73    },
74}
75
76pub fn decode_base64(value: &str) -> Result<Vec<u8>, WorkerError> {
77    base64::engine::general_purpose::STANDARD
78        .decode(value)
79        .map_err(|err| WorkerError::hostcall(format!("decode base64 response: {err}")))
80}
81
82pub fn decode_base64_text(value: &str) -> Result<String, WorkerError> {
83    String::from_utf8(decode_base64(value)?)
84        .map_err(|_| WorkerError::hostcall("decoded response is not UTF-8"))
85}
86
87#[doc(hidden)]
88pub fn allocate(length: u32) -> u32 {
89    if length == 0 {
90        return 0;
91    }
92    let (pointer, _) = leak_buffer(vec![0_u8; length as usize].into_boxed_slice());
93    pointer as u32
94}
95
96#[doc(hidden)]
97pub unsafe fn deallocate(pointer: u32, length: u32) {
98    if pointer == 0 || length == 0 {
99        return;
100    }
101    unsafe { drop_buffer(pointer as *mut u8, length as usize) }
102}
103
104#[doc(hidden)]
105pub unsafe fn invoke(pointer: u32, length: u32, handler: fn(WorkerRequest) -> WorkerResult) -> u64 {
106    let request_bytes = if pointer == 0 || length == 0 {
107        &[][..]
108    } else {
109        unsafe { std::slice::from_raw_parts(pointer as *const u8, length as usize) }
110    };
111    let result = serde_json::from_slice::<WorkerRequest>(request_bytes)
112        .map_err(|err| WorkerError::invalid_request(format!("decode worker request: {err}")))
113        .and_then(|request| {
114            if request.schema_version != WORKER_REQUEST_SCHEMA_VERSION {
115                return Err(WorkerError::invalid_request(
116                    "worker request schema version is unsupported",
117                ));
118            }
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            "schema_version": WORKER_REQUEST_SCHEMA_VERSION,
197            "method": "notes.list",
198            "params": {}
199        }))
200        .unwrap();
201        assert_eq!(valid.method, "notes.list");
202        assert!(
203            serde_json::from_value::<WorkerRequest>(json!({
204                "schema_version": WORKER_REQUEST_SCHEMA_VERSION,
205                "method": "notes.list",
206                "params": {},
207                "gateway_token": "secret"
208            }))
209            .is_err()
210        );
211    }
212
213    #[test]
214    fn exact_layout_buffers_round_trip_across_response_sizes() {
215        for length in [1_usize, 7, 255, 4096, 65_537] {
216            let expected = (0..length)
217                .map(|index| (index % 251) as u8)
218                .collect::<Vec<_>>();
219            let (pointer, actual_length) = leak_buffer(expected.clone().into_boxed_slice());
220            assert_eq!(actual_length, length);
221            let actual = unsafe { std::slice::from_raw_parts(pointer, actual_length) };
222            assert_eq!(actual, expected);
223            unsafe { drop_buffer(pointer, actual_length) };
224        }
225    }
226}