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

nordisk/holger

plugin/host/src/backend.rs

//! **The one `RepositoryBackendTrait` implementation.**
//!
//! There is exactly one in this crate, and both a linked handler and a loaded
//! `.wasm` reach the router through it. That is not a convention that has to be
//! checked — `fast_routes.rs` receives an `Arc<dyn RepositoryBackendTrait>`
//! whose concrete type is [`HandlerBackend`] in both cases, so there is no
//! second code path for it to take. If the router ever needed to know which kind
//! it had, the design would be wrong.
//!
//! The split that makes this possible is [`HandlerRuntime`]: *how* a verb is
//! executed (a direct call, or an ABI round trip through wasmtime) versus *what*
//! the router asks for. Storage is bound into the runtime at construction, so by
//! the time the trait is implemented the two are the same shape.

use std::sync::Arc;

use holger_plugin_abi::{
    BlobStore, PackageHandler, WireArtifactEntry, WireArtifactId, WireHttpRequest,
    WireHttpResponse, WireManifest,
};
use traits::{ArtifactEntry, ArtifactFormat, ArtifactId, RepositoryBackendTrait};

use crate::convert::{entry_from_wire, format_from_wire, id_from_wire, id_to_wire};

/// A [`BlobStore`] usable from the server's request path.
pub type SharedStore = Arc<dyn BlobStore + Send + Sync>;

/// One handler, with its storage already bound — however it is executed.
///
/// The verbs mirror [`PackageHandler`] minus the `store` argument: a runtime owns
/// its store, because a wasm module's store has to live in the wasmtime `Store`
/// data and cannot be handed in per call.
pub trait HandlerRuntime: Send + Sync {
    fn manifest(&self) -> &WireManifest;
    fn fetch(&self, id: &WireArtifactId) -> Result<Option<Vec<u8>>, String>;
    fn put(&self, id: &WireArtifactId, data: &[u8]) -> Result<(), String>;
    fn list(
        &self,
        name_filter: Option<&str>,
        limit: usize,
    ) -> Result<Vec<WireArtifactEntry>, String>;
    fn coordinate_for_path(&self, suburl: &str) -> Option<WireArtifactId>;
    fn http(&self, req: &WireHttpRequest) -> Result<WireHttpResponse, String>;
}

/// A handler compiled into the server, bound to a store. The **native** half of
/// "every package handler can be written native or WASM".
pub struct LinkedHandler<H: PackageHandler + Send + Sync> {
    handler: H,
    store: SharedStore,
    manifest: WireManifest,
}

impl<H: PackageHandler + Send + Sync> LinkedHandler<H> {
    pub fn new(handler: H, store: SharedStore) -> Self {
        let manifest = handler.manifest();
        Self {
            handler,
            store,
            manifest,
        }
    }
}

impl<H: PackageHandler + Send + Sync> HandlerRuntime for LinkedHandler<H> {
    fn manifest(&self) -> &WireManifest {
        &self.manifest
    }

    fn fetch(&self, id: &WireArtifactId) -> Result<Option<Vec<u8>>, String> {
        self.handler.fetch(&*self.store, id)
    }

    fn put(&self, id: &WireArtifactId, data: &[u8]) -> Result<(), String> {
        self.handler.put(&*self.store, id, data)
    }

    fn list(
        &self,
        name_filter: Option<&str>,
        limit: usize,
    ) -> Result<Vec<WireArtifactEntry>, String> {
        self.handler.list(&*self.store, name_filter, limit)
    }

    fn coordinate_for_path(&self, suburl: &str) -> Option<WireArtifactId> {
        self.handler.coordinate_for_path(suburl)
    }

    fn http(&self, req: &WireHttpRequest) -> Result<WireHttpResponse, String> {
        self.handler.http(&*self.store, req)
    }
}

/// A package handler as holger's router sees it.
///
/// `Debug` prints the repository name, the format and the handler name — never
/// the runtime, because the whole point is that a caller cannot tell a linked
/// handler from a loaded module by looking at one.
pub struct HandlerBackend {
    /// The repository name this backend is wired under — the router's key, which
    /// is an operator's choice and NOT the handler's name (two repositories can
    /// run the same handler over different stores).
    repo_name: String,
    format: ArtifactFormat,
    runtime: Box<dyn HandlerRuntime>,
}

impl HandlerBackend {
    /// Wrap a runtime. Fails if the handler declared a format holger does not
    /// know — never falling back to `Raw`, because a mis-declared format that
    /// registered anyway would serve real bytes under the wrong protocol.
    pub fn new(
        repo_name: impl Into<String>,
        runtime: Box<dyn HandlerRuntime>,
    ) -> Result<Self, String> {
        let format = format_from_wire(&runtime.manifest().format)?;
        Ok(Self {
            repo_name: repo_name.into(),
            format,
            runtime,
        })
    }

    /// The handler's own name (`skidbladnir`), as distinct from the repository
    /// name this backend is mounted at.
    pub fn handler_name(&self) -> &str {
        &self.runtime.manifest().handler
    }

    pub fn into_backend(self) -> Arc<dyn RepositoryBackendTrait> {
        Arc::new(self)
    }
}

impl std::fmt::Debug for HandlerBackend {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("HandlerBackend")
            .field("repo_name", &self.repo_name)
            .field("format", &self.format)
            .field("handler", &self.runtime.manifest().handler)
            .finish()
    }
}

impl RepositoryBackendTrait for HandlerBackend {
    fn name(&self) -> &str {
        &self.repo_name
    }

    fn format(&self) -> ArtifactFormat {
        self.format.clone()
    }

    fn is_writable(&self) -> bool {
        self.runtime.manifest().writable
    }

    fn fetch(&self, id: &ArtifactId) -> anyhow::Result<Option<traits::Bytes>> {
        self.runtime
            .fetch(&id_to_wire(id))
            .map(|o| o.map(traits::Bytes::from))
            .map_err(|e| anyhow::anyhow!("{}: {e}", self.repo_name))
    }

    fn put(&self, id: &ArtifactId, data: &[u8]) -> anyhow::Result<()> {
        self.runtime
            .put(&id_to_wire(id), data)
            .map_err(|e| anyhow::anyhow!("{}: {e}", self.repo_name))
    }

    fn list(&self, name_filter: Option<&str>, limit: usize) -> anyhow::Result<Vec<ArtifactEntry>> {
        let rows = self
            .runtime
            .list(name_filter, limit)
            .map_err(|e| anyhow::anyhow!("{}: {e}", self.repo_name))?;
        Ok(rows.into_iter().map(entry_from_wire).collect())
    }

    /// A handler lists, and its `coordinate_for_path` is mandatory in the ABI
    /// (the inverse of its own path scheme), so the mapping is total. No
    /// archive (a handler has no znippy handle) and no delete (this backend
    /// cannot vouch for the store's digest recomputation).
    fn caps(&self) -> traits::Caps {
        traits::Caps {
            listing: true,
            path_mapping: true,
            ..traits::Caps::NONE
        }
    }

    fn handle_http2_request(
        &self,
        method: &str,
        suburl: &str,
        body: &[u8],
    ) -> anyhow::Result<traits::HttpReply> {
        let req = WireHttpRequest {
            method: method.to_string(),
            suburl: suburl.to_string(),
            body: body.to_vec(),
        };
        let resp = self
            .runtime
            .http(&req)
            .map_err(|e| anyhow::anyhow!("{}: {e}", self.repo_name))?;
        Ok((resp.status, resp.headers, resp.body))
    }

    fn coordinate_for_path(&self, suburl: &str) -> anyhow::Result<Option<ArtifactId>> {
        Ok(self.runtime.coordinate_for_path(suburl).map(id_from_wire))
    }
}

#[cfg(test)]
mod drift_tests {
    use super::*;

    struct Lists(WireManifest);
    impl HandlerRuntime for Lists {
        fn manifest(&self) -> &WireManifest {
            &self.0
        }
        fn fetch(&self, _: &WireArtifactId) -> Result<Option<Vec<u8>>, String> {
            Ok(None)
        }
        fn put(&self, _: &WireArtifactId, _: &[u8]) -> Result<(), String> {
            Err("ro".into())
        }
        fn list(&self, _: Option<&str>, _: usize) -> Result<Vec<WireArtifactEntry>, String> {
            Ok(Vec::new())
        }
        fn coordinate_for_path(&self, _: &str) -> Option<WireArtifactId> {
            None
        }
        fn http(&self, _: &WireHttpRequest) -> Result<WireHttpResponse, String> {
            Err("none".into())
        }
    }

    /// RED drift bug: `list` is overridden, so a counter must be told the
    /// listing is real — the flag stayed at the trait default `false`.
    #[test]
    fn a_handler_that_lists_says_it_lists() {
        let m = WireManifest {
            abi_version: holger_plugin_abi::ABI_VERSION,
            handler: "h".into(),
            format: "raw".into(),
            writable: false,
            identity: None,
        };
        let b = HandlerBackend::new("r", Box::new(Lists(m))).unwrap();
        assert!(
            b.caps().listing,
            "HandlerBackend overrides list() but did not claim the listing"
        );
        // The ABI makes `coordinate_for_path` mandatory: the inverse of the
        // handler's own path scheme, so the mapping is total.
        assert!(
            b.caps().path_mapping,
            "HandlerBackend overrides coordinate_for_path() but did not claim the mapping"
        );
        traits::backend::conformance::check(&b).unwrap();
    }
}