znippy_common/plugins/native/
cargo_native.rs1use crate::plugin::{ArchiveTypePlugin, ExtensionRow, ExtensionValue, HandlerCommand, HandlerMeta};
6use arrow::datatypes::{DataType, Field};
7use std::collections::HashMap;
8
9pub struct CargoPlugin {
12 pub parse_deps: bool,
14}
15
16impl CargoPlugin {
17 pub fn new() -> Self {
18 Self { parse_deps: false }
19 }
20
21 pub fn with_deps() -> Self {
22 Self { parse_deps: true }
23 }
24
25 fn parse_filename(path: &str) -> Option<(String, String)> {
27 let filename = path.rsplit('/').next()?;
28 let stem = filename.strip_suffix(".crate")?;
29 let mut split_pos = None;
31 for (i, c) in stem.char_indices() {
32 if c == '-' {
33 if let Some(next) = stem[i+1..].chars().next() {
35 if next.is_ascii_digit() {
36 split_pos = Some(i);
37 }
38 }
39 }
40 }
41 let pos = split_pos?;
42 let name = &stem[..pos];
43 let version = &stem[pos+1..];
44 Some((name.to_string(), version.to_string()))
45 }
46
47 #[cfg(feature = "host-decompressors")]
55 fn parse_deps_from_tarball(data: &[u8]) -> Vec<String> {
56 let entries = match lgz::decompress_tar_gz_filter(data, "Cargo.toml") {
57 Ok(entries) => entries,
58 Err(_) => return Vec::new(),
59 };
60
61 for (path, bytes) in &entries {
62 if path.ends_with("/Cargo.toml") || path == "Cargo.toml" {
63 let contents = String::from_utf8_lossy(bytes);
64 return Self::extract_dep_names(&contents);
65 }
66 }
67 Vec::new()
68 }
69
70 #[cfg(feature = "host-decompressors")]
71 fn extract_dep_names(cargo_toml: &str) -> Vec<String> {
72 let mut deps = Vec::new();
73 let mut in_deps = false;
74 for line in cargo_toml.lines() {
75 let trimmed = line.trim();
76 if trimmed == "[dependencies]" {
77 in_deps = true;
78 } else if trimmed.starts_with('[') {
79 in_deps = false;
80 } else if in_deps {
81 if let Some(dep_name) = trimmed.split('=').next() {
82 let dep_name = dep_name.trim();
83 if !dep_name.is_empty() && !dep_name.starts_with('#') {
84 deps.push(dep_name.to_string());
85 }
86 }
87 }
88 }
89 deps
90 }
91}
92
93impl ArchiveTypePlugin for CargoPlugin {
94 fn name(&self) -> &str {
95 "cargo"
96 }
97
98 fn type_id(&self) -> i8 {
99 1
100 }
101
102 fn meta(&self) -> HandlerMeta {
103 HandlerMeta {
104 name: "cargo".into(),
105 aliases: vec!["rust".into()],
106 type_id: 1,
107 ecosystem: "Rust / crates.io".into(),
108 extensions: vec![".crate".into()],
109 description: "Rust crate registry tarballs — name + version from filename, deps from Cargo.toml".into(),
110 commands: vec![
111 HandlerCommand::new("coords", "Print crate name + version parsed from a .crate path"),
112 ],
113 }
114 }
115
116 fn run_command(&self, cmd: &str, args: &[String]) -> anyhow::Result<()> {
117 match cmd {
118 "coords" => {
119 let path = args.first()
120 .ok_or_else(|| anyhow::anyhow!("usage: cargo coords <file.crate>"))?;
121 let (name, version) = Self::parse_filename(path)
122 .ok_or_else(|| anyhow::anyhow!("not a .crate path: {}", path))?;
123 println!("{} {}", name, version);
124 Ok(())
125 }
126 other => anyhow::bail!("cargo: unknown subcommand '{}'", other),
127 }
128 }
129
130 fn matches_path(&self, path: &str) -> bool {
131 path.ends_with(".crate")
132 }
133
134 fn schema_fields(&self) -> Vec<Field> {
139 vec![
140 Field::new("crate_name", DataType::Utf8, true),
141 Field::new("version", DataType::Utf8, true),
142 ]
143 }
144
145 fn extract_metadata(&self, path: &str, data: &[u8]) -> Option<ExtensionRow> {
146 let (crate_name, version) = Self::parse_filename(path)?;
147
148 let mut fields = HashMap::new();
149 fields.insert("crate_name".into(), ExtensionValue::Str(crate_name));
150 fields.insert("version".into(), ExtensionValue::Str(version));
151
152 #[cfg(feature = "host-decompressors")]
153 if self.parse_deps {
154 let deps = Self::parse_deps_from_tarball(data);
155 fields.insert("deps".into(), ExtensionValue::StrList(deps));
156 }
157
158 #[cfg(not(feature = "host-decompressors"))]
159 let _ = data; Some(ExtensionRow { fields })
162 }
163}