znippy_plugin_rust_toolchain/
lib.rs1use std::collections::HashMap;
23
24use znippy_common::arrow::datatypes::{DataType, Field};
25use znippy_common::plugin::{
26 ArchiveTypePlugin, ExtensionRow, ExtensionValue, HandlerCommand, HandlerMeta,
27};
28
29pub mod member;
30pub use member::{classify, component_of, is_rust_dev_bundle, MemberKind};
31
32pub const RUST_TOOLCHAIN_TYPE_ID: i8 = 41;
35
36pub const BUNDLE_LAYOUT: &str = "\
39rust-dev-rhel8-<rustc-ver>.znippy
40├── manifest/
41│ ├── bundle.json # rustc/cargo version, host triple, source-URL provenance
42│ ├── toolchain.pins.json # per-payload sha256 + blake3 (the \"pinned like the kernel\" table)
43│ └── vendor.lock # exact crate set = a copy of the source Cargo.lock
44├── toolchain/ # the standalone (rustup-dist) toolchain, UNPACKED into the tree
45│ ├── bin/ # rustc, cargo, rustdoc, rust-lld, …
46│ ├── lib/ # librustc driver .so's, std .rlibs
47│ └── lib/rustlib/<triple>/ # the rust-std for the target
48├── vendor/ # `cargo vendor` output — the \"basic crates\" set
49│ └── <crate>-<ver>/… # already-compressed .crate contents expanded to source
50└── cargo-config/
51 └── config.toml # source-replacement → vendor/, offline hard-on";
52
53pub struct NativeRustToolchainPlugin;
55
56impl NativeRustToolchainPlugin {
57 pub fn new() -> Self {
58 NativeRustToolchainPlugin
59 }
60}
61
62impl Default for NativeRustToolchainPlugin {
63 fn default() -> Self {
64 Self::new()
65 }
66}
67
68fn json_str_field(data: &[u8], key: &str) -> Option<String> {
71 let v: serde_json::Value = serde_json::from_slice(data).ok()?;
72 v.get(key)?.as_str().map(str::to_string)
73}
74
75impl ArchiveTypePlugin for NativeRustToolchainPlugin {
76 fn name(&self) -> &str {
77 "rust-toolchain"
78 }
79
80 fn type_id(&self) -> i8 {
81 RUST_TOOLCHAIN_TYPE_ID
82 }
83
84 fn meta(&self) -> HandlerMeta {
85 HandlerMeta {
86 name: "rust-toolchain".into(),
87 aliases: vec!["rust-dev".into(), "rustdev".into(), "toolchain".into()],
88 type_id: RUST_TOOLCHAIN_TYPE_ID,
89 ecosystem: "Airgapped Rust developer bundle (rustup-dist toolchain + cargo-vendor set)"
90 .into(),
91 extensions: vec![
92 ".pins.json".into(),
93 ".lock".into(),
94 ".rlib".into(),
95 ".rmeta".into(),
96 ".toml".into(),
97 ],
98 description: "Tags each rust-dev bundle entry (toolchain bin / rust-std / \
99 vendored crate / cargo-config / pins manifest) into the index so \
100 a sealed offline-Rust bundle is self-describing and queryable"
101 .into(),
102 commands: vec![
103 HandlerCommand::new(
104 "classify",
105 "Print the member_kind / component for a bundle path",
106 ),
107 HandlerCommand::new("layout", "Print the canonical rust-dev bundle layout"),
108 ],
109 }
110 }
111
112 fn run_command(&self, cmd: &str, args: &[String]) -> anyhow::Result<()> {
113 match cmd {
114 "layout" => {
115 println!("{BUNDLE_LAYOUT}");
116 Ok(())
117 }
118 "classify" => {
119 let path = args
120 .first()
121 .ok_or_else(|| anyhow::anyhow!("usage: rust-toolchain classify <bundle-path>"))?;
122 match classify(path) {
123 Some(kind) => {
124 println!("path: {path}");
125 println!("member_kind: {}", kind.as_str());
126 println!("component: {}", component_of(kind, path).as_deref().unwrap_or("-"));
127 Ok(())
128 }
129 None => {
130 anyhow::bail!("'{path}' is not a recognised rust-dev bundle member")
131 }
132 }
133 }
134 other => anyhow::bail!("rust-toolchain: unknown subcommand '{other}'"),
135 }
136 }
137
138 fn matches_path(&self, path: &str) -> bool {
139 classify(path).is_some()
140 }
141
142 fn schema_fields(&self) -> Vec<Field> {
143 vec![
144 Field::new("member_kind", DataType::Utf8, true),
145 Field::new("component", DataType::Utf8, true),
146 Field::new("logical_path", DataType::Utf8, true),
147 Field::new("toolchain_version", DataType::Utf8, true),
148 Field::new("host_triple", DataType::Utf8, true),
149 ]
150 }
151
152 fn extract_metadata(&self, path: &str, data: &[u8]) -> Option<ExtensionRow> {
153 let kind = classify(path)?;
154 let mut f: HashMap<String, ExtensionValue> = HashMap::new();
155
156 f.insert("member_kind".into(), ExtensionValue::Str(kind.as_str().into()));
157 f.insert("logical_path".into(), ExtensionValue::Str(path.into()));
158 if let Some(c) = component_of(kind, path) {
159 f.insert("component".into(), ExtensionValue::Str(c));
160 }
161
162 if kind == MemberKind::Manifest {
165 if let Some(v) = json_str_field(data, "version")
166 .or_else(|| json_str_field(data, "rustc_version"))
167 {
168 f.insert("toolchain_version".into(), ExtensionValue::Str(v));
169 }
170 if let Some(h) = json_str_field(data, "host")
171 .or_else(|| json_str_field(data, "host_triple"))
172 {
173 f.insert("host_triple".into(), ExtensionValue::Str(h));
174 }
175 }
176
177 Some(ExtensionRow { fields: f })
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 fn get<'a>(row: &'a ExtensionRow, k: &str) -> Option<&'a ExtensionValue> {
186 row.fields.get(k)
187 }
188
189 #[test]
190 fn tags_a_vendored_crate() {
191 let p = NativeRustToolchainPlugin::new();
192 let row = p.extract_metadata("vendor/serde-1.0.203/src/lib.rs", b"pub fn x() {}").unwrap();
193 assert_eq!(get(&row, "member_kind"), Some(&ExtensionValue::Str("vendor-crate".into())));
194 assert_eq!(get(&row, "component"), Some(&ExtensionValue::Str("serde-1.0.203".into())));
195 assert!(get(&row, "toolchain_version").is_none());
197 }
198
199 #[test]
200 fn tags_the_rust_std_with_its_triple() {
201 let p = NativeRustToolchainPlugin::new();
202 let row = p
203 .extract_metadata(
204 "toolchain/lib/rustlib/x86_64-unknown-linux-gnu/lib/libstd.rlib",
205 b"\x00rlib",
206 )
207 .unwrap();
208 assert_eq!(get(&row, "member_kind"), Some(&ExtensionValue::Str("rust-std".into())));
209 assert_eq!(
210 get(&row, "component"),
211 Some(&ExtensionValue::Str("x86_64-unknown-linux-gnu".into()))
212 );
213 }
214
215 #[test]
216 fn parses_bundle_manifest_version_and_host() {
217 let p = NativeRustToolchainPlugin::new();
218 let json = br#"{"version":"1.97.1","host":"x86_64-unknown-linux-gnu","source":"https://static.rust-lang.org"}"#;
219 let row = p.extract_metadata("manifest/bundle.json", json).unwrap();
220 assert_eq!(get(&row, "toolchain_version"), Some(&ExtensionValue::Str("1.97.1".into())));
221 assert_eq!(
222 get(&row, "host_triple"),
223 Some(&ExtensionValue::Str("x86_64-unknown-linux-gnu".into()))
224 );
225 }
226
227 #[test]
228 fn manifest_with_garbage_json_falls_back_never_panics() {
229 let p = NativeRustToolchainPlugin::new();
230 let row = p.extract_metadata("manifest/bundle.json", b"not json at all }{").unwrap();
231 assert_eq!(get(&row, "member_kind"), Some(&ExtensionValue::Str("manifest".into())));
232 assert!(get(&row, "toolchain_version").is_none());
233 assert!(get(&row, "host_triple").is_none());
234 }
235
236 #[test]
237 fn does_not_claim_foreign_paths() {
238 let p = NativeRustToolchainPlugin::new();
239 assert!(!p.matches_path("README.md"));
240 assert!(!p.matches_path("s3/doc.pdf"));
241 assert!(p.matches_path("toolchain/bin/rustc"));
242 assert!(p.extract_metadata("README.md", b"x").is_none());
243 }
244
245 #[test]
246 fn schema_and_meta_are_consistent() {
247 let p = NativeRustToolchainPlugin::new();
248 assert_eq!(p.type_id(), RUST_TOOLCHAIN_TYPE_ID);
249 assert_eq!(p.meta().type_id, RUST_TOOLCHAIN_TYPE_ID);
250 assert_ne!(p.type_id(), 40);
252 assert_eq!(p.schema_fields().len(), 5);
253 }
254}