#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MemberKind {
Manifest,
ToolchainPins,
VendorLock,
ToolchainBin,
RustStd,
ToolchainLib,
VendorCrate,
CargoConfig,
}
impl MemberKind {
pub fn as_str(self) -> &'static str {
match self {
MemberKind::Manifest => "manifest",
MemberKind::ToolchainPins => "toolchain-pins",
MemberKind::VendorLock => "vendor-lock",
MemberKind::ToolchainBin => "toolchain-bin",
MemberKind::RustStd => "rust-std",
MemberKind::ToolchainLib => "toolchain-lib",
MemberKind::VendorCrate => "vendor-crate",
MemberKind::CargoConfig => "cargo-config",
}
}
}
fn segments(path: &str) -> Vec<&str> {
path.split(['/', '\\']).filter(|s| !s.is_empty() && *s != ".").collect()
}
pub fn classify(path: &str) -> Option<MemberKind> {
let segs = segments(path);
let top = *segs.first()?;
match top {
"manifest" => {
let name = *segs.last().unwrap_or(&top);
if name == "toolchain.pins.json" {
Some(MemberKind::ToolchainPins)
} else if name == "vendor.lock" {
Some(MemberKind::VendorLock)
} else {
Some(MemberKind::Manifest)
}
}
"toolchain" => {
if segs.get(1).copied() == Some("bin") {
Some(MemberKind::ToolchainBin)
} else if let Some(i) = segs.iter().position(|s| *s == "rustlib") {
if segs.len() > i + 2 {
Some(MemberKind::RustStd)
} else {
Some(MemberKind::ToolchainLib)
}
} else {
Some(MemberKind::ToolchainLib)
}
}
"vendor" => Some(MemberKind::VendorCrate),
"cargo-config" => Some(MemberKind::CargoConfig),
_ => None,
}
}
pub fn component_of(kind: MemberKind, path: &str) -> Option<String> {
let segs = segments(path);
match kind {
MemberKind::VendorCrate => segs.get(1).map(|s| s.to_string()),
MemberKind::RustStd => {
let idx = segs.iter().position(|s| *s == "rustlib")?;
segs.get(idx + 1).map(|s| s.to_string())
}
MemberKind::ToolchainBin => segs.last().map(|s| s.to_string()),
_ => None,
}
}
pub fn is_rust_dev_bundle(name: &str) -> bool {
let base = name.rsplit(['/', '\\']).next().unwrap_or(name);
base.starts_with("rust-dev-") && base.ends_with(".znippy")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classifies_every_member_role() {
let cases = [
("manifest/bundle.json", MemberKind::Manifest),
("manifest/toolchain.pins.json", MemberKind::ToolchainPins),
("manifest/vendor.lock", MemberKind::VendorLock),
("toolchain/bin/rustc", MemberKind::ToolchainBin),
("toolchain/bin/cargo", MemberKind::ToolchainBin),
("toolchain/lib/librustc_driver.so", MemberKind::ToolchainLib),
(
"toolchain/lib/rustlib/x86_64-unknown-linux-gnu/lib/libstd.rlib",
MemberKind::RustStd,
),
("vendor/serde-1.0.203/src/lib.rs", MemberKind::VendorCrate),
("vendor/anyhow-1.0.86/Cargo.toml", MemberKind::VendorCrate),
("cargo-config/config.toml", MemberKind::CargoConfig),
];
for (p, want) in cases {
assert_eq!(classify(p), Some(want), "classify({p})");
}
}
#[test]
fn rustlib_root_is_generic_lib_not_std() {
assert_eq!(
classify("toolchain/lib/rustlib/components"),
Some(MemberKind::ToolchainLib)
);
}
#[test]
fn unknown_paths_are_not_claimed() {
assert_eq!(classify("README.md"), None);
assert_eq!(classify("s3/doc.pdf"), None);
assert_eq!(classify(""), None);
assert_eq!(classify("./vendor/serde-1.0/src/lib.rs"), Some(MemberKind::VendorCrate));
}
#[test]
fn component_derivation() {
assert_eq!(
component_of(MemberKind::VendorCrate, "vendor/serde-1.0.203/src/lib.rs").as_deref(),
Some("serde-1.0.203")
);
assert_eq!(
component_of(
MemberKind::RustStd,
"toolchain/lib/rustlib/x86_64-unknown-linux-gnu/lib/libstd.rlib"
)
.as_deref(),
Some("x86_64-unknown-linux-gnu")
);
assert_eq!(
component_of(MemberKind::ToolchainBin, "toolchain/bin/cargo").as_deref(),
Some("cargo")
);
assert_eq!(component_of(MemberKind::Manifest, "manifest/bundle.json"), None);
}
#[test]
fn recognises_bundle_archive_name() {
assert!(is_rust_dev_bundle("rust-dev-rhel8-1.97.1.znippy"));
assert!(is_rust_dev_bundle("/tmp/rust-dev-rhel8-1.97.1.znippy"));
assert!(!is_rust_dev_bundle("tillsynia-20260706.znippy"));
assert!(!is_rust_dev_bundle("rust-dev-rhel8-1.97.1.tar.zst"));
}
}