znippy-common/src/plugins/native/cargo_native.rs
//! Native Cargo/crate registry plugin.
//! Extracts crate name + version from .crate filenames (zero decompression cost).
//! Optionally parses Cargo.toml inside the tarball for deps (only if needed).
use crate::plugin::{file_name, ArchiveTypePlugin, Col, ExtensionRow, ExtensionValue, Claim, HandlerSpec};
use std::collections::HashMap;
/// Native plugin that extracts crate metadata from .crate file paths.
/// Name and version are parsed from the filename (no I/O needed).
pub struct CargoPlugin {
/// If true, also decompress and parse Cargo.toml for dependency list
pub parse_deps: bool,
}
impl CargoPlugin {
pub fn new() -> Self {
Self { parse_deps: false }
}
pub fn with_deps() -> Self {
Self { parse_deps: true }
}
/// Parse name and version from filename like "serde-1.0.200.crate"
fn parse_filename(path: &str) -> Option<(String, String)> {
let stem = file_name(path).strip_suffix(".crate")?;
// Split at last hyphen followed by a digit (version start)
let mut split_pos = None;
for (i, c) in stem.char_indices() {
if c == '-' {
// Check if next char is a digit
if let Some(next) = stem[i+1..].chars().next() {
if next.is_ascii_digit() {
split_pos = Some(i);
}
}
}
}
let pos = split_pos?;
let name = &stem[..pos];
let version = &stem[pos+1..];
Some((name.to_string(), version.to_string()))
}
/// Parse deps from .crate tarball (only when parse_deps = true).
///
/// Uses the workspace's own `lgz` crate, which does gzip-decompress +
/// tar-extract + filter-by-name in one parallel zero-copy call. We filter
/// for `Cargo.toml`, pick the entry whose path ends in `Cargo.toml`
/// (the top-level `<name-version>/Cargo.toml`), and feed it to
/// `extract_dep_names`. Any error → empty dep list (never panic).
#[cfg(feature = "host-decompressors")]
fn parse_deps_from_tarball(data: &[u8]) -> Vec<String> {
let entries = match lgz::decompress_tar_gz_filter(data, "Cargo.toml") {
Ok(entries) => entries,
Err(_) => return Vec::new(),
};
for (path, bytes) in &entries {
if path.ends_with("/Cargo.toml") || path == "Cargo.toml" {
let contents = String::from_utf8_lossy(bytes);
return Self::extract_dep_names(&contents);
}
}
Vec::new()
}
#[cfg(feature = "host-decompressors")]
fn extract_dep_names(cargo_toml: &str) -> Vec<String> {
let mut deps = Vec::new();
let mut in_deps = false;
for line in cargo_toml.lines() {
let trimmed = line.trim();
if trimmed == "[dependencies]" {
in_deps = true;
} else if trimmed.starts_with('[') {
in_deps = false;
} else if in_deps {
if let Some(dep_name) = trimmed.split('=').next() {
let dep_name = dep_name.trim();
if !dep_name.is_empty() && !dep_name.starts_with('#') {
deps.push(dep_name.to_string());
}
}
}
}
deps
}
}
/// The cargo handler, stated once.
pub static CARGO_SPEC: HandlerSpec = HandlerSpec {
name: "cargo",
aliases: &["rust"],
type_id: 1,
ecosystem: "Rust / crates.io",
extensions: &[".crate"],
claim: Claim::Extensions,
description: "Rust crate registry tarballs — name + version from filename, deps from Cargo.toml",
// `crate_name`/`version` are the READ-side coords the typed RustView maps back;
// `deps` (`[dependencies]` names) is filled only by `with_deps()`.
fields: &[("crate_name", Col::Utf8), ("version", Col::Utf8), ("deps", Col::Utf8List)],
commands: &[],
};
impl ArchiveTypePlugin for CargoPlugin {
fn spec(&self) -> &HandlerSpec {
&CARGO_SPEC
}
fn extract_metadata(&self, path: &str, data: &[u8]) -> Option<ExtensionRow> {
let (crate_name, version) = Self::parse_filename(path)?;
let mut fields = HashMap::new();
fields.insert("crate_name".into(), ExtensionValue::Str(crate_name));
fields.insert("version".into(), ExtensionValue::Str(version));
#[cfg(feature = "host-decompressors")]
if self.parse_deps {
let deps = Self::parse_deps_from_tarball(data);
fields.insert("deps".into(), ExtensionValue::StrList(deps));
}
#[cfg(not(feature = "host-decompressors"))]
let _ = data; // suppress unused warning
Some(ExtensionRow { fields })
}
}