1use crate::plugin::{file_name, ArchiveTypePlugin, Col, ExtensionRow, ExtensionValue, HandlerSpec};
28use std::collections::HashMap;
29
30fn parse_coords(path: &str, extensions: &[&str]) -> (String, Option<String>) {
37 let filename = file_name(path);
38 let stem = extensions
39 .iter()
40 .find_map(|ext| filename.strip_suffix(ext))
41 .unwrap_or(filename);
42
43 let mut split_pos = None;
44 for (i, c) in stem.char_indices() {
45 if matches!(c, '-' | '_' | '@') {
46 if let Some(next) = stem[i + 1..].chars().next() {
47 if next.is_ascii_digit() {
48 split_pos = Some(i);
49 }
50 }
51 }
52 }
53
54 match split_pos {
55 Some(pos) => (stem[..pos].to_string(), Some(stem[pos + 1..].to_string())),
56 None => (stem.to_string(), None),
57 }
58}
59
60macro_rules! skeleton_handler {
63 (
64 struct: $struct:ident,
65 name: $name:literal,
66 type_id: $tid:literal,
67 aliases: [$($alias:literal),* $(,)?],
68 ecosystem: $eco:literal,
69 extensions: [$($ext:literal),* $(,)?],
70 description: $desc:literal $(,)?
71 ) => {
72 #[doc = concat!("Skeleton handler for the ", $eco, " ecosystem (stub).")]
73 pub struct $struct;
74
75 impl ArchiveTypePlugin for $struct {
76 fn spec(&self) -> &HandlerSpec {
77 static SPEC: HandlerSpec = HandlerSpec {
78 name: $name,
79 aliases: &[$($alias),*],
80 type_id: $tid,
81 ecosystem: $eco,
82 extensions: &[$($ext),*],
83 description: concat!($desc, " (skeleton — filename-only)"),
84 fields: SKELETON_FIELDS,
85 commands: &[],
86 };
87 &SPEC
88 }
89
90 fn extract_metadata(&self, path: &str, _data: &[u8]) -> Option<ExtensionRow> {
91 skeleton_row(path, self.spec().extensions)
92 }
93 }
94 };
95}
96
97const SKELETON_FIELDS: &[(&str, Col)] = &[("name", Col::Utf8), ("version", Col::Utf8)];
99
100fn skeleton_row(path: &str, extensions: &[&str]) -> Option<ExtensionRow> {
101 let (name, version) = parse_coords(path, extensions);
102 let mut fields = HashMap::new();
103 fields.insert("name".into(), ExtensionValue::Str(name));
104 fields.insert("version".into(), ExtensionValue::OptStr(version));
105 Some(ExtensionRow { fields })
106}
107
108skeleton_handler! {
109 struct: GoPlugin,
110 name: "go",
111 type_id: 4,
112 aliases: ["golang"],
113 ecosystem: "Go / Go modules (proxy.golang.org)",
114 extensions: [".zip", ".mod", ".info"],
115 description: "Go module zips published to the module proxy",
116}
117
118skeleton_handler! {
119 struct: NugetPlugin,
120 name: "nuget",
121 type_id: 5,
122 aliases: ["dotnet", ".net"],
123 ecosystem: ".NET / NuGet (nuget.org)",
124 extensions: [".nupkg", ".snupkg"],
125 description: ".NET NuGet packages (zip with a .nuspec manifest)",
126}
127
128skeleton_handler! {
157 struct: FlatpakPlugin,
158 name: "flatpak",
159 type_id: 10,
160 aliases: ["flatpakref"],
161 ecosystem: "Flatpak bundles (Flathub)",
162 extensions: [".flatpak", ".flatpakref"],
163 description: "Flatpak single-file application bundles (OSTree)",
164}
165
166skeleton_handler! {
182 struct: DockerPlugin,
183 name: "docker",
184 type_id: 12,
185 aliases: ["oci", "container"],
186 ecosystem: "OCI / Docker container images",
187 extensions: [".oci", ".docker"],
188 description: "OCI image layouts / docker save tarballs (manifest + layers)",
189}
190
191skeleton_handler! {
192 struct: HelmPlugin,
193 name: "helm",
194 type_id: 13,
195 aliases: ["chart", "k8s"],
196 ecosystem: "Helm charts (Kubernetes / Artifact Hub)",
197 extensions: [".tgz"],
198 description: "Helm chart archives (Chart.yaml + templates)",
199}
200
201skeleton_handler! {
207 struct: SnapPlugin,
208 name: "snap",
209 type_id: 15,
210 aliases: ["snapcraft", "snapd"],
211 ecosystem: "Snap packages (Snapcraft / Snap Store)",
212 extensions: [".snap"],
213 description: "Snap application packages (squashfs image)",
214}
215
216skeleton_handler! {
217 struct: AppImagePlugin,
218 name: "appimage",
219 type_id: 16,
220 aliases: ["appdir"],
221 ecosystem: "AppImage portable Linux applications",
222 extensions: [".AppImage", ".appimage"],
223 description: "AppImage self-mounting application bundles (ELF + squashfs)",
224}
225
226skeleton_handler! {
227 struct: ComposerPlugin,
228 name: "composer",
229 type_id: 17,
230 aliases: ["php", "packagist"],
231 ecosystem: "PHP / Composer (Packagist)",
232 extensions: [".zip", ".phar"],
233 description: "Composer/PHP packages (zip with composer.json, or PHAR)",
234}
235
236skeleton_handler! {
237 struct: HexPlugin,
238 name: "hex",
239 type_id: 18,
240 aliases: ["elixir", "erlang", "mix"],
241 ecosystem: "Erlang / Elixir (hex.pm)",
242 extensions: [".tar"],
243 description: "Hex packages (tar of metadata + contents.tar.gz)",
244}
245
246skeleton_handler! {
247 struct: CabalPlugin,
248 name: "cabal",
249 type_id: 19,
250 aliases: ["haskell", "hackage"],
251 ecosystem: "Haskell / Cabal (Hackage)",
252 extensions: [".tar.gz"],
253 description: "Cabal source packages (sdist tarball with a .cabal file)",
254}
255
256skeleton_handler! {
257 struct: SwiftPlugin,
258 name: "swift",
259 type_id: 20,
260 aliases: ["swiftpm", "spm"],
261 ecosystem: "Swift / Swift Package Manager",
262 extensions: [".zip"],
263 description: "Swift package archives (Package.swift + sources)",
264}
265
266skeleton_handler! {
267 struct: ParquetPlugin,
268 name: "parquet",
269 type_id: 21,
270 aliases: ["geoparquet", "pq"],
271 ecosystem: "Apache Parquet / GeoParquet columnar data",
272 extensions: [".parquet", ".geoparquet"],
273 description: "Parquet column files (GeoParquet adds a geo metadata key)",
274}
275
276skeleton_handler! {
277 struct: DatasetPlugin,
278 name: "dataset",
279 type_id: 22,
280 aliases: ["hf", "huggingface", "hub"],
281 ecosystem: "ML datasets (Hugging Face Hub / Croissant)",
282 extensions: [".dataset", ".hf"],
283 description: "Versioned ML dataset bundles (data shards + dataset card)",
284}
285
286skeleton_handler! {
287 struct: ArrowPlugin,
288 name: "arrow",
289 type_id: 23,
290 aliases: ["ipc", "feather"],
291 ecosystem: "Apache Arrow IPC / Feather",
292 extensions: [".arrow", ".feather", ".ipc"],
293 description: "Arrow IPC record-batch files (zero-copy columnar)",
294}
295
296skeleton_handler! {
297 struct: GeoJsonPlugin,
298 name: "geojson",
299 type_id: 24,
300 aliases: ["geo", "gis", "shapefile"],
301 ecosystem: "GIS vector data (GeoJSON / Shapefile / GeoPackage)",
302 extensions: [".geojson", ".gpkg", ".shp", ".fgb"],
303 description: "Geospatial vector datasets (features + CRS)",
304}
305
306pub fn skeleton_handlers() -> Vec<Box<dyn ArchiveTypePlugin>> {
309 vec![
310 Box::new(GoPlugin),
311 Box::new(NugetPlugin),
312 Box::new(FlatpakPlugin),
313 Box::new(DockerPlugin),
314 Box::new(HelmPlugin),
315 Box::new(SnapPlugin),
316 Box::new(AppImagePlugin),
317 Box::new(ComposerPlugin),
318 Box::new(HexPlugin),
319 Box::new(CabalPlugin),
320 Box::new(SwiftPlugin),
321 Box::new(ParquetPlugin),
322 Box::new(DatasetPlugin),
323 Box::new(ArrowPlugin),
324 Box::new(GeoJsonPlugin),
325 ]
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331
332 #[test]
333 fn parse_coords_splits_name_and_version() {
334 assert_eq!(
335 parse_coords("foo/bar-1.2.3.nupkg", &[".nupkg"]),
336 ("bar".to_string(), Some("1.2.3".to_string()))
337 );
338 assert_eq!(
339 parse_coords("lodash-4.17.21.tgz", &[".tgz"]),
340 ("lodash".to_string(), Some("4.17.21".to_string()))
341 );
342 }
343
344 #[test]
345 fn parse_coords_handles_no_version() {
346 assert_eq!(
347 parse_coords("/usr/bin/ls.elf", &[".elf"]),
348 ("ls".to_string(), None)
349 );
350 }
351
352 #[test]
353 fn skeletons_have_unique_sequential_type_ids() {
354 let mut ids: Vec<i8> = skeleton_handlers().iter().map(|h| h.type_id()).collect();
355 let mut sorted = ids.clone();
356 sorted.sort();
357 sorted.dedup();
358 assert_eq!(ids.len(), sorted.len(), "type_ids must be unique");
359 ids.sort();
360 assert_eq!(*ids.first().unwrap(), 4, "skeletons start at type_id 4");
361 assert_eq!(*ids.last().unwrap(), 24);
362 }
363
364 #[test]
365 fn every_skeleton_matches_and_extracts() {
366 for h in skeleton_handlers() {
367 let m = h.meta();
368 assert!(!m.extensions.is_empty(), "{} has no extensions", m.name);
369 let sample = format!("pkg-1.0{}", m.extensions[0]);
370 assert!(h.matches_path(&sample), "{} should match {}", m.name, sample);
371 let row = h.extract_metadata(&sample, &[]).expect("row");
372 assert!(row.fields.contains_key("name"));
373 }
374 }
375}