dwara_core/wasm/
abi.rs

1//! proxy-wasm ABI constants and types (DW-055).
2//!
3//! The proxy-wasm ABI is the standard interface between a WebAssembly
4//! plugin and the host proxy. The spec lives at
5//! <https://github.com/proxy-wasm/spec>. This module defines the
6//! constants the host and plugin exchange over the ABI boundary; the
7//! host implementation (the import functions the plugin calls back to)
8//! lives in [`super::host`], and the plugin lifecycle (the exports the
9//! host calls) lives in `super::instance`.
10//!
11//! Only the HTTP filter subset is implemented (dwara is an HTTP
12//! gateway, not a TCP proxy): the stream-context calls
13//! (`proxy_on_downstream_data`, `proxy_on_upstream_data`, etc.) are
14//! stubbed to Continue, and the TCP/context-creation exports are
15//! called but their actions are treated as Continue-only.
16
17// --- Buffer types (the `bt` parameter in many ABI calls) ---------------
18//
19// Maps a buffer type integer to the logical data stream the host
20// should read from or write to. The values follow the proxy-wasm spec
21// (§2.1 Buffer Types and §2.2 Map Types — the buffer hostcalls use the
22// BufferType namespace, the header-map hostcalls the MapType
23// namespace; both are 0-based and deliberately share the `bt`
24// parameter). NOTE: the header-map values are the SPEC values
25// (request headers = 0, response headers = 2) — the Rust proxy-wasm
26// SDK (and Envoy/Kong filters built against it) send exactly these.
27
28/// Buffer type: HTTP request body (`proxy_get/set_buffer_bytes`).
29pub const BUFFER_REQUEST_BODY: u32 = 0;
30/// Buffer type: HTTP response body (`proxy_get/set_buffer_bytes`).
31pub const BUFFER_RESPONSE_BODY: u32 = 1;
32/// Map type: HTTP request headers (header-map hostcalls).
33pub const BUFFER_REQUEST_HEADERS: u32 = 0;
34/// Map type: HTTP request trailers (header-map hostcalls).
35pub const BUFFER_REQUEST_TRAILERS: u32 = 1;
36/// Map type: HTTP response headers (header-map hostcalls).
37pub const BUFFER_RESPONSE_HEADERS: u32 = 2;
38/// Map type: HTTP response trailers (header-map hostcalls).
39pub const BUFFER_RESPONSE_TRAILERS: u32 = 3;
40/// Map type: HTTP callout response headers (proxy-wasm MapType 6, the
41/// map `proxy_on_http_call_response` callbacks read via
42/// `proxy_get_header_map_pairs`/`proxy_get_header_map_value`).
43pub const BUFFER_CALLOUT_RESPONSE_HEADERS: u32 = 6;
44/// Map type: HTTP callout response trailers (proxy-wasm MapType 7).
45/// Not plumbed (the sync callout transport never surfaces trailers);
46/// reads answer empty and writes are accepted and discarded, like the
47/// request/response trailer maps.
48pub const BUFFER_CALLOUT_RESPONSE_TRAILERS: u32 = 7;
49/// Buffer type: HTTP callout response body (`proxy_get_buffer_bytes`;
50/// proxy-wasm BufferType 4, what the SDK's `get_http_call_response_body`
51/// reads).
52pub const BUFFER_CALLOUT_RESPONSE_BODY: u32 = 4;
53/// Buffer type: VM configuration (passed to `proxy_on_vm_start`).
54pub const BUFFER_VM_CONFIGURATION: u32 = 6;
55/// Buffer type: Plugin configuration (passed to `proxy_on_configure`).
56pub const BUFFER_PLUGIN_CONFIGURATION: u32 = 7;
57
58// --- Log levels (the `level` parameter in `proxy_log`) -----------------
59
60pub const LOG_TRACE: u32 = 0;
61pub const LOG_DEBUG: u32 = 1;
62pub const LOG_INFO: u32 = 2;
63pub const LOG_WARN: u32 = 3;
64pub const LOG_ERROR: u32 = 4;
65pub const LOG_CRITICAL: u32 = 5;
66
67// --- Action return values (the return type of phase exports) -----------
68//
69// The plugin returns an action from each phase callback. For HTTP
70// filters, Continue and EndStream are the phase outcomes; Pause is
71// what a plugin returns after `proxy_http_call` registered a callout
72// (DW-167) — the host performs the callout, delivers
73// `proxy_on_http_call_response`, and the phase resumes from the
74// plugin's post-callback state.
75
76/// Continue processing — the request/response proceeds normally.
77pub const ACTION_CONTINUE: u32 = 0;
78/// Pause for a host async operation — the plugin dispatched an HTTP
79/// callout (`proxy_http_call`) and awaits its response callback
80/// (proxy-wasm Action value 1; the Rust SDK's `Action::Pause`).
81pub const ACTION_PAUSE: u32 = 1;
82/// End the stream — short-circuit the request (e.g. after
83/// `proxy_send_http_response`). No further phase callbacks fire.
84pub const ACTION_END_STREAM: u32 = 2;
85
86// --- Close types (the `close_type` parameter in close callbacks) -------
87
88pub const CLOSE_UNKNOWN: u32 = 0;
89pub const CLOSE_LOCAL: u32 = 1;
90pub const CLOSE_REMOTE: u32 = 2;
91
92// --- Header map serialization formats -----------------------------------
93//
94// There are two wire layouts, keyed by import spelling (the additive
95// rule: the spec-named hostcalls speak the SDK layout, dwara's legacy
96// spellings keep the original layout they shipped with):
97//
98// 1. Spec/SDK layout — `proxy_get_header_map_pairs`,
99//    `proxy_set_header_map_pairs`, and `proxy_send_local_response`.
100//    This is the layout the Rust proxy-wasm SDK produces and parses
101//    (proxy-wasm 0.2.5, src/hostcalls.rs `utils::serialize_map` /
102//    `utils::deserialize_map`, ~lines 1265-1327): a little-endian u32
103//    entry count, then a length table of little-endian u32 pairs
104//    (key_len, value_len — one pair per entry), then the strings:
105//    each key and each value NUL-terminated. An empty buffer
106//    deserializes to an empty map; an empty map serializes to the
107//    4-byte LE count 0.
108//
109// 2. dwara legacy layout — `proxy_send_http_response` only. A
110//    sequence of (u32 key_len BE, key bytes, u32 val_len BE, val
111//    bytes) tuples with no count and no NULs.
112
113/// Serialize a list of (key, value) header pairs into the spec/SDK
114/// proxy-wasm wire layout (mirrors the Rust proxy-wasm SDK's
115/// `utils::serialize_map`, hostcalls.rs ~line 1265): LE u32 entry
116/// count, then the LE u32 (key_len, value_len) length table, then the
117/// NUL-terminated strings. An empty map serializes to exactly the
118/// 4-byte LE count 0 — the bytes the SDK passes for
119/// `send_http_response` with empty headers.
120pub fn serialize_header_map_spec(headers: &[(String, String)]) -> Vec<u8> {
121    let mut size: usize = 4;
122    for (key, value) in headers {
123        size += key.len() + value.len() + 10;
124    }
125    let mut buf = Vec::with_capacity(size);
126    buf.extend_from_slice(&(headers.len() as u32).to_le_bytes());
127    for (key, value) in headers {
128        buf.extend_from_slice(&(key.len() as u32).to_le_bytes());
129        buf.extend_from_slice(&(value.len() as u32).to_le_bytes());
130    }
131    for (key, value) in headers {
132        buf.extend_from_slice(key.as_bytes());
133        buf.push(0);
134        buf.extend_from_slice(value.as_bytes());
135        buf.push(0);
136    }
137    buf
138}
139
140/// Deserialize a spec/SDK proxy-wasm wire-layout header map (the
141/// format `serialize_header_map_spec` produces and the Rust proxy-wasm
142/// SDK's `utils::deserialize_map`, hostcalls.rs ~line 1305, parses).
143/// An empty buffer is an empty map (the SDK's convention); any
144/// truncated or corrupt buffer returns `None` (the SDK would panic on
145/// its `unwrap`s — the host fails the hostcall instead).
146pub fn deserialize_header_map_spec(buf: &[u8]) -> Option<Vec<(String, String)>> {
147    if buf.is_empty() {
148        return Some(Vec::new());
149    }
150    if buf.len() < 4 {
151        return None;
152    }
153    let count = u32::from_le_bytes(buf[0..4].try_into().expect("4 bytes")) as usize;
154    let table_end = 4usize.checked_add(count.checked_mul(8)?)?;
155    if buf.len() < table_end {
156        return None;
157    }
158    let mut result = Vec::with_capacity(count);
159    let mut pos = table_end;
160    for n in 0..count {
161        let entry = 4 + n * 8;
162        let key_len =
163            u32::from_le_bytes(buf[entry..entry + 4].try_into().expect("4 bytes")) as usize;
164        let val_len =
165            u32::from_le_bytes(buf[entry + 4..entry + 8].try_into().expect("4 bytes")) as usize;
166        let key = take_nul_terminated(buf, &mut pos, key_len)?;
167        let value = take_nul_terminated(buf, &mut pos, val_len)?;
168        result.push((key, value));
169    }
170    Some(result)
171}
172
173/// Read `len` bytes at `*pos` followed by a NUL terminator (the SDK's
174/// string-data layout), advancing `*pos` past both. Returns `None` on
175/// a missing terminator, a truncated string, or non-UTF-8 bytes.
176fn take_nul_terminated(buf: &[u8], pos: &mut usize, len: usize) -> Option<String> {
177    let start = *pos;
178    let end = start.checked_add(len)?;
179    if end >= buf.len() || buf[end] != 0 {
180        return None;
181    }
182    let s = String::from_utf8(buf[start..end].to_vec()).ok()?;
183    *pos = end + 1;
184    Some(s)
185}
186
187/// Serialize a list of (key, value) header pairs into dwara's LEGACY
188/// wire layout (dwara's own `proxy_send_http_response` spelling only):
189/// a sequence of (u32 key_len, key bytes, u32 val_len, val bytes)
190/// tuples, big-endian. The spec-named hostcalls use
191/// [`serialize_header_map_spec`] instead.
192pub fn serialize_header_map(headers: &[(String, String)]) -> Vec<u8> {
193    let mut buf = Vec::with_capacity(headers.len() * 32);
194    for (key, value) in headers {
195        buf.extend_from_slice(&(key.len() as u32).to_be_bytes());
196        buf.extend_from_slice(key.as_bytes());
197        buf.extend_from_slice(&(value.len() as u32).to_be_bytes());
198        buf.extend_from_slice(value.as_bytes());
199    }
200    buf
201}
202
203/// Deserialize a LEGACY-layout header map (dwara's
204/// `proxy_send_http_response` spelling only) back into pairs. Returns
205/// `None` on a truncated/corrupt buffer. The spec-named hostcalls use
206/// [`deserialize_header_map_spec`] instead.
207pub fn deserialize_header_map(buf: &[u8]) -> Option<Vec<(String, String)>> {
208    let mut result = Vec::new();
209    let mut pos = 0;
210    while pos < buf.len() {
211        if pos + 4 > buf.len() {
212            return None;
213        }
214        let key_len = u32::from_be_bytes(buf[pos..pos + 4].try_into().ok()?) as usize;
215        pos += 4;
216        if pos + key_len > buf.len() {
217            return None;
218        }
219        let key = String::from_utf8(buf[pos..pos + key_len].to_vec()).ok()?;
220        pos += key_len;
221
222        if pos + 4 > buf.len() {
223            return None;
224        }
225        let val_len = u32::from_be_bytes(buf[pos..pos + 4].try_into().ok()?) as usize;
226        pos += 4;
227        if pos + val_len > buf.len() {
228            return None;
229        }
230        let value = String::from_utf8(buf[pos..pos + val_len].to_vec()).ok()?;
231        pos += val_len;
232
233        result.push((key, value));
234    }
235    Some(result)
236}