znippy-plugin-rust-toolchain 0.1.1

Airgapped Rust-toolchain bundle metadata plugin for znippy (native builtin — no WASM)
Documentation
//! 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 std::collections::HashMap;

#[cfg(feature = "znippy-handler")]
use znippy_common::arrow::datatypes::{DataType, Field};
#[cfg(feature = "znippy-handler")]
use znippy_common::plugin::{
    ArchiveTypePlugin, ExtensionRow, ExtensionValue, HandlerCommand, HandlerMeta,
};

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

/// Best-effort pull of a top-level string field from a JSON member. Never
/// panics — bad/garbage JSON just yields `None` (P-4).
#[cfg(feature = "znippy-handler")]
fn json_str_field(data: &[u8], key: &str) -> Option<String> {
    let v: serde_json::Value = serde_json::from_slice(data).ok()?;
    v.get(key)?.as_str().map(str::to_string)
}

#[cfg(feature = "znippy-handler")]
impl ArchiveTypePlugin for NativeRustToolchainPlugin {
    fn name(&self) -> &str {
        "rust-toolchain"
    }

    fn type_id(&self) -> i8 {
        RUST_TOOLCHAIN_TYPE_ID
    }

    fn meta(&self) -> HandlerMeta {
        HandlerMeta {
            name: "rust-toolchain".into(),
            aliases: vec!["rust-dev".into(), "rustdev".into(), "toolchain".into()],
            type_id: RUST_TOOLCHAIN_TYPE_ID,
            ecosystem: "Airgapped Rust developer bundle (rustup-dist toolchain + cargo-vendor set)"
                .into(),
            extensions: vec![
                ".pins.json".into(),
                ".lock".into(),
                ".rlib".into(),
                ".rmeta".into(),
                ".toml".into(),
            ],
            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"
                .into(),
            commands: vec![
                HandlerCommand::new(
                    "classify",
                    "Print the member_kind / component for a bundle path",
                ),
                HandlerCommand::new("layout", "Print the canonical rust-dev bundle layout"),
            ],
        }
    }

    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 => anyhow::bail!("rust-toolchain: unknown subcommand '{other}'"),
        }
    }

    fn matches_path(&self, path: &str) -> bool {
        classify(path).is_some()
    }

    fn schema_fields(&self) -> Vec<Field> {
        vec![
            Field::new("member_kind", DataType::Utf8, true),
            Field::new("component", DataType::Utf8, true),
            Field::new("logical_path", DataType::Utf8, true),
            Field::new("toolchain_version", DataType::Utf8, true),
            Field::new("host_triple", DataType::Utf8, true),
        ]
    }

    fn extract_metadata(&self, path: &str, data: &[u8]) -> Option<ExtensionRow> {
        let kind = classify(path)?;
        let mut f: HashMap<String, ExtensionValue> = HashMap::new();

        f.insert("member_kind".into(), ExtensionValue::Str(kind.as_str().into()));
        f.insert("logical_path".into(), ExtensionValue::Str(path.into()));
        if let Some(c) = component_of(kind, path) {
            f.insert("component".into(), ExtensionValue::Str(c));
        }

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