Skip to main content

barbacane_wasm/
body_access.rs

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