Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

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)
}