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}