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