Skip to main content

provenant/assembly/
assemblers.rs

1// SPDX-FileCopyrightText: nexB Inc. and others
2// SPDX-FileCopyrightText: Provenant contributors
3// SPDX-License-Identifier: Apache-2.0
4// Derived from ScanCode Toolkit (Apache-2.0); modified. See NOTICE.
5
6use std::collections::HashSet;
7
8use crate::models::PackageType;
9use crate::models::{DatasourceId, FileInfo, Package, TopLevelDependency};
10use strum::EnumIter;
11
12use super::{
13    AssemblerConfig, AssemblyMode, DirectoryMergeOutput, bazel_merge, bazel_prune,
14    cargo_resource_assign, composer_resource_assign, conda_rootfs_merge, debian_source_merge,
15    file_ref_resolve, hackage_merge, nix_flake_compat_merge, npm_resource_assign,
16    nuget_cpm_resolve, python_requirements_assign, ruby_resource_assign, swift_merge, topology,
17};
18
19#[derive(Clone, Copy)]
20pub(super) enum SpecialDirectoryMergerKind {
21    Skip,
22    Bazel,
23    DebianSource,
24    Hackage,
25    WindowsUpdate,
26}
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, EnumIter)]
29pub(super) enum PostAssemblyPassKind {
30    SwiftMerge,
31    CondaRootfsMerge,
32    NpmResourceAssign,
33    PythonRequirementsAssign,
34    FileReferenceResolve,
35    RpmYumdbMerge,
36    NpmWorkspaceMerge,
37    CargoWorkspaceMerge,
38    NugetCpmResolve,
39    CargoResourceAssign,
40    ComposerResourceAssign,
41    RubyResourceAssign,
42    NixFlakeCompatMerge,
43    BazelPrune,
44}
45
46pub(super) fn special_directory_merger_for(
47    config_key: DatasourceId,
48) -> Option<SpecialDirectoryMergerKind> {
49    match config_key {
50        DatasourceId::BazelBuild => Some(SpecialDirectoryMergerKind::Bazel),
51        DatasourceId::DebianControlInSource => Some(SpecialDirectoryMergerKind::DebianSource),
52        DatasourceId::HackageCabal => Some(SpecialDirectoryMergerKind::Hackage),
53        DatasourceId::MicrosoftUpdateManifestMum => Some(SpecialDirectoryMergerKind::WindowsUpdate),
54        DatasourceId::SwiftPackageManifestJson => Some(SpecialDirectoryMergerKind::Skip),
55        _ => None,
56    }
57}
58
59pub(super) static POST_ASSEMBLY_PASSES: &[PostAssemblyPassKind] = &[
60    PostAssemblyPassKind::SwiftMerge,
61    PostAssemblyPassKind::CondaRootfsMerge,
62    PostAssemblyPassKind::NpmResourceAssign,
63    PostAssemblyPassKind::PythonRequirementsAssign,
64    PostAssemblyPassKind::FileReferenceResolve,
65    PostAssemblyPassKind::RpmYumdbMerge,
66    PostAssemblyPassKind::NpmWorkspaceMerge,
67    PostAssemblyPassKind::CargoWorkspaceMerge,
68    PostAssemblyPassKind::NugetCpmResolve,
69    PostAssemblyPassKind::CargoResourceAssign,
70    PostAssemblyPassKind::ComposerResourceAssign,
71    PostAssemblyPassKind::RubyResourceAssign,
72    PostAssemblyPassKind::NixFlakeCompatMerge,
73    PostAssemblyPassKind::BazelPrune,
74];
75
76const SWIFT_POST_ASSEMBLY_DATASOURCE_IDS: &[DatasourceId] = &[
77    DatasourceId::SwiftPackageManifestJson,
78    DatasourceId::SwiftPackageResolved,
79    DatasourceId::SwiftPackageShowDependencies,
80];
81
82const CONDA_ROOTFS_POST_ASSEMBLY_DATASOURCE_IDS: &[DatasourceId] =
83    &[DatasourceId::CondaMetaJson, DatasourceId::CondaMetaYaml];
84
85const RPM_INSTALLED_DATABASE_DATASOURCE_IDS: &[DatasourceId] = &[
86    DatasourceId::RpmInstalledDatabaseBdb,
87    DatasourceId::RpmInstalledDatabaseNdb,
88    DatasourceId::RpmInstalledDatabaseSqlite,
89];
90
91const NUGET_CPM_CONFIG_DATASOURCE_IDS: &[DatasourceId] = &[
92    DatasourceId::NugetDirectoryBuildProps,
93    DatasourceId::NugetDirectoryPackagesProps,
94];
95
96const NUGET_CPM_PROJECT_DATASOURCE_IDS: &[DatasourceId] = &[
97    DatasourceId::NugetCsproj,
98    DatasourceId::NugetFsproj,
99    DatasourceId::NugetVbproj,
100];
101
102#[derive(Default)]
103struct PostAssemblyInputs {
104    package_types: HashSet<PackageType>,
105    file_datasource_ids: HashSet<DatasourceId>,
106    has_npm_workspace_markers: bool,
107    has_cargo_workspace_markers: bool,
108}
109
110pub(super) fn run_post_assembly_passes(
111    files: &mut [FileInfo],
112    packages: &mut Vec<Package>,
113    dependencies: &mut Vec<TopLevelDependency>,
114    topology_plan: &topology::TopologyPlan,
115) {
116    let inputs = PostAssemblyInputs::collect(files, packages);
117
118    for pass in POST_ASSEMBLY_PASSES {
119        if !pass.should_run(&inputs) {
120            continue;
121        }
122
123        pass.run(files, packages, dependencies, topology_plan);
124    }
125}
126
127impl PostAssemblyInputs {
128    fn collect(files: &[FileInfo], packages: &[Package]) -> Self {
129        let mut inputs = Self {
130            package_types: packages
131                .iter()
132                .filter_map(|package| package.package_type)
133                .collect(),
134            ..Self::default()
135        };
136
137        for file in files {
138            for package_data in &file.package_data {
139                let Some(datasource_id) = package_data.datasource_id else {
140                    continue;
141                };
142
143                inputs.file_datasource_ids.insert(datasource_id);
144
145                if matches!(
146                    datasource_id,
147                    DatasourceId::NpmPackageJson | DatasourceId::PnpmWorkspaceYaml
148                ) && package_data
149                    .extra_data
150                    .as_ref()
151                    .is_some_and(|extra_data| extra_data.contains_key("workspaces"))
152                {
153                    inputs.has_npm_workspace_markers = true;
154                }
155
156                if datasource_id == DatasourceId::CargoToml
157                    && package_data
158                        .extra_data
159                        .as_ref()
160                        .and_then(|extra_data| extra_data.get("workspace"))
161                        .and_then(|workspace| workspace.get("members"))
162                        .and_then(|members| members.as_array())
163                        .is_some_and(|members| !members.is_empty())
164                {
165                    inputs.has_cargo_workspace_markers = true;
166                }
167            }
168        }
169
170        inputs
171    }
172
173    fn has_package_type(&self, package_type: PackageType) -> bool {
174        self.package_types.contains(&package_type)
175    }
176
177    fn has_any_file_datasource(&self, datasource_ids: &[DatasourceId]) -> bool {
178        datasource_ids
179            .iter()
180            .any(|datasource_id| self.file_datasource_ids.contains(datasource_id))
181    }
182
183    fn has_all_file_datasources(&self, datasource_ids: &[DatasourceId]) -> bool {
184        datasource_ids
185            .iter()
186            .all(|datasource_id| self.file_datasource_ids.contains(datasource_id))
187    }
188}
189
190impl SpecialDirectoryMergerKind {
191    pub(super) fn run(
192        self,
193        config: &AssemblerConfig,
194        files: &[FileInfo],
195        file_indices: &[usize],
196    ) -> Vec<DirectoryMergeOutput> {
197        match self {
198            Self::Skip => Vec::new(),
199            Self::Bazel => bazel_merge::assemble_bazel_packages(config, files, file_indices),
200            Self::DebianSource => {
201                debian_source_merge::assemble_debian_source_packages(config, files, file_indices)
202            }
203            Self::Hackage => hackage_merge::assemble_hackage_packages(files, file_indices),
204            Self::WindowsUpdate => super::windows_update_merge::assemble_windows_update_packages(
205                config,
206                files,
207                file_indices,
208            ),
209        }
210    }
211}
212
213impl PostAssemblyPassKind {
214    fn should_run(self, inputs: &PostAssemblyInputs) -> bool {
215        match self {
216            Self::SwiftMerge => inputs.has_any_file_datasource(SWIFT_POST_ASSEMBLY_DATASOURCE_IDS),
217            Self::CondaRootfsMerge => {
218                inputs.has_all_file_datasources(CONDA_ROOTFS_POST_ASSEMBLY_DATASOURCE_IDS)
219            }
220            Self::NpmResourceAssign => inputs.has_package_type(PackageType::Npm),
221            Self::PythonRequirementsAssign => {
222                inputs.has_package_type(PackageType::Pypi)
223                    && inputs.has_any_file_datasource(&[DatasourceId::PipRequirements])
224            }
225            Self::FileReferenceResolve => {
226                file_ref_resolve::has_relevant_file_reference_datasource_ids(
227                    &inputs.file_datasource_ids,
228                )
229            }
230            Self::RpmYumdbMerge => {
231                inputs.has_any_file_datasource(&[DatasourceId::RpmYumdb])
232                    && inputs.has_any_file_datasource(RPM_INSTALLED_DATABASE_DATASOURCE_IDS)
233            }
234            Self::NpmWorkspaceMerge => inputs.has_npm_workspace_markers,
235            Self::CargoWorkspaceMerge => inputs.has_cargo_workspace_markers,
236            Self::NugetCpmResolve => {
237                inputs.has_any_file_datasource(NUGET_CPM_CONFIG_DATASOURCE_IDS)
238                    && inputs.has_any_file_datasource(NUGET_CPM_PROJECT_DATASOURCE_IDS)
239            }
240            Self::CargoResourceAssign => inputs.has_package_type(PackageType::Cargo),
241            Self::ComposerResourceAssign => inputs.has_package_type(PackageType::Composer),
242            Self::RubyResourceAssign => inputs.has_package_type(PackageType::Gem),
243            Self::NixFlakeCompatMerge => {
244                inputs.has_any_file_datasource(&[DatasourceId::NixDefaultNix])
245                    && inputs.has_any_file_datasource(&[
246                        DatasourceId::NixFlakeNix,
247                        DatasourceId::NixFlakeLock,
248                    ])
249            }
250            Self::BazelPrune => inputs.has_package_type(PackageType::Bazel),
251        }
252    }
253
254    fn run(
255        self,
256        files: &mut [FileInfo],
257        packages: &mut Vec<Package>,
258        dependencies: &mut Vec<TopLevelDependency>,
259        topology_plan: &topology::TopologyPlan,
260    ) {
261        match self {
262            Self::SwiftMerge => swift_merge::assemble_swift_packages(files, packages, dependencies),
263            Self::CondaRootfsMerge => {
264                conda_rootfs_merge::merge_conda_rootfs_metadata(files, packages, dependencies)
265            }
266            Self::NpmResourceAssign => {
267                npm_resource_assign::assign_npm_package_resources(files, packages)
268            }
269            Self::PythonRequirementsAssign => {
270                python_requirements_assign::assign_python_requirements_to_projects(
271                    files,
272                    packages,
273                    dependencies,
274                )
275            }
276            Self::FileReferenceResolve => {
277                file_ref_resolve::resolve_file_references(files, packages, dependencies)
278            }
279            Self::RpmYumdbMerge => file_ref_resolve::merge_rpm_yumdb_metadata(files, packages),
280            Self::NpmWorkspaceMerge => {
281                topology_plan.apply_npm_workspace_domains(files, packages, dependencies)
282            }
283            Self::CargoWorkspaceMerge => {
284                topology_plan.apply_cargo_workspace_domains(files, packages, dependencies)
285            }
286            Self::NugetCpmResolve => {
287                nuget_cpm_resolve::resolve_nuget_cpm_versions(files, dependencies)
288            }
289            Self::CargoResourceAssign => {
290                cargo_resource_assign::assign_cargo_package_resources(files, packages)
291            }
292            Self::ComposerResourceAssign => {
293                composer_resource_assign::assign_composer_package_resources(files, packages)
294            }
295            Self::RubyResourceAssign => {
296                ruby_resource_assign::assign_ruby_package_resources(files, packages)
297            }
298            Self::NixFlakeCompatMerge => {
299                nix_flake_compat_merge::attach_flake_compat_default_files(files, packages)
300            }
301            Self::BazelPrune => {
302                bazel_prune::prune_unused_bazel_packages(files, packages, dependencies)
303            }
304        }
305    }
306}
307
308pub static ASSEMBLERS: &[AssemblerConfig] = &[
309    // ── Sibling-merge assemblers ──
310    //
311    // npm ecosystem: package.json + lockfiles in same directory.
312    // NOTE: npm-shrinkwrap.json emits "npm_package_lock_json" as its datasource_id,
313    // so "npm_shrinkwrap_json" is NOT a real datasource_id.
314    AssemblerConfig {
315        datasource_ids: &[
316            DatasourceId::BunLock,
317            DatasourceId::BunLockb,
318            DatasourceId::NpmPackageJson,
319            DatasourceId::NpmPackageLockJson,
320            DatasourceId::YarnLock,
321            DatasourceId::YarnLockV1,
322            DatasourceId::YarnLockV2,
323            DatasourceId::YarnPnpCjs,
324            DatasourceId::PnpmLockYaml,
325            DatasourceId::PnpmWorkspaceYaml,
326        ],
327        sibling_file_patterns: &[
328            "package.json",
329            "bun.lock",
330            "bun.lockb",
331            ".package-lock.json",
332            "package-lock.json",
333            ".npm-shrinkwrap.json",
334            "npm-shrinkwrap.json",
335            "yarn.lock",
336            ".pnp.cjs",
337            "pnpm-lock.yaml",
338            "shrinkwrap.yaml",
339            "pnpm-workspace.yaml",
340        ],
341        mode: AssemblyMode::SiblingMerge,
342    },
343    // Rust/Cargo ecosystem
344    AssemblerConfig {
345        datasource_ids: &[DatasourceId::CargoToml, DatasourceId::CargoLock],
346        sibling_file_patterns: &["Cargo.toml", "Cargo.lock"],
347        mode: AssemblyMode::SiblingMerge,
348    },
349    // Julia ecosystem
350    AssemblerConfig {
351        datasource_ids: &[
352            DatasourceId::JuliaProjectToml,
353            DatasourceId::JuliaManifestToml,
354        ],
355        sibling_file_patterns: &["Project.toml", "Manifest.toml"],
356        mode: AssemblyMode::SiblingMerge,
357    },
358    // Erlang/OTP Rebar ecosystem
359    AssemblerConfig {
360        datasource_ids: &[DatasourceId::RebarConfig, DatasourceId::RebarLock],
361        sibling_file_patterns: &["rebar.config", "rebar.lock"],
362        mode: AssemblyMode::SiblingMerge,
363    },
364    // Carthage ecosystem
365    AssemblerConfig {
366        datasource_ids: &[
367            DatasourceId::CarthageCartfile,
368            DatasourceId::CarthageCartfileResolved,
369        ],
370        sibling_file_patterns: &["Cartfile", "Cartfile.private", "Cartfile.resolved"],
371        mode: AssemblyMode::SiblingMerge,
372    },
373    // CocoaPods ecosystem
374    AssemblerConfig {
375        datasource_ids: &[
376            DatasourceId::CocoapodsPodspec,
377            DatasourceId::CocoapodsPodspecJson,
378            DatasourceId::CocoapodsPodfile,
379            DatasourceId::CocoapodsPodfileLock,
380        ],
381        sibling_file_patterns: &["*.podspec", "*.podspec.json", "Podfile", "Podfile.lock"],
382        mode: AssemblyMode::SiblingMerge,
383    },
384    // PHP Composer ecosystem
385    AssemblerConfig {
386        datasource_ids: &[DatasourceId::PhpComposerJson, DatasourceId::PhpComposerLock],
387        sibling_file_patterns: &[
388            "*composer.json",
389            "composer.*.json",
390            "*composer.lock",
391            "composer.*.lock",
392        ],
393        mode: AssemblyMode::SiblingMerge,
394    },
395    // Go ecosystem (includes legacy Godeps)
396    AssemblerConfig {
397        datasource_ids: &[
398            DatasourceId::GoMod,
399            DatasourceId::GoModGraph,
400            DatasourceId::GoSum,
401            DatasourceId::GoWork,
402            DatasourceId::Godeps,
403        ],
404        sibling_file_patterns: &[
405            "go.mod",
406            "go.work",
407            "go.mod.graph",
408            "go.modgraph",
409            "go.sum",
410            "Godeps.json",
411        ],
412        mode: AssemblyMode::SiblingMerge,
413    },
414    // Dart/Flutter ecosystem
415    AssemblerConfig {
416        datasource_ids: &[DatasourceId::PubspecYaml, DatasourceId::PubspecLock],
417        sibling_file_patterns: &["pubspec.yaml", "pubspec.lock"],
418        mode: AssemblyMode::SiblingMerge,
419    },
420    // Pixi ecosystem
421    AssemblerConfig {
422        datasource_ids: &[DatasourceId::PixiToml, DatasourceId::PixiLock],
423        sibling_file_patterns: &["pixi.toml", "pixi.lock"],
424        mode: AssemblyMode::SiblingMerge,
425    },
426    AssemblerConfig {
427        datasource_ids: &[DatasourceId::NixFlakeNix, DatasourceId::NixFlakeLock],
428        sibling_file_patterns: &["flake.nix", "flake.lock"],
429        mode: AssemblyMode::SiblingMerge,
430    },
431    AssemblerConfig {
432        datasource_ids: &[DatasourceId::NixDefaultNix],
433        sibling_file_patterns: &["default.nix"],
434        mode: AssemblyMode::OnePerPackageData,
435    },
436    // Helm chart ecosystem
437    AssemblerConfig {
438        datasource_ids: &[DatasourceId::HelmChartYaml, DatasourceId::HelmChartLock],
439        sibling_file_patterns: &["Chart.yaml", "Chart.lock"],
440        mode: AssemblyMode::SiblingMerge,
441    },
442    AssemblerConfig {
443        datasource_ids: &[
444            DatasourceId::HackageCabal,
445            DatasourceId::HackageCabalProject,
446            DatasourceId::HackageStackYaml,
447        ],
448        sibling_file_patterns: &["*.cabal", "cabal.project", "stack.yaml"],
449        mode: AssemblyMode::SiblingMerge,
450    },
451    // Chef ecosystem
452    AssemblerConfig {
453        datasource_ids: &[
454            DatasourceId::ChefCookbookMetadataJson,
455            DatasourceId::ChefCookbookMetadataRb,
456        ],
457        sibling_file_patterns: &["metadata.json", "metadata.rb"],
458        mode: AssemblyMode::SiblingMerge,
459    },
460    // Conan (C/C++) ecosystem
461    AssemblerConfig {
462        datasource_ids: &[
463            DatasourceId::ConanConanFilePy,
464            DatasourceId::ConanConanFileTxt,
465            DatasourceId::ConanLock,
466            DatasourceId::ConanConanDataYml,
467        ],
468        sibling_file_patterns: &[
469            "conanfile.py",
470            "conanfile.txt",
471            "conan.lock",
472            "conandata.yml",
473        ],
474        mode: AssemblyMode::SiblingMerge,
475    },
476    // Maven/Java ecosystem (nested merge via META-INF)
477    AssemblerConfig {
478        datasource_ids: &[
479            DatasourceId::MavenPom,
480            DatasourceId::MavenPomProperties,
481            DatasourceId::JavaJarManifest,
482            DatasourceId::JavaOsgiManifest,
483        ],
484        sibling_file_patterns: &[
485            "pom.xml",
486            "*.pom",
487            "pom.properties",
488            "**/META-INF/MANIFEST.MF",
489        ],
490        mode: AssemblyMode::SiblingMerge,
491    },
492    AssemblerConfig {
493        datasource_ids: &[DatasourceId::PypiWheel, DatasourceId::PypiPipOriginJson],
494        sibling_file_patterns: &["*.whl", "origin.json"],
495        mode: AssemblyMode::SiblingMerge,
496    },
497    // Python/PyPI ecosystem
498    AssemblerConfig {
499        datasource_ids: &[
500            DatasourceId::PypiPyprojectToml,
501            DatasourceId::PypiPoetryPyprojectToml,
502            DatasourceId::PypiSetupPy,
503            DatasourceId::PypiSetupCfg,
504            DatasourceId::PypiWheelMetadata,
505            DatasourceId::PypiEgg,
506            DatasourceId::PypiEggPkginfo,
507            DatasourceId::PypiEditableEggPkginfo,
508            DatasourceId::PypiJson,
509            DatasourceId::PypiSdist,
510            DatasourceId::PypiSdistPkginfo,
511            DatasourceId::PypiInspectDeplock,
512            DatasourceId::PipRequirements,
513            DatasourceId::PypiPoetryLock,
514            DatasourceId::PypiPylockToml,
515            DatasourceId::PypiUvLock,
516            DatasourceId::Pipfile,
517            DatasourceId::PipfileLock,
518        ],
519        sibling_file_patterns: &[
520            "pyproject.toml",
521            "setup.py",
522            "setup.cfg",
523            "PKG-INFO",
524            "METADATA",
525            "pypi.json",
526            "pip-inspect.deplock",
527            "*.tar.gz",
528            "*.tgz",
529            "*.tar.bz2",
530            "*.tar.xz",
531            "*.zip",
532            "requirements*.txt",
533            "Pipfile",
534            "Pipfile.lock",
535            "poetry.lock",
536            "pylock.toml",
537            "pylock.*.toml",
538            "uv.lock",
539        ],
540        mode: AssemblyMode::SiblingMerge,
541    },
542    AssemblerConfig {
543        datasource_ids: &[DatasourceId::DenoJson, DatasourceId::DenoLock],
544        sibling_file_patterns: &["deno.json", "deno.jsonc", "deno.lock"],
545        mode: AssemblyMode::SiblingMerge,
546    },
547    // Ruby/RubyGems ecosystem
548    AssemblerConfig {
549        datasource_ids: &[
550            DatasourceId::GemArchiveExtracted,
551            DatasourceId::Gemspec,
552            DatasourceId::GemspecExtracted,
553            DatasourceId::Gemfile,
554            DatasourceId::GemfileExtracted,
555            DatasourceId::GemfileLock,
556            DatasourceId::GemfileLockExtracted,
557        ],
558        sibling_file_patterns: &[
559            "metadata.gz-extract",
560            "**/data.gz-extract/*.gemspec",
561            "**/data.gz-extract/Gemfile",
562            "**/data.gz-extract/Gemfile.lock",
563            "*.gemspec",
564            "Gemfile",
565            "Gemfile.lock",
566        ],
567        mode: AssemblyMode::SiblingMerge,
568    },
569    AssemblerConfig {
570        datasource_ids: &[DatasourceId::GemArchive],
571        sibling_file_patterns: &["*.gem"],
572        mode: AssemblyMode::OnePerPackageData,
573    },
574    // Conda ecosystem
575    AssemblerConfig {
576        datasource_ids: &[
577            DatasourceId::CondaMetaYaml,
578            DatasourceId::CondaYaml,
579            DatasourceId::CondaMetaJson,
580        ],
581        sibling_file_patterns: &[
582            "meta.yaml",
583            "meta.yml",
584            "recipe.yaml",
585            "recipe.yml",
586            "environment.yml",
587            "environment.yaml",
588            "conda.yaml",
589            "conda.yml",
590            "*conda*.yaml",
591            "*conda*.yml",
592            "env.yaml",
593            "env.yml",
594            "*env*.yaml",
595            "*env*.yml",
596            "*environment*.yaml",
597            "*environment*.yml",
598            "*.json",
599        ],
600        mode: AssemblyMode::SiblingMerge,
601    },
602    // RPM specfile (source packages)
603    AssemblerConfig {
604        datasource_ids: &[DatasourceId::RpmSpecfile],
605        sibling_file_patterns: &["*.spec"],
606        mode: AssemblyMode::OnePerPackageData,
607    },
608    // Debian source packages (nested merge via debian/ directory)
609    AssemblerConfig {
610        datasource_ids: &[
611            DatasourceId::DebianControlInSource,
612            DatasourceId::DebianCopyrightInSource,
613        ],
614        sibling_file_patterns: &["control", "copyright"],
615        mode: AssemblyMode::SiblingMerge,
616    },
617    // Gradle/Android ecosystem
618    AssemblerConfig {
619        datasource_ids: &[DatasourceId::BuildGradle, DatasourceId::GradleLockfile],
620        sibling_file_patterns: &["build.gradle", "build.gradle.kts", "gradle.lockfile"],
621        mode: AssemblyMode::SiblingMerge,
622    },
623    AssemblerConfig {
624        datasource_ids: &[DatasourceId::GradleModule],
625        sibling_file_patterns: &["*.module"],
626        mode: AssemblyMode::OnePerPackageData,
627    },
628    // CPAN/Perl ecosystem
629    AssemblerConfig {
630        datasource_ids: &[
631            DatasourceId::CpanMetaJson,
632            DatasourceId::CpanMetaYml,
633            DatasourceId::CpanManifest,
634            DatasourceId::CpanDistIni,
635            DatasourceId::CpanMakefile,
636        ],
637        sibling_file_patterns: &[
638            "META.json",
639            "META.yml",
640            "MANIFEST",
641            "dist.ini",
642            "Makefile.PL",
643        ],
644        mode: AssemblyMode::SiblingMerge,
645    },
646    // NuGet/.NET ecosystem
647    AssemblerConfig {
648        datasource_ids: &[
649            DatasourceId::NugetCsproj,
650            DatasourceId::NugetFsproj,
651            DatasourceId::NugetNuspec,
652            DatasourceId::NugetNupkg,
653            DatasourceId::NugetProjectJson,
654            DatasourceId::NugetProjectLockJson,
655            DatasourceId::NugetPackagesConfig,
656            DatasourceId::NugetPackagesLock,
657            DatasourceId::NugetVbproj,
658        ],
659        sibling_file_patterns: &[
660            "*.csproj",
661            "*.fsproj",
662            "*.nuspec",
663            "*.nupkg",
664            "project.json",
665            "project.lock.json",
666            "packages.config",
667            "packages.lock.json",
668            "*.packages.lock.json",
669            "*.vbproj",
670        ],
671        mode: AssemblyMode::SiblingMerge,
672    },
673    AssemblerConfig {
674        datasource_ids: &[DatasourceId::NugetDepsJson],
675        sibling_file_patterns: &["*.deps.json"],
676        mode: AssemblyMode::OnePerPackageData,
677    },
678    // Swift/SPM ecosystem
679    AssemblerConfig {
680        datasource_ids: &[
681            DatasourceId::SwiftPackageManifestJson,
682            DatasourceId::SwiftPackageResolved,
683            DatasourceId::SwiftPackageShowDependencies,
684        ],
685        sibling_file_patterns: &[
686            "Package.swift.json",
687            "Package.swift.deplock",
688            "Package.resolved",
689            ".package.resolved",
690            "swift-show-dependencies.deplock",
691        ],
692        mode: AssemblyMode::SiblingMerge,
693    },
694    // ── Standalone assemblers (single file → single package) ──
695    //
696    // These ecosystems have only one manifest file type with no sibling merging.
697    // They still need configs so their datasource_ids are recognized by the assembler.
698    //
699    // Bower (JavaScript)
700    AssemblerConfig {
701        datasource_ids: &[DatasourceId::BowerJson],
702        sibling_file_patterns: &["bower.json"],
703        mode: AssemblyMode::SiblingMerge,
704    },
705    // CRAN (R language)
706    AssemblerConfig {
707        datasource_ids: &[DatasourceId::CranDescription],
708        sibling_file_patterns: &["DESCRIPTION"],
709        mode: AssemblyMode::SiblingMerge,
710    },
711    // FreeBSD packages
712    AssemblerConfig {
713        datasource_ids: &[DatasourceId::FreebsdCompactManifest],
714        sibling_file_patterns: &["+COMPACT_MANIFEST"],
715        mode: AssemblyMode::SiblingMerge,
716    },
717    // Haxe ecosystem
718    AssemblerConfig {
719        datasource_ids: &[DatasourceId::HaxelibJson],
720        sibling_file_patterns: &["haxelib.json"],
721        mode: AssemblyMode::SiblingMerge,
722    },
723    AssemblerConfig {
724        datasource_ids: &[DatasourceId::Gitmodules],
725        sibling_file_patterns: &[".gitmodules"],
726        mode: AssemblyMode::SiblingMerge,
727    },
728    // OCaml/opam ecosystem
729    AssemblerConfig {
730        datasource_ids: &[DatasourceId::OpamFile],
731        sibling_file_patterns: &["opam", "*.opam"],
732        mode: AssemblyMode::SiblingMerge,
733    },
734    AssemblerConfig {
735        datasource_ids: &[DatasourceId::RpmYumdb],
736        sibling_file_patterns: &["**/var/lib/yum/yumdb/*/*/from_repo"],
737        mode: AssemblyMode::OnePerPackageData,
738    },
739    // Microsoft Update Manifest
740    AssemblerConfig {
741        datasource_ids: &[DatasourceId::MicrosoftUpdateManifestMum],
742        sibling_file_patterns: &["*.mum"],
743        mode: AssemblyMode::SiblingMerge,
744    },
745    // Autotools (C/C++ build system)
746    AssemblerConfig {
747        datasource_ids: &[DatasourceId::AutotoolsConfigure],
748        sibling_file_patterns: &["configure", "configure.ac"],
749        mode: AssemblyMode::SiblingMerge,
750    },
751    // Bazel (build system)
752    AssemblerConfig {
753        datasource_ids: &[DatasourceId::BazelBuild],
754        sibling_file_patterns: &["BUILD"],
755        mode: AssemblyMode::SiblingMerge,
756    },
757    AssemblerConfig {
758        datasource_ids: &[DatasourceId::BazelModule],
759        sibling_file_patterns: &["MODULE.bazel"],
760        mode: AssemblyMode::OnePerPackageData,
761    },
762    // Buck (build system)
763    AssemblerConfig {
764        datasource_ids: &[DatasourceId::BuckFile, DatasourceId::BuckMetadata],
765        sibling_file_patterns: &["BUCK", "METADATA.bzl", ".buckconfig"],
766        mode: AssemblyMode::SiblingMerge,
767    },
768    // Ant/Ivy (Java dependency management)
769    AssemblerConfig {
770        datasource_ids: &[DatasourceId::AntIvyXml],
771        sibling_file_patterns: &["ivy.xml"],
772        mode: AssemblyMode::SiblingMerge,
773    },
774    // Meteor (JavaScript platform)
775    AssemblerConfig {
776        datasource_ids: &[DatasourceId::MeteorPackage],
777        sibling_file_patterns: &["package.js"],
778        mode: AssemblyMode::SiblingMerge,
779    },
780    // ── One-per-PackageData assemblers (database files with many packages) ──
781    //
782    // Alpine installed package database
783    AssemblerConfig {
784        datasource_ids: &[DatasourceId::AlpineInstalledDb],
785        sibling_file_patterns: &["installed"],
786        mode: AssemblyMode::OnePerPackageData,
787    },
788    AssemblerConfig {
789        datasource_ids: &[DatasourceId::AlpineApkbuild],
790        sibling_file_patterns: &["APKBUILD"],
791        mode: AssemblyMode::SiblingMerge,
792    },
793    // RPM installed package databases (BDB, NDB, SQLite)
794    AssemblerConfig {
795        datasource_ids: &[
796            DatasourceId::RpmInstalledDatabaseBdb,
797            DatasourceId::RpmInstalledDatabaseNdb,
798            DatasourceId::RpmInstalledDatabaseSqlite,
799            DatasourceId::RpmMarinerManifest,
800        ],
801        sibling_file_patterns: &[
802            "Packages",
803            "Packages.db",
804            "rpmdb.sqlite",
805            "container-manifest-2",
806        ],
807        mode: AssemblyMode::OnePerPackageData,
808    },
809    AssemblerConfig {
810        datasource_ids: &[DatasourceId::RpmArchive],
811        sibling_file_patterns: &["*.rpm", "*.srpm"],
812        mode: AssemblyMode::OnePerPackageData,
813    },
814    // Debian installed package databases
815    AssemblerConfig {
816        datasource_ids: &[DatasourceId::DebianDeb],
817        sibling_file_patterns: &["*.deb"],
818        mode: AssemblyMode::OnePerPackageData,
819    },
820    AssemblerConfig {
821        datasource_ids: &[
822            DatasourceId::DebianInstalledStatusDb,
823            DatasourceId::DebianDistrolessInstalledDb,
824        ],
825        sibling_file_patterns: &["status"],
826        mode: AssemblyMode::OnePerPackageData,
827    },
828    AssemblerConfig {
829        datasource_ids: &[
830            DatasourceId::DebianControlExtractedDeb,
831            DatasourceId::DebianMd5SumsInExtractedDeb,
832        ],
833        sibling_file_patterns: &["control", "md5sums"],
834        mode: AssemblyMode::SiblingMerge,
835    },
836    AssemblerConfig {
837        datasource_ids: &[DatasourceId::DebianSourceControlDsc],
838        sibling_file_patterns: &["*.dsc"],
839        mode: AssemblyMode::OnePerPackageData,
840    },
841    AssemblerConfig {
842        datasource_ids: &[DatasourceId::AboutFile],
843        sibling_file_patterns: &["*.ABOUT"],
844        mode: AssemblyMode::OnePerPackageData,
845    },
846    AssemblerConfig {
847        datasource_ids: &[
848            DatasourceId::BitbakeRecipe,
849            DatasourceId::BitbakeRecipeAppend,
850        ],
851        sibling_file_patterns: &["*.bb", "*.bbappend"],
852        mode: AssemblyMode::SiblingMerge,
853    },
854];
855
856// Datasource IDs intentionally excluded from package assembly.
857//
858// This list is runtime-significant: files with these datasource IDs may remain
859// unowned by any Package, while their dependencies are still eligible for
860// top-level hoisting. Tests also use it to enforce explicit assembly accounting.
861pub static UNASSEMBLED_DATASOURCE_IDS: &[DatasourceId] = &[
862    // Non-package metadata
863    DatasourceId::Readme,
864    DatasourceId::EtcOsRelease,
865    // Binary archives (require external extraction via ExtractCode before scanning)
866    DatasourceId::AlpineApkArchive,
867    DatasourceId::AndroidAab,
868    DatasourceId::AndroidAarLibrary,
869    DatasourceId::AndroidApk,
870    DatasourceId::AndroidManifestXml,
871    DatasourceId::AndroidSoongMetadata,
872    DatasourceId::AppleDmg,
873    DatasourceId::Axis2Mar,
874    DatasourceId::ChromeCrx,
875    DatasourceId::DebianOriginalSourceTarball,
876    DatasourceId::DebianSourceMetadataTarball,
877    DatasourceId::InstallshieldInstaller,
878    DatasourceId::IosIpa,
879    DatasourceId::IsoDiskImage,
880    DatasourceId::JavaEarArchive,
881    DatasourceId::JavaJar,
882    DatasourceId::JavaWarArchive,
883    DatasourceId::JbossSar,
884    DatasourceId::MicrosoftCabinet,
885    DatasourceId::MozillaXpi,
886    DatasourceId::NsisInstaller,
887    DatasourceId::SharShellArchive,
888    DatasourceId::SquashfsDiskImage,
889    // Supplementary metadata (not primary package definitions)
890    DatasourceId::ArchAurinfo,
891    DatasourceId::ArchPkginfo,
892    DatasourceId::ArchSrcinfo,
893    DatasourceId::Axis2ModuleXml,
894    DatasourceId::ClojureDepsEdn,
895    DatasourceId::ClojureProjectClj,
896    DatasourceId::DebianInstalledFilesList,
897    DatasourceId::DebianInstalledMd5Sums,
898    DatasourceId::DebianCopyright,
899    DatasourceId::DebianCopyrightInPackage,
900    DatasourceId::DebianCopyrightStandalone,
901    DatasourceId::GoBinary,
902    DatasourceId::WindowsExecutable,
903    DatasourceId::Dockerfile,
904    DatasourceId::ErlangOtpAppSrc,
905    DatasourceId::HexMixLock,
906    DatasourceId::JavaEarApplicationXml,
907    DatasourceId::JavaWarWebXml,
908    DatasourceId::JbossServiceXml,
909    DatasourceId::MesonBuild,
910    DatasourceId::GemGemspecInstalledSpecifications,
911    DatasourceId::NugetDirectoryBuildProps,
912    DatasourceId::NugetDirectoryPackagesProps,
913    DatasourceId::CitationCff,
914    DatasourceId::PubliccodeYaml,
915    DatasourceId::RpmPackageLicenses,
916    DatasourceId::RustBinary,
917    DatasourceId::SbtBuildSbt,
918    DatasourceId::VcpkgJson,
919];
920
921#[cfg(test)]
922mod tests {
923    use super::*;
924    use std::collections::HashSet;
925    use strum::IntoEnumIterator;
926
927    #[test]
928    fn test_every_datasource_id_is_accounted_for() {
929        let mut assembled: HashSet<DatasourceId> = HashSet::new();
930        for config in ASSEMBLERS {
931            for &dsid in config.datasource_ids {
932                assembled.insert(dsid);
933            }
934        }
935
936        let unassembled: HashSet<DatasourceId> =
937            UNASSEMBLED_DATASOURCE_IDS.iter().copied().collect();
938
939        let overlap: Vec<_> = assembled.intersection(&unassembled).collect();
940        assert!(
941            overlap.is_empty(),
942            "Datasource IDs in BOTH ASSEMBLERS and UNASSEMBLED: {overlap:?}"
943        );
944
945        let missing: Vec<_> = DatasourceId::iter()
946            .filter(|dsid| !assembled.contains(dsid) && !unassembled.contains(dsid))
947            .collect();
948
949        assert!(
950            missing.is_empty(),
951            "Datasource IDs in NEITHER ASSEMBLERS nor UNASSEMBLED: {missing:?}\n\
952             Add each to an AssemblerConfig in ASSEMBLERS, or to UNASSEMBLED_DATASOURCE_IDS."
953        );
954    }
955
956    #[test]
957    fn test_post_assembly_passes_are_unique() {
958        let unique: HashSet<PostAssemblyPassKind> = POST_ASSEMBLY_PASSES.iter().copied().collect();
959
960        assert_eq!(
961            unique.len(),
962            POST_ASSEMBLY_PASSES.len(),
963            "POST_ASSEMBLY_PASSES contains duplicate entries"
964        );
965    }
966
967    #[test]
968    fn test_every_post_assembly_pass_kind_is_registered_once() {
969        let registered: HashSet<PostAssemblyPassKind> =
970            POST_ASSEMBLY_PASSES.iter().copied().collect();
971
972        let missing: Vec<_> = PostAssemblyPassKind::iter()
973            .filter(|pass| !registered.contains(pass))
974            .collect();
975
976        assert!(
977            missing.is_empty(),
978            "Post-assembly pass variants not registered in POST_ASSEMBLY_PASSES: {missing:?}"
979        );
980
981        for pass in PostAssemblyPassKind::iter() {
982            let count = POST_ASSEMBLY_PASSES
983                .iter()
984                .filter(|registered| **registered == pass)
985                .count();
986            assert_eq!(
987                count, 1,
988                "Post-assembly pass {pass:?} should be registered exactly once"
989            );
990        }
991    }
992
993    #[test]
994    fn test_post_assembly_passes_skip_irrelevant_inputs() {
995        let inputs = PostAssemblyInputs::default();
996
997        for pass in PostAssemblyPassKind::iter() {
998            assert!(
999                !pass.should_run(&inputs),
1000                "{pass:?} should skip when no relevant inputs are present"
1001            );
1002        }
1003    }
1004
1005    #[test]
1006    fn test_npm_workspace_inputs_only_run_npm_passes() {
1007        let inputs = PostAssemblyInputs {
1008            package_types: HashSet::from([PackageType::Npm]),
1009            file_datasource_ids: HashSet::from([DatasourceId::NpmPackageJson]),
1010            has_npm_workspace_markers: true,
1011            has_cargo_workspace_markers: false,
1012        };
1013
1014        let runnable: HashSet<_> = PostAssemblyPassKind::iter()
1015            .filter(|pass| pass.should_run(&inputs))
1016            .collect();
1017
1018        assert_eq!(
1019            runnable,
1020            HashSet::from([
1021                PostAssemblyPassKind::NpmResourceAssign,
1022                PostAssemblyPassKind::NpmWorkspaceMerge,
1023            ])
1024        );
1025    }
1026
1027    #[test]
1028    fn test_cargo_workspace_merge_requires_workspace_markers() {
1029        let without_markers = PostAssemblyInputs {
1030            package_types: HashSet::from([PackageType::Cargo]),
1031            file_datasource_ids: HashSet::from([DatasourceId::CargoToml]),
1032            has_npm_workspace_markers: false,
1033            has_cargo_workspace_markers: false,
1034        };
1035
1036        assert!(!PostAssemblyPassKind::CargoWorkspaceMerge.should_run(&without_markers));
1037
1038        let with_markers = PostAssemblyInputs {
1039            has_cargo_workspace_markers: true,
1040            ..without_markers
1041        };
1042
1043        assert!(PostAssemblyPassKind::CargoWorkspaceMerge.should_run(&with_markers));
1044    }
1045}