Skip to main content

znippy_common/plugins/native/
cargo_native.rs

1//! Native Cargo/crate registry plugin.
2//! Extracts crate name + version from .crate filenames (zero decompression cost).
3//! Optionally parses Cargo.toml inside the tarball for deps (only if needed).
4
5use crate::plugin::{file_name, ArchiveTypePlugin, Col, ExtensionRow, ExtensionValue, Claim, HandlerSpec};
6use std::collections::HashMap;
7
8/// Native plugin that extracts crate metadata from .crate file paths.
9/// Name and version are parsed from the filename (no I/O needed).
10pub struct CargoPlugin {
11    /// If true, also decompress and parse Cargo.toml for dependency list
12    pub parse_deps: bool,
13}
14
15impl CargoPlugin {
16    pub fn new() -> Self {
17        Self { parse_deps: false }
18    }
19
20    pub fn with_deps() -> Self {
21        Self { parse_deps: true }
22    }
23
24    /// Parse name and version from filename like "serde-1.0.200.crate"
25    fn parse_filename(path: &str) -> Option<(String, String)> {
26        let stem = file_name(path).strip_suffix(".crate")?;
27        // Split at last hyphen followed by a digit (version start)
28        let mut split_pos = None;
29        for (i, c) in stem.char_indices() {
30            if c == '-' {
31                // Check if next char is a digit
32                if let Some(next) = stem[i+1..].chars().next() {
33                    if next.is_ascii_digit() {
34                        split_pos = Some(i);
35                    }
36                }
37            }
38        }
39        let pos = split_pos?;
40        let name = &stem[..pos];
41        let version = &stem[pos+1..];
42        Some((name.to_string(), version.to_string()))
43    }
44
45    /// Parse deps from .crate tarball (only when parse_deps = true).
46    ///
47    /// Uses the workspace's own `lgz` crate, which does gzip-decompress +
48    /// tar-extract + filter-by-name in one parallel zero-copy call. We filter
49    /// for `Cargo.toml`, pick the entry whose path ends in `Cargo.toml`
50    /// (the top-level `<name-version>/Cargo.toml`), and feed it to
51    /// `extract_dep_names`. Any error → empty dep list (never panic).
52    #[cfg(feature = "host-decompressors")]
53    fn parse_deps_from_tarball(data: &[u8]) -> Vec<String> {
54        let entries = match lgz::decompress_tar_gz_filter(data, "Cargo.toml") {
55            Ok(entries) => entries,
56            Err(_) => return Vec::new(),
57        };
58
59        for (path, bytes) in &entries {
60            if path.ends_with("/Cargo.toml") || path == "Cargo.toml" {
61                let contents = String::from_utf8_lossy(bytes);
62                return Self::extract_dep_names(&contents);
63            }
64        }
65        Vec::new()
66    }
67
68    #[cfg(feature = "host-decompressors")]
69    fn extract_dep_names(cargo_toml: &str) -> Vec<String> {
70        let mut deps = Vec::new();
71        let mut in_deps = false;
72        for line in cargo_toml.lines() {
73            let trimmed = line.trim();
74            if trimmed == "[dependencies]" {
75                in_deps = true;
76            } else if trimmed.starts_with('[') {
77                in_deps = false;
78            } else if in_deps {
79                if let Some(dep_name) = trimmed.split('=').next() {
80                    let dep_name = dep_name.trim();
81                    if !dep_name.is_empty() && !dep_name.starts_with('#') {
82                        deps.push(dep_name.to_string());
83                    }
84                }
85            }
86        }
87        deps
88    }
89}
90
91/// The cargo handler, stated once.
92pub static CARGO_SPEC: HandlerSpec = HandlerSpec {
93    name: "cargo",
94    aliases: &["rust"],
95    type_id: 1,
96    ecosystem: "Rust / crates.io",
97    extensions: &[".crate"],
98    claim: Claim::Extensions,
99    description: "Rust crate registry tarballs — name + version from filename, deps from Cargo.toml",
100    // `crate_name`/`version` are the READ-side coords the typed RustView maps back;
101    // `deps` (`[dependencies]` names) is filled only by `with_deps()`.
102    fields: &[("crate_name", Col::Utf8), ("version", Col::Utf8), ("deps", Col::Utf8List)],
103    commands: &[],
104};
105
106impl ArchiveTypePlugin for CargoPlugin {
107    fn spec(&self) -> &HandlerSpec {
108        &CARGO_SPEC
109    }
110
111    fn extract_metadata(&self, path: &str, data: &[u8]) -> Option<ExtensionRow> {
112        let (crate_name, version) = Self::parse_filename(path)?;
113
114        let mut fields = HashMap::new();
115        fields.insert("crate_name".into(), ExtensionValue::Str(crate_name));
116        fields.insert("version".into(), ExtensionValue::Str(version));
117
118        #[cfg(feature = "host-decompressors")]
119        if self.parse_deps {
120            let deps = Self::parse_deps_from_tarball(data);
121            fields.insert("deps".into(), ExtensionValue::StrList(deps));
122        }
123
124        #[cfg(not(feature = "host-decompressors"))]
125        let _ = data; // suppress unused warning
126
127        Some(ExtensionRow { fields })
128    }
129}