plugins/native/znippy-plugin-rust-toolchain/src/lib.rs
//! znippy handler for **airgapped Rust-developer bundles** — the
//! `rust-dev-rhel8-<ver>.znippy` payload (a standalone rustup-dist toolchain +
//! a `cargo vendor` crate set + a `.cargo/config` + pinned integrity manifest)
//! that lets a disconnected RHEL 8 box `cargo build`/`cargo run` offline.
//!
//! It teaches znippy's Arrow-IPC index what each entry of a sealed rust-dev
//! bundle *is*, so the archive is self-describing and DuckDB/Polars-queryable
//! (`SELECT logical_path FROM bundle WHERE member_kind = 'vendor-crate'`). It adds
//! **only metadata columns** — the format, the pack/move/unfold mechanics and the
//! sealing stay owned by znippy + Skidbladnir.
//!
//! Native builtin: registered in `znippy-cli/src/handlers.rs::builtin_handlers`,
//! discovered via [`ArchiveTypePlugin::meta`]. NOT a WASM plugin. Sibling of
//! `znippy-plugin-skidbladnir` (type_id 40) — a fresh `type_id` (41) so a
//! `znippy meta` query over a toolchain bundle is unambiguous (design decision §6.2).
//!
//! Laws it honours: **P-1** one archive = one ecosystem (`--format rust-toolchain`);
//! **P-2** writes only the columns it declared; **P-4** never panics — a member it
//! cannot parse still writes with its path-derived `member_kind`, only the optional
//! `toolchain_version`/`host_triple` degrade to null.
#[cfg(feature = "znippy-handler")]
use znippy_common::plugin::{
default_command, ArchiveTypePlugin, Col, ExtensionRow, ExtensionValue, Claim, HandlerSpec,
};
#[cfg(feature = "znippy-handler")]
use znippy_common::plugins::bundle::{json_str_field, member_row};
/// The bundle-member classifier. **Unconditional**, and deliberately free of
/// every dependency this crate has: it is pure `&str` -> enum, so it compiles for
/// `wasm32-unknown-unknown` as-is.
///
/// That is what `--no-default-features` buys a consumer. holger's package-handler
/// plugins (`holger-handler-rust-toolchain`, and its wasm shim) need this
/// ownership rule and nothing else; taking it with the default features would
/// drag `znippy-common` -- arrow, io_uring and the gatling engine -- into a
/// `wasm32` build that cannot link any of them. Same shape as
/// `znippy-plugin-maven`'s `host-decompressors`: the target-independent core
/// stays, the host-only machinery is compiled out.
pub mod member;
pub use member::{classify, component_of, is_rust_dev_bundle, MemberKind};
/// DenseUnion / pkg_type discriminant. Clear of the built-ins (1–18), media (25)
/// and the skidbladnir bundle classifier (40).
pub const RUST_TOOLCHAIN_TYPE_ID: i8 = 41;
/// The canonical bundle layout, printed by the `layout` subcommand and mirrored
/// by the classifier in [`member`].
pub const BUNDLE_LAYOUT: &str = "\
rust-dev-rhel8-<rustc-ver>.znippy
├── manifest/
│ ├── bundle.json # rustc/cargo version, host triple, source-URL provenance
│ ├── toolchain.pins.json # per-payload sha256 + blake3 (the \"pinned like the kernel\" table)
│ └── vendor.lock # exact crate set = a copy of the source Cargo.lock
├── toolchain/ # the standalone (rustup-dist) toolchain, UNPACKED into the tree
│ ├── bin/ # rustc, cargo, rustdoc, rust-lld, …
│ ├── lib/ # librustc driver .so's, std .rlibs
│ └── lib/rustlib/<triple>/ # the rust-std for the target
├── vendor/ # `cargo vendor` output — the \"basic crates\" set
│ └── <crate>-<ver>/… # already-compressed .crate contents expanded to source
└── cargo-config/
└── config.toml # source-replacement → vendor/, offline hard-on";
/// Native rust-dev-bundle handler.
#[cfg(feature = "znippy-handler")]
pub struct NativeRustToolchainPlugin;
#[cfg(feature = "znippy-handler")]
impl NativeRustToolchainPlugin {
pub fn new() -> Self {
NativeRustToolchainPlugin
}
}
#[cfg(feature = "znippy-handler")]
impl Default for NativeRustToolchainPlugin {
fn default() -> Self {
Self::new()
}
}
/// The handler's claim: a path is its own when it sits in the bundle layout
/// (`member::classify`), whatever its extension.
#[cfg(feature = "znippy-handler")]
fn is_bundle_member(path: &str) -> bool {
classify(path).is_some()
}
/// The rust-dev bundle handler, stated once.
#[cfg(feature = "znippy-handler")]
pub static RUST_TOOLCHAIN_SPEC: HandlerSpec = HandlerSpec {
name: "rust-toolchain",
aliases: &["rust-dev", "rustdev", "toolchain"],
type_id: RUST_TOOLCHAIN_TYPE_ID,
ecosystem: "Airgapped Rust developer bundle (rustup-dist toolchain + cargo-vendor set)",
extensions: &[".pins.json", ".lock", ".rlib", ".rmeta", ".toml"],
claim: Claim::Layout(is_bundle_member),
description: "Tags each rust-dev bundle entry (toolchain bin / rust-std / \
vendored crate / cargo-config / pins manifest) into the index so \
a sealed offline-Rust bundle is self-describing and queryable",
fields: &[
("member_kind", Col::Utf8),
("component", Col::Utf8),
("logical_path", Col::Utf8),
("toolchain_version", Col::Utf8),
("host_triple", Col::Utf8),
],
commands: &[
("classify", "Print the member_kind / component for a bundle path"),
("layout", "Print the canonical rust-dev bundle layout"),
],
};
#[cfg(feature = "znippy-handler")]
impl ArchiveTypePlugin for NativeRustToolchainPlugin {
fn spec(&self) -> &HandlerSpec {
&RUST_TOOLCHAIN_SPEC
}
fn run_command(&self, cmd: &str, args: &[String]) -> anyhow::Result<()> {
match cmd {
"layout" => {
println!("{BUNDLE_LAYOUT}");
Ok(())
}
"classify" => {
let path = args
.first()
.ok_or_else(|| anyhow::anyhow!("usage: rust-toolchain classify <bundle-path>"))?;
match classify(path) {
Some(kind) => {
println!("path: {path}");
println!("member_kind: {}", kind.as_str());
println!("component: {}", component_of(kind, path).as_deref().unwrap_or("-"));
Ok(())
}
None => {
anyhow::bail!("'{path}' is not a recognised rust-dev bundle member")
}
}
}
other => default_command(self, other, args),
}
}
fn extract_metadata(&self, path: &str, data: &[u8]) -> Option<ExtensionRow> {
let kind = classify(path)?;
let mut f = member_row(kind.as_str(), path, component_of(kind, path));
// toolchain_version / host_triple only live in the bundle.json manifest;
// parse best-effort. A garbage or oversized file leaves them null (P-4).
if kind == MemberKind::Manifest {
if let Some(v) = json_str_field(data, "version")
.or_else(|| json_str_field(data, "rustc_version"))
{
f.insert("toolchain_version".into(), ExtensionValue::Str(v));
}
if let Some(h) = json_str_field(data, "host")
.or_else(|| json_str_field(data, "host_triple"))
{
f.insert("host_triple".into(), ExtensionValue::Str(h));
}
}
Some(ExtensionRow { fields: f })
}
}
#[cfg(all(test, feature = "znippy-handler"))]
mod tests {
use super::*;
fn get<'a>(row: &'a ExtensionRow, k: &str) -> Option<&'a ExtensionValue> {
row.fields.get(k)
}
#[test]
fn tags_a_vendored_crate() {
let p = NativeRustToolchainPlugin::new();
let row = p.extract_metadata("vendor/serde-1.0.203/src/lib.rs", b"pub fn x() {}").unwrap();
assert_eq!(get(&row, "member_kind"), Some(&ExtensionValue::Str("vendor-crate".into())));
assert_eq!(get(&row, "component"), Some(&ExtensionValue::Str("serde-1.0.203".into())));
// a source file carries no toolchain version
assert!(get(&row, "toolchain_version").is_none());
}
#[test]
fn tags_the_rust_std_with_its_triple() {
let p = NativeRustToolchainPlugin::new();
let row = p
.extract_metadata(
"toolchain/lib/rustlib/x86_64-unknown-linux-gnu/lib/libstd.rlib",
b"\x00rlib",
)
.unwrap();
assert_eq!(get(&row, "member_kind"), Some(&ExtensionValue::Str("rust-std".into())));
assert_eq!(
get(&row, "component"),
Some(&ExtensionValue::Str("x86_64-unknown-linux-gnu".into()))
);
}
#[test]
fn parses_bundle_manifest_version_and_host() {
let p = NativeRustToolchainPlugin::new();
let json = br#"{"version":"1.97.1","host":"x86_64-unknown-linux-gnu","source":"https://static.rust-lang.org"}"#;
let row = p.extract_metadata("manifest/bundle.json", json).unwrap();
assert_eq!(get(&row, "toolchain_version"), Some(&ExtensionValue::Str("1.97.1".into())));
assert_eq!(
get(&row, "host_triple"),
Some(&ExtensionValue::Str("x86_64-unknown-linux-gnu".into()))
);
}
#[test]
fn manifest_with_garbage_json_falls_back_never_panics() {
let p = NativeRustToolchainPlugin::new();
let row = p.extract_metadata("manifest/bundle.json", b"not json at all }{").unwrap();
assert_eq!(get(&row, "member_kind"), Some(&ExtensionValue::Str("manifest".into())));
assert!(get(&row, "toolchain_version").is_none());
assert!(get(&row, "host_triple").is_none());
}
#[test]
fn does_not_claim_foreign_paths() {
let p = NativeRustToolchainPlugin::new();
assert!(!p.matches_path("README.md"));
assert!(!p.matches_path("s3/doc.pdf"));
assert!(p.matches_path("toolchain/bin/rustc"));
assert!(p.extract_metadata("README.md", b"x").is_none());
}
#[test]
fn schema_and_meta_are_consistent() {
let p = NativeRustToolchainPlugin::new();
assert_eq!(p.type_id(), RUST_TOOLCHAIN_TYPE_ID);
assert_eq!(p.meta().type_id, RUST_TOOLCHAIN_TYPE_ID);
// distinct from the skidbladnir bundle classifier's type_id (40)
assert_ne!(p.type_id(), 40);
assert_eq!(p.schema_fields().len(), 5);
}
}