Skip to main content

holger_plugin_abi/
wire.rs

1//! The values that cross the boundary, and their encoders.
2//!
3//! These deliberately mirror `holger_traits::{ArtifactId, ArtifactEntry}` rather
4//! than reusing them: `holger-traits` depends on `znippy-common`, which carries
5//! arrow, io_uring and the gatling engine, so it cannot be compiled for
6//! `wasm32-unknown-unknown`. The mapping between the two lives in exactly one
7//! place — `holger_plugin_host::convert` — so a guest and a linked backend are
8//! projected into the router through the same code.
9
10use crate::codec::{read_result, write_result, DecodeError, Reader, Writer};
11
12/// Bumped when the layout of anything in this module changes. The host refuses a
13/// module that reports a different number: a silently mismatched ABI decodes
14/// garbage into real fields, which is worse than not loading at all.
15pub const ABI_VERSION: u32 = 1;
16
17/// A package coordinate. Mirror of `holger_traits::ArtifactId`.
18#[derive(Debug, Clone, PartialEq, Eq, Default)]
19pub struct WireArtifactId {
20    pub namespace: Option<String>,
21    pub name: String,
22    pub version: String,
23}
24
25impl WireArtifactId {
26    pub fn encode(&self, w: &mut Writer) {
27        w.opt_str(self.namespace.as_deref()).str(&self.name).str(&self.version);
28    }
29
30    pub fn decode(r: &mut Reader<'_>) -> Result<Self, DecodeError> {
31        Ok(Self {
32            namespace: r.opt_str("ArtifactId.namespace")?,
33            name: r.str("ArtifactId.name")?,
34            version: r.str("ArtifactId.version")?,
35        })
36    }
37
38    pub fn to_bytes(&self) -> Vec<u8> {
39        let mut w = Writer::new();
40        self.encode(&mut w);
41        w.finish()
42    }
43
44    pub fn from_bytes(b: &[u8]) -> Result<Self, DecodeError> {
45        let mut r = Reader::new(b);
46        let v = Self::decode(&mut r)?;
47        r.expect_end("ArtifactId")?;
48        Ok(v)
49    }
50}
51
52/// One listed artifact. Mirror of `holger_traits::ArtifactEntry`.
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct WireArtifactEntry {
55    pub id: WireArtifactId,
56    pub size_bytes: i64,
57    pub content_type: String,
58}
59
60impl WireArtifactEntry {
61    pub fn encode(&self, w: &mut Writer) {
62        self.id.encode(w);
63        w.i64(self.size_bytes).str(&self.content_type);
64    }
65
66    pub fn decode(r: &mut Reader<'_>) -> Result<Self, DecodeError> {
67        Ok(Self {
68            id: WireArtifactId::decode(r)?,
69            size_bytes: r.i64("ArtifactEntry.size_bytes")?,
70            content_type: r.str("ArtifactEntry.content_type")?,
71        })
72    }
73}
74
75/// What a module says about itself at load time, read once from
76/// `plugin_manifest`. This is why registration can read a *directory* rather
77/// than a config file: the module names itself.
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct WireManifest {
80    /// Must equal [`ABI_VERSION`] or the host refuses the module.
81    pub abi_version: u32,
82    /// The handler's own name — `skidbladnir`, `rust-toolchain`, …
83    pub handler: String,
84    /// The `holger_traits::ArtifactFormat` this handler reports, as its
85    /// serde-lowercase spelling (`znippy`, `maven3`, …). The host parses it and
86    /// **refuses an unknown one by name** rather than falling back to `raw`.
87    pub format: String,
88    /// Whether `put` is expected to succeed.
89    pub writable: bool,
90}
91
92impl WireManifest {
93    pub fn encode(&self, w: &mut Writer) {
94        w.u32(self.abi_version).str(&self.handler).str(&self.format).bool(self.writable);
95    }
96
97    pub fn decode(r: &mut Reader<'_>) -> Result<Self, DecodeError> {
98        Ok(Self {
99            abi_version: r.u32("Manifest.abi_version")?,
100            handler: r.str("Manifest.handler")?,
101            format: r.str("Manifest.format")?,
102            writable: r.bool("Manifest.writable")?,
103        })
104    }
105
106    pub fn to_bytes(&self) -> Vec<u8> {
107        let mut w = Writer::new();
108        self.encode(&mut w);
109        w.finish()
110    }
111
112    pub fn from_bytes(b: &[u8]) -> Result<Self, DecodeError> {
113        let mut r = Reader::new(b);
114        let v = Self::decode(&mut r)?;
115        r.expect_end("Manifest")?;
116        Ok(v)
117    }
118}
119
120/// `(status, headers, body)` — the established HTTP-door contract across
121/// holger's backends, carried as one value.
122#[derive(Debug, Clone, PartialEq, Eq)]
123pub struct WireHttpResponse {
124    pub status: u16,
125    pub headers: Vec<(String, String)>,
126    pub body: Vec<u8>,
127}
128
129impl WireHttpResponse {
130    pub fn encode(&self, w: &mut Writer) {
131        w.u16(self.status).u32(self.headers.len() as u32);
132        for (k, v) in &self.headers {
133            w.str(k).str(v);
134        }
135        w.bytes(&self.body);
136    }
137
138    pub fn decode(r: &mut Reader<'_>) -> Result<Self, DecodeError> {
139        let status = r.u16("HttpResponse.status")?;
140        // Each header costs at least 8 bytes on the wire, so a count is checked
141        // against the bytes that remain before anything is allocated.
142        let n = r.count("HttpResponse.headers")?;
143        let mut headers = Vec::with_capacity(n.min(1024));
144        for _ in 0..n {
145            let k = r.str("HttpResponse.header.name")?;
146            let v = r.str("HttpResponse.header.value")?;
147            headers.push((k, v));
148        }
149        Ok(Self { status, headers, body: r.bytes("HttpResponse.body")? })
150    }
151}
152
153/// The request half of the HTTP door.
154#[derive(Debug, Clone, PartialEq, Eq)]
155pub struct WireHttpRequest {
156    pub method: String,
157    pub suburl: String,
158    pub body: Vec<u8>,
159}
160
161impl WireHttpRequest {
162    pub fn to_bytes(&self) -> Vec<u8> {
163        let mut w = Writer::new();
164        w.str(&self.method).str(&self.suburl).bytes(&self.body);
165        w.finish()
166    }
167
168    pub fn from_bytes(b: &[u8]) -> Result<Self, DecodeError> {
169        let mut r = Reader::new(b);
170        let v = Self {
171            method: r.str("HttpRequest.method")?,
172            suburl: r.str("HttpRequest.suburl")?,
173            body: r.bytes("HttpRequest.body")?,
174        };
175        r.expect_end("HttpRequest")?;
176        Ok(v)
177    }
178}
179
180/// Arguments to `plugin_put`.
181pub struct WirePutRequest {
182    pub id: WireArtifactId,
183    pub data: Vec<u8>,
184}
185
186impl WirePutRequest {
187    pub fn to_bytes(&self) -> Vec<u8> {
188        let mut w = Writer::new();
189        self.id.encode(&mut w);
190        w.bytes(&self.data);
191        w.finish()
192    }
193
194    pub fn from_bytes(b: &[u8]) -> Result<Self, DecodeError> {
195        let mut r = Reader::new(b);
196        let v = Self { id: WireArtifactId::decode(&mut r)?, data: r.bytes("PutRequest.data")? };
197        r.expect_end("PutRequest")?;
198        Ok(v)
199    }
200}
201
202/// Arguments to `plugin_list`.
203pub struct WireListRequest {
204    pub name_filter: Option<String>,
205    pub limit: u32,
206}
207
208impl WireListRequest {
209    pub fn to_bytes(&self) -> Vec<u8> {
210        let mut w = Writer::new();
211        w.opt_str(self.name_filter.as_deref()).u32(self.limit);
212        w.finish()
213    }
214
215    pub fn from_bytes(b: &[u8]) -> Result<Self, DecodeError> {
216        let mut r = Reader::new(b);
217        let v = Self {
218            name_filter: r.opt_str("ListRequest.name_filter")?,
219            limit: r.u32("ListRequest.limit")?,
220        };
221        r.expect_end("ListRequest")?;
222        Ok(v)
223    }
224}
225
226// ─── Response encoders ───────────────────────────────────────────────
227//
228// One function per response shape, called by BOTH the guest (to write) and the
229// host (to read). A verb's two halves cannot drift apart because there is only
230// one of each.
231
232pub fn encode_fetch_response(v: &Result<Option<Vec<u8>>, String>) -> Vec<u8> {
233    let mut w = Writer::new();
234    write_result(&mut w, v, |w, body| {
235        w.opt_bytes(body.as_deref());
236    });
237    w.finish()
238}
239
240pub fn decode_fetch_response(b: &[u8]) -> Result<Result<Option<Vec<u8>>, String>, DecodeError> {
241    let mut r = Reader::new(b);
242    let v = read_result(&mut r, "fetch", |r| r.opt_bytes("fetch.body"))?;
243    r.expect_end("fetch")?;
244    Ok(v)
245}
246
247pub fn encode_unit_response(v: &Result<(), String>) -> Vec<u8> {
248    let mut w = Writer::new();
249    write_result(&mut w, v, |_, _| {});
250    w.finish()
251}
252
253pub fn decode_unit_response(b: &[u8]) -> Result<Result<(), String>, DecodeError> {
254    let mut r = Reader::new(b);
255    let v = read_result(&mut r, "unit", |_| Ok(()))?;
256    r.expect_end("unit")?;
257    Ok(v)
258}
259
260pub fn encode_list_response(v: &Result<Vec<WireArtifactEntry>, String>) -> Vec<u8> {
261    let mut w = Writer::new();
262    write_result(&mut w, v, |w, entries| {
263        w.u32(entries.len() as u32);
264        for e in entries {
265            e.encode(w);
266        }
267    });
268    w.finish()
269}
270
271pub fn decode_list_response(b: &[u8]) -> Result<Result<Vec<WireArtifactEntry>, String>, DecodeError>
272{
273    let mut r = Reader::new(b);
274    let v = read_result(&mut r, "list", |r| {
275        let n = r.count("list.count")?;
276        let mut out = Vec::with_capacity(n.min(4096));
277        for _ in 0..n {
278            out.push(WireArtifactEntry::decode(r)?);
279        }
280        Ok(out)
281    })?;
282    r.expect_end("list")?;
283    Ok(v)
284}
285
286pub fn encode_coordinate_response(v: &Option<WireArtifactId>) -> Vec<u8> {
287    let mut w = Writer::new();
288    match v {
289        None => {
290            w.u8(0);
291        }
292        Some(id) => {
293            w.u8(1);
294            id.encode(&mut w);
295        }
296    }
297    w.finish()
298}
299
300pub fn decode_coordinate_response(b: &[u8]) -> Result<Option<WireArtifactId>, DecodeError> {
301    let mut r = Reader::new(b);
302    let v = match r.u8("coordinate.tag")? {
303        0 => None,
304        1 => Some(WireArtifactId::decode(&mut r)?),
305        tag => return Err(DecodeError::BadTag { field: "coordinate.tag", tag }),
306    };
307    r.expect_end("coordinate")?;
308    Ok(v)
309}
310
311pub fn encode_http_response(v: &Result<WireHttpResponse, String>) -> Vec<u8> {
312    let mut w = Writer::new();
313    write_result(&mut w, v, |w, resp| resp.encode(w));
314    w.finish()
315}
316
317pub fn decode_http_response(b: &[u8]) -> Result<Result<WireHttpResponse, String>, DecodeError> {
318    let mut r = Reader::new(b);
319    let v = read_result(&mut r, "http", WireHttpResponse::decode)?;
320    r.expect_end("http")?;
321    Ok(v)
322}
323
324/// A `(key, size)` listing row from [`crate::BlobStore::list`].
325pub fn encode_store_listing(rows: &[(String, u64)]) -> Vec<u8> {
326    let mut w = Writer::new();
327    w.u32(rows.len() as u32);
328    for (k, n) in rows {
329        w.str(k).u64(*n);
330    }
331    w.finish()
332}
333
334pub fn decode_store_listing(b: &[u8]) -> Result<Vec<(String, u64)>, DecodeError> {
335    let mut r = Reader::new(b);
336    let n = r.count("store_listing.count")?;
337    let mut out = Vec::with_capacity(n.min(4096));
338    for _ in 0..n {
339        let k = r.str("store_listing.key")?;
340        out.push((k, r.u64("store_listing.size")?));
341    }
342    r.expect_end("store_listing")?;
343    Ok(out)
344}
345
346pub fn encode_store_get(v: &Option<Vec<u8>>) -> Vec<u8> {
347    let mut w = Writer::new();
348    w.opt_bytes(v.as_deref());
349    w.finish()
350}
351
352pub fn decode_store_get(b: &[u8]) -> Result<Option<Vec<u8>>, DecodeError> {
353    let mut r = Reader::new(b);
354    let v = r.opt_bytes("store_get.body")?;
355    r.expect_end("store_get")?;
356    Ok(v)
357}