plugin/abi/src/wire.rs
//! The values that cross the boundary, and their encoders.
//!
//! These deliberately mirror `holger_traits::{ArtifactId, ArtifactEntry}` rather
//! than reusing them: `holger-traits` depends on `znippy-common`, which carries
//! arrow, io_uring and the gatling engine, so it cannot be compiled for
//! `wasm32-unknown-unknown`. The mapping between the two lives in exactly one
//! place — `holger_plugin_host::convert` — so a guest and a linked backend are
//! projected into the router through the same code.
use crate::codec::{read_result, write_result, DecodeError, Reader, Writer};
/// Bumped when the layout of anything in this module changes. The host refuses a
/// module that reports a different number: a silently mismatched ABI decodes
/// garbage into real fields, which is worse than not loading at all.
pub const ABI_VERSION: u32 = 1;
/// A package coordinate. Mirror of `holger_traits::ArtifactId`.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct WireArtifactId {
pub namespace: Option<String>,
pub name: String,
pub version: String,
}
impl WireArtifactId {
pub fn encode(&self, w: &mut Writer) {
w.opt_str(self.namespace.as_deref())
.str(&self.name)
.str(&self.version);
}
pub fn decode(r: &mut Reader<'_>) -> Result<Self, DecodeError> {
Ok(Self {
namespace: r.opt_str("ArtifactId.namespace")?,
name: r.str("ArtifactId.name")?,
version: r.str("ArtifactId.version")?,
})
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut w = Writer::new();
self.encode(&mut w);
w.finish()
}
pub fn from_bytes(b: &[u8]) -> Result<Self, DecodeError> {
let mut r = Reader::new(b);
let v = Self::decode(&mut r)?;
r.expect_end("ArtifactId")?;
Ok(v)
}
}
/// One listed artifact. Mirror of `holger_traits::ArtifactEntry`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WireArtifactEntry {
pub id: WireArtifactId,
pub size_bytes: i64,
pub content_type: String,
}
impl WireArtifactEntry {
pub fn encode(&self, w: &mut Writer) {
self.id.encode(w);
w.i64(self.size_bytes).str(&self.content_type);
}
pub fn decode(r: &mut Reader<'_>) -> Result<Self, DecodeError> {
Ok(Self {
id: WireArtifactId::decode(r)?,
size_bytes: r.i64("ArtifactEntry.size_bytes")?,
content_type: r.str("ArtifactEntry.content_type")?,
})
}
}
/// **Who a module is, for the anti-rollback floor** (W14).
///
/// `(supplier, product)` is the floor's LANE — the same key the update-bundle
/// floor uses, so a plugin update can never read as a firmware rollback — and
/// `version` is what is compared against it. The three are inside the manifest,
/// which is inside the module bytes, which is what the detached CMS signature
/// covers: an identity cannot be edited without breaking the signature.
///
/// `version` is `MAJOR.MINOR.PATCH` in decimal. The host parses it and refuses
/// by name what it cannot order — a version it cannot compare is not a version
/// the floor can hold.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ModuleIdentity {
pub supplier: String,
pub product: String,
pub version: String,
}
impl ModuleIdentity {
pub fn new(
supplier: impl Into<String>,
product: impl Into<String>,
version: impl Into<String>,
) -> Self {
Self {
supplier: supplier.into(),
product: product.into(),
version: version.into(),
}
}
pub fn encode(&self, w: &mut Writer) {
w.str(&self.supplier).str(&self.product).str(&self.version);
}
pub fn decode(r: &mut Reader<'_>) -> Result<Self, DecodeError> {
Ok(Self {
supplier: r.str("ModuleIdentity.supplier")?,
product: r.str("ModuleIdentity.product")?,
version: r.str("ModuleIdentity.version")?,
})
}
}
/// What a module says about itself at load time, read once from
/// `plugin_manifest`. This is why registration can read a *directory* rather
/// than a config file: the module names itself.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WireManifest {
/// Must equal [`ABI_VERSION`] or the host refuses the module.
pub abi_version: u32,
/// The handler's own name — `skidbladnir`, `rust-toolchain`, …
pub handler: String,
/// The `holger_traits::ArtifactFormat` this handler reports, as its
/// serde-lowercase spelling (`znippy`, `maven3`, …). The host parses it and
/// **refuses an unknown one by name** rather than falling back to `raw`.
pub format: String,
/// Whether `put` is expected to succeed.
pub writable: bool,
/// `(supplier, product, version)` for the anti-rollback floor. A TRAILING
/// block, written only when present and read only when bytes remain, so a
/// module built before the floor existed still decodes — as `None`, which
/// the host names when a floor is kept (`holger-plugin-12`), never as
/// "no identity, no floor, fine".
pub identity: Option<ModuleIdentity>,
}
impl WireManifest {
pub fn encode(&self, w: &mut Writer) {
w.u32(self.abi_version)
.str(&self.handler)
.str(&self.format)
.bool(self.writable);
if let Some(id) = &self.identity {
id.encode(w);
}
}
pub fn decode(r: &mut Reader<'_>) -> Result<Self, DecodeError> {
let abi_version = r.u32("Manifest.abi_version")?;
let handler = r.str("Manifest.handler")?;
let format = r.str("Manifest.format")?;
let writable = r.bool("Manifest.writable")?;
let identity = if r.remaining() > 0 {
Some(ModuleIdentity::decode(r)?)
} else {
None
};
Ok(Self {
abi_version,
handler,
format,
writable,
identity,
})
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut w = Writer::new();
self.encode(&mut w);
w.finish()
}
pub fn from_bytes(b: &[u8]) -> Result<Self, DecodeError> {
let mut r = Reader::new(b);
let v = Self::decode(&mut r)?;
r.expect_end("Manifest")?;
Ok(v)
}
}
/// `(status, headers, body)` — the established HTTP-door contract across
/// holger's backends, carried as one value.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WireHttpResponse {
pub status: u16,
pub headers: Vec<(String, String)>,
pub body: Vec<u8>,
}
impl WireHttpResponse {
pub fn encode(&self, w: &mut Writer) {
w.u16(self.status).u32(self.headers.len() as u32);
for (k, v) in &self.headers {
w.str(k).str(v);
}
w.bytes(&self.body);
}
pub fn decode(r: &mut Reader<'_>) -> Result<Self, DecodeError> {
let status = r.u16("HttpResponse.status")?;
// Each header costs at least 8 bytes on the wire, so a count is checked
// against the bytes that remain before anything is allocated.
let n = r.count("HttpResponse.headers")?;
let mut headers = Vec::with_capacity(n.min(1024));
for _ in 0..n {
let k = r.str("HttpResponse.header.name")?;
let v = r.str("HttpResponse.header.value")?;
headers.push((k, v));
}
Ok(Self {
status,
headers,
body: r.bytes("HttpResponse.body")?,
})
}
}
/// The request half of the HTTP door.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WireHttpRequest {
pub method: String,
pub suburl: String,
pub body: Vec<u8>,
}
impl WireHttpRequest {
pub fn to_bytes(&self) -> Vec<u8> {
let mut w = Writer::new();
w.str(&self.method).str(&self.suburl).bytes(&self.body);
w.finish()
}
pub fn from_bytes(b: &[u8]) -> Result<Self, DecodeError> {
let mut r = Reader::new(b);
let v = Self {
method: r.str("HttpRequest.method")?,
suburl: r.str("HttpRequest.suburl")?,
body: r.bytes("HttpRequest.body")?,
};
r.expect_end("HttpRequest")?;
Ok(v)
}
}
/// Arguments to `plugin_put`.
pub struct WirePutRequest {
pub id: WireArtifactId,
pub data: Vec<u8>,
}
impl WirePutRequest {
pub fn to_bytes(&self) -> Vec<u8> {
let mut w = Writer::new();
self.id.encode(&mut w);
w.bytes(&self.data);
w.finish()
}
pub fn from_bytes(b: &[u8]) -> Result<Self, DecodeError> {
let mut r = Reader::new(b);
let v = Self {
id: WireArtifactId::decode(&mut r)?,
data: r.bytes("PutRequest.data")?,
};
r.expect_end("PutRequest")?;
Ok(v)
}
}
/// Arguments to `plugin_list`.
pub struct WireListRequest {
pub name_filter: Option<String>,
pub limit: u32,
}
impl WireListRequest {
pub fn to_bytes(&self) -> Vec<u8> {
let mut w = Writer::new();
w.opt_str(self.name_filter.as_deref()).u32(self.limit);
w.finish()
}
pub fn from_bytes(b: &[u8]) -> Result<Self, DecodeError> {
let mut r = Reader::new(b);
let v = Self {
name_filter: r.opt_str("ListRequest.name_filter")?,
limit: r.u32("ListRequest.limit")?,
};
r.expect_end("ListRequest")?;
Ok(v)
}
}
// ─── Response encoders ───────────────────────────────────────────────
//
// One function per response shape, called by BOTH the guest (to write) and the
// host (to read). A verb's two halves cannot drift apart because there is only
// one of each.
pub fn encode_fetch_response(v: &Result<Option<Vec<u8>>, String>) -> Vec<u8> {
let mut w = Writer::new();
write_result(&mut w, v, |w, body| {
w.opt_bytes(body.as_deref());
});
w.finish()
}
pub fn decode_fetch_response(b: &[u8]) -> Result<Result<Option<Vec<u8>>, String>, DecodeError> {
let mut r = Reader::new(b);
let v = read_result(&mut r, "fetch", |r| r.opt_bytes("fetch.body"))?;
r.expect_end("fetch")?;
Ok(v)
}
pub fn encode_unit_response(v: &Result<(), String>) -> Vec<u8> {
let mut w = Writer::new();
write_result(&mut w, v, |_, _| {});
w.finish()
}
pub fn decode_unit_response(b: &[u8]) -> Result<Result<(), String>, DecodeError> {
let mut r = Reader::new(b);
let v = read_result(&mut r, "unit", |_| Ok(()))?;
r.expect_end("unit")?;
Ok(v)
}
pub fn encode_list_response(v: &Result<Vec<WireArtifactEntry>, String>) -> Vec<u8> {
let mut w = Writer::new();
write_result(&mut w, v, |w, entries| {
w.u32(entries.len() as u32);
for e in entries {
e.encode(w);
}
});
w.finish()
}
pub fn decode_list_response(
b: &[u8],
) -> Result<Result<Vec<WireArtifactEntry>, String>, DecodeError> {
let mut r = Reader::new(b);
let v = read_result(&mut r, "list", |r| {
let n = r.count("list.count")?;
let mut out = Vec::with_capacity(n.min(4096));
for _ in 0..n {
out.push(WireArtifactEntry::decode(r)?);
}
Ok(out)
})?;
r.expect_end("list")?;
Ok(v)
}
pub fn encode_coordinate_response(v: &Option<WireArtifactId>) -> Vec<u8> {
let mut w = Writer::new();
match v {
None => {
w.u8(0);
}
Some(id) => {
w.u8(1);
id.encode(&mut w);
}
}
w.finish()
}
pub fn decode_coordinate_response(b: &[u8]) -> Result<Option<WireArtifactId>, DecodeError> {
let mut r = Reader::new(b);
let v = match r.u8("coordinate.tag")? {
0 => None,
1 => Some(WireArtifactId::decode(&mut r)?),
tag => {
return Err(DecodeError::BadTag {
field: "coordinate.tag",
tag,
})
}
};
r.expect_end("coordinate")?;
Ok(v)
}
pub fn encode_http_response(v: &Result<WireHttpResponse, String>) -> Vec<u8> {
let mut w = Writer::new();
write_result(&mut w, v, |w, resp| resp.encode(w));
w.finish()
}
pub fn decode_http_response(b: &[u8]) -> Result<Result<WireHttpResponse, String>, DecodeError> {
let mut r = Reader::new(b);
let v = read_result(&mut r, "http", WireHttpResponse::decode)?;
r.expect_end("http")?;
Ok(v)
}
/// A `(key, size)` listing row from [`crate::BlobStore::list`].
pub fn encode_store_listing(rows: &[(String, u64)]) -> Vec<u8> {
let mut w = Writer::new();
w.u32(rows.len() as u32);
for (k, n) in rows {
w.str(k).u64(*n);
}
w.finish()
}
pub fn decode_store_listing(b: &[u8]) -> Result<Vec<(String, u64)>, DecodeError> {
let mut r = Reader::new(b);
let n = r.count("store_listing.count")?;
let mut out = Vec::with_capacity(n.min(4096));
for _ in 0..n {
let k = r.str("store_listing.key")?;
out.push((k, r.u64("store_listing.size")?));
}
r.expect_end("store_listing")?;
Ok(out)
}
pub fn encode_store_get(v: &Option<Vec<u8>>) -> Vec<u8> {
let mut w = Writer::new();
w.opt_bytes(v.as_deref());
w.finish()
}
pub fn decode_store_get(b: &[u8]) -> Result<Option<Vec<u8>>, DecodeError> {
let mut r = Reader::new(b);
let v = r.opt_bytes("store_get.body")?;
r.expect_end("store_get")?;
Ok(v)
}