feluda 1.12.0

A CLI tool to check dependency licenses.
use super::parser;
use super::reporter::{ValidationIssue, ValidationReport};
use crate::debug::FeludaResult;
use serde_json::Value as JsonValue;

pub fn validate(json: &JsonValue) -> FeludaResult<ValidationReport> {
    let mut report = ValidationReport::new("SPDX");

    let obj = match json.as_object() {
        Some(o) => o,
        None => {
            report.add_issue(ValidationIssue::error(
                "SPDX document must be a JSON object",
            ));
            return Ok(report);
        }
    };

    validate_required_fields(&mut report, obj);
    validate_spdx_version(&mut report, obj);
    validate_document_name(&mut report, obj);
    validate_namespace(&mut report, obj);
    validate_packages(&mut report, obj);

    Ok(report)
}

fn validate_required_fields(
    report: &mut ValidationReport,
    obj: &serde_json::Map<String, JsonValue>,
) {
    let required_fields = [
        "spdxVersion",
        "dataLicense",
        "SPDXID",
        "name",
        "documentNamespace",
        "creationInfo",
    ];

    for field in required_fields {
        if !obj.contains_key(field) {
            report.add_issue(
                ValidationIssue::error(format!("Missing required field: {field}"))
                    .with_field(field),
            );
        }
    }
}

fn validate_spdx_version(report: &mut ValidationReport, obj: &serde_json::Map<String, JsonValue>) {
    if let Some(version) = parser::get_string(&JsonValue::Object(obj.clone()), "spdxVersion") {
        if !version.starts_with("SPDX-") {
            report.add_issue(
                ValidationIssue::warning(format!(
                    "Invalid SPDX version format: {version}. Expected format: SPDX-X.Y"
                ))
                .with_field("spdxVersion"),
            );
        }

        let supported_versions = ["SPDX-2.2", "SPDX-2.3"];
        if !supported_versions.iter().any(|v| version.starts_with(v)) {
            report.add_issue(
                ValidationIssue::info(format!("SPDX version {version} may not be fully supported"))
                    .with_field("spdxVersion"),
            );
        }
    }
}

fn validate_document_name(report: &mut ValidationReport, obj: &serde_json::Map<String, JsonValue>) {
    let json_obj = JsonValue::Object(obj.clone());

    if let Some(name) = parser::get_string(&json_obj, "name") {
        if name.is_empty() {
            report.add_issue(
                ValidationIssue::error("Document name cannot be empty").with_field("name"),
            );
        }
    }
}

fn validate_namespace(report: &mut ValidationReport, obj: &serde_json::Map<String, JsonValue>) {
    let json_obj = JsonValue::Object(obj.clone());

    if let Some(namespace) = parser::get_string(&json_obj, "documentNamespace") {
        if namespace.is_empty() {
            report.add_issue(
                ValidationIssue::error("Document namespace cannot be empty")
                    .with_field("documentNamespace"),
            );
        } else if !namespace.starts_with("https://") && !namespace.starts_with("http://") {
            report.add_issue(
                ValidationIssue::warning("Document namespace should be a valid URI")
                    .with_field("documentNamespace"),
            );
        }
    }
}

fn validate_packages(report: &mut ValidationReport, obj: &serde_json::Map<String, JsonValue>) {
    let json_obj = JsonValue::Object(obj.clone());

    if let Some(packages) = parser::get_array(&json_obj, "packages") {
        if packages.is_empty() {
            report.add_issue(ValidationIssue::warning("No packages defined in SBOM"));
        }

        for (idx, package) in packages.iter().enumerate() {
            validate_package(report, package, idx);
        }
    }
}

fn validate_package(report: &mut ValidationReport, package: &JsonValue, index: usize) {
    if let Some(pkg_obj) = package.as_object() {
        let pkg_json = JsonValue::Object(pkg_obj.clone());

        let package_name =
            parser::get_string(&pkg_json, "name").unwrap_or_else(|| format!("Package[{index}]"));

        if !parser::has_key(&pkg_json, "SPDXID") {
            report.add_issue(
                ValidationIssue::error(format!("Package '{package_name}': missing SPDXID"))
                    .with_field("SPDXID"),
            );
        } else if let Some(spdx_id) = parser::get_string(&pkg_json, "SPDXID") {
            if !spdx_id.starts_with("SPDXRef-") {
                report.add_issue(
                    ValidationIssue::warning(format!(
                        "Package '{package_name}': SPDXID should start with 'SPDXRef-'"
                    ))
                    .with_field("SPDXID"),
                );
            }
        }

        if !parser::has_key(&pkg_json, "downloadLocation") {
            report.add_issue(
                ValidationIssue::error(format!(
                    "Package '{package_name}': missing downloadLocation"
                ))
                .with_field("downloadLocation"),
            );
        }

        if !parser::has_key(&pkg_json, "filesAnalyzed") {
            report.add_issue(
                ValidationIssue::info(format!(
                    "Package '{package_name}': filesAnalyzed not specified"
                ))
                .with_field("filesAnalyzed"),
            );
        }
    }
}