provenant-cli 0.1.0

Rust-based ScanCode-compatible scanner for licenses, package metadata, SBOMs, and provenance data.
Documentation
// SPDX-FileCopyrightText: Provenant contributors
// SPDX-License-Identifier: Apache-2.0

use std::collections::HashMap;

use crate::models::{DatasourceId, FileInfo, Package, PackageData, PackageUid, TopLevelDependency};

pub fn merge_conda_rootfs_metadata(
    files: &mut [FileInfo],
    packages: &mut Vec<Package>,
    dependencies: &mut [TopLevelDependency],
) {
    let conda_json_data: Vec<(String, PackageData, String)> = files
        .iter()
        .flat_map(|file| {
            file.package_data.iter().filter_map(|pkg_data| {
                if pkg_data.datasource_id != Some(DatasourceId::CondaMetaJson) {
                    return None;
                }

                Some((
                    file.path.clone(),
                    pkg_data.clone(),
                    pkg_data
                        .extra_data
                        .as_ref()?
                        .get("extracted_package_dir")?
                        .as_str()?
                        .replace('\\', "/")
                        .split("/pkgs/")
                        .nth(1)?
                        .to_string(),
                ))
            })
        })
        .collect();

    let json_package_uids: HashMap<Option<String>, PackageUid> = packages
        .iter()
        .filter(|package| {
            package
                .datasource_ids
                .contains(&DatasourceId::CondaMetaJson)
        })
        .map(|package| (package.purl.clone(), package.package_uid.clone()))
        .collect();

    let mut removal_indices = Vec::new();

    for (json_path, pkg_data, package_dir_name) in conda_json_data {
        let Some(target_idx) = packages.iter().enumerate().find_map(|(idx, package)| {
            if !package
                .datasource_ids
                .contains(&DatasourceId::CondaMetaYaml)
            {
                return None;
            }

            let matches_recipe = package.datafile_paths.iter().any(|path| {
                path.contains(&format!("pkgs/{package_dir_name}/info/recipe/meta.yaml"))
                    || path.contains(&format!("pkgs/{package_dir_name}/info/recipe/meta.yml"))
                    || path.contains(&format!(
                        "pkgs/{package_dir_name}/info/recipe.tar-extract/recipe/meta.yaml"
                    ))
                    || path.contains(&format!(
                        "pkgs/{package_dir_name}/info/recipe.tar-extract/recipe/meta.yml"
                    ))
            });

            (matches_recipe && package.purl == pkg_data.purl).then_some(idx)
        }) else {
            continue;
        };

        let old_uid = json_package_uids.get(&pkg_data.purl).cloned();
        packages[target_idx].update(&pkg_data, json_path);
        let new_uid = packages[target_idx].package_uid.clone();

        if let Some(old_uid) = old_uid {
            for file in files.iter_mut() {
                for package_uid in &mut file.for_packages {
                    if *package_uid == old_uid {
                        *package_uid = new_uid.clone();
                    }
                }
            }

            for dep in dependencies.iter_mut() {
                if dep.for_package_uid.as_ref() == Some(&old_uid) {
                    dep.for_package_uid = Some(new_uid.clone());
                }
            }

            if let Some(idx) = packages
                .iter()
                .position(|package| package.package_uid == old_uid)
            {
                removal_indices.push(idx);
            }
        }
    }

    removal_indices.sort_unstable();
    removal_indices.dedup();
    for idx in removal_indices.into_iter().rev() {
        packages.remove(idx);
    }
}