plugin/handler-skidbladnir/src/lib.rs
//! **The Skidbladnir airgap-bundle package handler for holger.**
//!
//! A Skidbladnir bundle is the Tillsynia/Njord deploy payload: one sealed
//! `.znippy` holding a pg_dump, S3 document blobs, container images, infra
//! operators, a Talos ISO, declarative config and age-encrypted secrets.
//! holger's job is to *serve* those bundles; znippy's job is to build and index
//! them.
//!
//! # This crate owns the logic; its wasm sibling owns none
//!
//! `holger-handler-skidbladnir-wasm` depends on this crate and exports only the
//! eight ABI symbols. Everything a request is answered with comes from here and
//! from [`holger_handler_bundle_core`], so the two surfaces cannot diverge —
//! `tests/native_wasm_agree.rs` in the wasm crate asserts field-by-field that
//! they do not.
//!
//! # Where the logic actually comes from
//!
//! | piece | owner |
//! |---|---|
//! | store key · coordinate parse · HTTP routes · fetch/put/list | [`holger_handler_bundle_core`] — shared verbatim with the rust-toolchain handler, which is the same repository with a different classifier |
//! | member classification | `znippy_plugin_skidbladnir::member`, taken `default-features = false`. That leaves the pure `path → MemberKind` classifier and compiles **out** the `ArchiveTypePlugin` impl with its `znippy-common` dependency — arrow, io_uring and the gatling engine, none of which links on `wasm32-unknown-unknown`. |
//! | the bytes | nowhere here. Storage is a `BlobStore` the caller supplies. |
//!
//! That second row is the seam. It is the same one `znippy-plugin-maven` uses for
//! its wasm build (`host-decompressors` off ⇒ the threaded `ljar` fan-out is
//! compiled out, the single-threaded `linflate` both sides share remains).
//! Unlike maven there is no *threading* to compile out here — a bundle
//! coordinate scheme was never parallel — so what the feature removes is the
//! Arrow index writer, which this handler does not use either way. Measured
//! 2026-08-05: `cargo check -p znippy-plugin-skidbladnir --no-default-features
//! --target wasm32-unknown-unknown` builds the crate alone, with nothing else in
//! the graph.
use holger_handler_bundle_core::{BundleHandler, BundleKind, MemberDescription, BUNDLE_EXT};
pub use holger_handler_bundle_core::{coordinate_from_key, store_key, BUNDLE_CONTENT_TYPE};
pub use znippy_plugin_skidbladnir::member::{classify, component_of, is_encrypted, MemberKind};
/// The handler's stable name — what a `.wasm` reports in its manifest and what
/// an operator writes in a repository config.
pub const HANDLER_NAME: &str = "skidbladnir";
/// The `holger_traits::ArtifactFormat` this handler reports, as its
/// serde-lowercase spelling.
///
/// **This is a finding, not a preference.** holger has no `Skidbladnir` variant:
/// `ALL_ARTIFACT_FORMATS` is Maven3, Pip, Rust, Go,
/// Nuget, Npm, Gem, Deb, Rpm, Helm, Docker, Conda, Composer, Znippy, Raw. Adding
/// one touches `ArtifactFormat`, `ArtifactFormat::home`,
/// `znippy_type_id` and every exhaustive match over the enum across sixteen
/// backend crates — the owner's call, so it is reported rather than smuggled in.
///
/// `znippy` is meanwhile the *honest* answer and not a placeholder: a Skidbladnir
/// bundle **is** a sealed `.znippy` served as the artifact itself, which is
/// exactly what `ArtifactFormat::Znippy` documents itself to mean. What is lost
/// is only telling a bundle from any other `.znippy` **by format alone** — the
/// handler's own identity survives, because the plugin manifest carries
/// `handler = "skidbladnir"` independently of `format`.
pub const HANDLER_FORMAT: &str = "znippy";
/// Skidbladnir's [`BundleKind`]: the three things that are not shared.
pub struct Skidbladnir;
impl BundleKind for Skidbladnir {
const HANDLER: &'static str = HANDLER_NAME;
const FORMAT: &'static str = HANDLER_FORMAT;
const WRITABLE: bool = true;
const SUPPLIER: &'static str = "nordisk";
const VERSION: &'static str = env!("CARGO_PKG_VERSION");
fn describe_member(path: &str) -> Option<MemberDescription> {
let kind = classify(path)?;
Some(MemberDescription {
kind: kind.as_str().to_string(),
component: component_of(kind, path).unwrap_or("-").to_string(),
encrypted: is_encrypted(kind, path),
})
}
/// Any sealed `.znippy` in the store is a candidate. A Skidbladnir bundle
/// carries no name prefix — `tillsynia-20260706.znippy` is a product name,
/// not a marker — so narrowing further would silently hide bundles for a
/// product nobody hardcoded. (Contrast the rust-toolchain handler, whose
/// bundles genuinely do carry a `rust-dev-` prefix.)
fn is_bundle_name(file_name: &str) -> bool {
file_name.ends_with(BUNDLE_EXT)
}
}
/// The handler instance holger loads.
pub type SkidbladnirHandler = BundleHandler<Skidbladnir>;
/// Classify one bundle-relative path — the shared ownership rule, re-exported so
/// a caller need not name the znippy crate.
pub fn describe_member(path: &str) -> Option<MemberDescription> {
Skidbladnir::describe_member(path)
}
#[cfg(test)]
mod tests {
use super::*;
use holger_plugin_abi::PackageHandler;
#[test]
fn the_manifest_names_this_handler() {
let m = SkidbladnirHandler::new().manifest();
assert_eq!(m.handler, "skidbladnir");
assert_eq!(m.format, "znippy");
assert!(m.writable);
}
/// The classifier really is znippy's, reached through the wasm-safe seam —
/// asserted on values it would be impossible to produce by accident.
#[test]
fn the_shared_classifier_is_reached() {
let d = describe_member("s3/2026/07/protokoll-123.pdf").expect("claimed");
assert_eq!(d.kind, "s3-blob");
assert_eq!(d.component, "minio");
assert!(!d.encrypted);
let s = describe_member("secrets/secrets.age").expect("claimed");
assert_eq!(s.kind, "secret");
assert!(s.encrypted, "an .age member was not flagged encrypted");
assert_eq!(
describe_member("README.md"),
None,
"a foreign path was claimed"
);
}
#[test]
fn bundle_level_members_report_a_dash_component() {
let d = describe_member("lakespec.json").expect("claimed");
assert_eq!(d.kind, "lakespec");
assert_eq!(d.component, "-");
}
}