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