1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
//! Body access control for middleware chains (SPEC-008).
//!
//! Bodies travel via side-channel host functions (not in JSON). This module
//! manages which middleware instances receive the body and collects any
//! modifications they make.
//!
//! Non-body-access middleware: body is not set on the instance (it sees `None`).
//! Body-access middleware: body is injected via `set_request_body()` before the
//! call and collected via `take_output_body()` after.
use crate::instance::PluginInstance;
/// Holds the split state: body-less JSON metadata and the raw body bytes.
///
/// Created once before the middleware chain runs. The body is only injected
/// (via side-channel) for middlewares that declare `body_access = true`.
pub struct BodyAccessControl {
/// Request/response JSON (body field is absent due to `#[serde(skip)]`).
metadata_json: Vec<u8>,
/// The held-aside raw body bytes.
held_body: Option<Vec<u8>>,
}
impl BodyAccessControl {
/// Create a new body access controller.
///
/// `metadata_json` is the serialized Request/Response (body is `#[serde(skip)]`
/// so it's already absent from JSON). `body` is the raw body bytes extracted
/// from the original struct before serialization.
pub fn new(metadata_json: Vec<u8>, body: Option<Vec<u8>>) -> Self {
Self {
metadata_json,
held_body: body,
}
}
/// Prepare an instance for a middleware call.
///
/// If `body_access` is true, the held body is set on the instance via
/// side-channel. Otherwise, no body is set (plugin sees `None`).
///
/// Returns a clone of the metadata JSON to pass to the WASM handler.
pub fn prepare_instance(&self, instance: &mut PluginInstance, body_access: bool) -> Vec<u8> {
if body_access {
instance.set_request_body(self.held_body.clone());
} else {
instance.set_request_body(None);
}
self.metadata_json.clone()
}
/// Collect results after a middleware call.
///
/// `output` is the metadata JSON returned by the plugin (via `take_output()`).
/// If `body_access` is true, the output body is taken from the instance's
/// side-channel and updates the held body.
pub fn collect_after(
&mut self,
instance: &mut PluginInstance,
output: Vec<u8>,
body_access: bool,
) {
if !output.is_empty() {
self.metadata_json = output;
}
if body_access {
// Plugin called host_body_set or host_body_clear → update held body.
// If plugin didn't call either (None), body is unchanged.
if let Some(new_body) = instance.take_output_body() {
self.held_body = new_body;
}
}
}
/// Get the held body (for passing to the dispatcher via side-channel).
pub fn body(&self) -> &Option<Vec<u8>> {
&self.held_body
}
/// Consume self and return the metadata JSON and body separately.
pub fn finalize(self) -> (Vec<u8>, Option<Vec<u8>>) {
(self.metadata_json, self.held_body)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn make_metadata(headers: serde_json::Value) -> Vec<u8> {
serde_json::to_vec(&json!({
"method": "POST",
"path": "/upload",
"query": null,
"headers": headers,
"client_ip": "127.0.0.1",
"path_params": {}
}))
.expect("serialize")
}
fn parse_header(json_bytes: &[u8], key: &str) -> Option<String> {
let v: serde_json::Value = serde_json::from_slice(json_bytes).expect("parse JSON");
v["headers"][key].as_str().map(|s| s.to_string())
}
// ── Construction ────────────────────────────────────────────────
#[test]
fn new_with_body() {
let meta = make_metadata(json!({}));
let ctrl = BodyAccessControl::new(meta, Some(b"hello".to_vec()));
assert_eq!(ctrl.held_body, Some(b"hello".to_vec()));
}
#[test]
fn new_without_body() {
let meta = make_metadata(json!({}));
let ctrl = BodyAccessControl::new(meta, None);
assert_eq!(ctrl.held_body, None);
}
// ── Finalize ────────────────────────────────────────────────────
#[test]
fn finalize_returns_metadata_and_body() {
let meta = make_metadata(json!({"content-type": "text/plain"}));
let body = Some(b"the body".to_vec());
let ctrl = BodyAccessControl::new(meta.clone(), body.clone());
let (final_meta, final_body) = ctrl.finalize();
assert_eq!(final_meta, meta);
assert_eq!(final_body, body);
}
#[test]
fn finalize_none_body() {
let meta = make_metadata(json!({}));
let ctrl = BodyAccessControl::new(meta, None);
let (_final_meta, final_body) = ctrl.finalize();
assert_eq!(final_body, None);
}
// ── collect_after without instance (unit-level) ─────────────────
// Full integration tests with real WASM instances live in workload tests.
// Here we test the metadata update logic.
#[test]
fn collect_updates_metadata_from_non_empty_output() {
let meta = make_metadata(json!({}));
let new_meta = make_metadata(json!({"x-added": "value"}));
let mut ctrl = BodyAccessControl::new(meta, Some(b"body".to_vec()));
// Simulate: non-body middleware returned new metadata
// (We can't call collect_after without an instance, so test metadata update directly)
ctrl.metadata_json = new_meta.clone();
assert_eq!(
parse_header(&ctrl.metadata_json, "x-added"),
Some("value".to_string())
);
// Body is unchanged
assert_eq!(ctrl.held_body, Some(b"body".to_vec()));
}
#[test]
fn body_accessor_returns_held_body() {
let meta = make_metadata(json!({}));
let ctrl = BodyAccessControl::new(meta, Some(b"raw bytes".to_vec()));
assert_eq!(ctrl.body(), &Some(b"raw bytes".to_vec()));
}
}