# Copyright (C) 2026 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause

# Records a reference to an SBOM external document in the given 'target' for the currently active
# SBOM project.
#
# External SBOM targets can then reference this document target, to allow creating relationships
# bewteen current project and external SBOM entities.
#
# Currently the following SBOM_FORMAT formats are supported:
# - 'SPDX_V2_TAG_VALUE'
# - 'SPDX_V2_JSON'
# - 'CYDX_V1_JSON'
#
# For the SPDX format, the SPDX_V2_DOCUMENT_REF_ID option is required, which is used to identify
# the external document within the current project SBOM.
#
# In addition, we need the SPDX document namespace and file sha1 checksum. These can be passed
# explcitily, or parsed from the external document.
#
# 1) To parse them from the document, the relative or absolute document path should be passed via
# the EXTERNAL_DOCUMENT_FILE_PATH option.
# If the passed path is relative, it gets searched in 'EXTERNAL_DOCUMENT_SEARCH_PATHS'.
# The file must exist at the point when the API is called.
#
# 2) The values can be specified via the SPDX_V2_DOCUMENT_NAMESPACE and SPDX_V2_DOCUMENT_SHA1
# options.
#
# For CycloneDX the CYDX_URN_SERIAL_NUMBER and CYDX_URN_BOM_VERSION options should be passed.
# Or they can be parsed via EXTERNAL_DOCUMENT_FILE_PATH.
function(_qt_internal_sbom_add_external_reference_document target)
    if(NOT QT_GENERATE_SBOM)
        return()
    endif()

    set(opt_args
        NO_SCAN_FILE_AT_CONFIGURE_TIME
    )
    set(single_args
        SBOM_FORMAT
        EXTERNAL_DOCUMENT_FILE_PATH

        SPDX_V2_DOCUMENT_REF_ID
        SPDX_V2_DOCUMENT_NAMESPACE
        SPDX_V2_DOCUMENT_SHA1

        CYDX_URN_SERIAL_NUMBER
        CYDX_URN_BOM_VERSION
    )
    set(multi_args
        EXTERNAL_DOCUMENT_SEARCH_PATHS
    )
    cmake_parse_arguments(PARSE_ARGV 1 arg "${opt_args}" "${single_args}" "${multi_args}")
    _qt_internal_validate_all_args_are_parsed(arg)

    # We don't support SPDX v3 json at the moment.
    set(supported_sbom_formats
        CYDX_V1_JSON
        SPDX_V2_TAG_VALUE
        SPDX_V2_JSON
    )

    if(NOT arg_SBOM_FORMAT)
        message(FATAL_ERROR "SBOM_FORMAT must be specified.")
    endif()

    if(NOT arg_SBOM_FORMAT IN_LIST supported_sbom_formats)
        message(FATAL_ERROR "Unsupported SBOM_FORMAT value: ${arg_SBOM_FORMAT}. "
            "Currently supported values are: ${supported_sbom_formats}")
    endif()

    if(NOT TARGET "${target}")
        add_library("${target}" INTERFACE IMPORTED)
        set_target_properties("${target}" PROPERTIES IMPORTED_GLOBAL TRUE)
    endif()

    get_target_property(is_external_project_sbom_target "${target}"
        _qt_sbom_is_external_project_sbom_target)
    if(NOT is_external_project_sbom_target)
        set_target_properties("${target}" PROPERTIES
            _qt_sbom_is_custom_sbom_target "TRUE"
            _qt_sbom_is_external_project_sbom_target "TRUE"

            # These aren't real values, but it's what we use to determine external packages
            # during dependency handling.
            _qt_sbom_spdx_repo_project_name_lowercase "fake_external_project_name_${target}"
            _qt_sbom_spdx_relative_installed_repo_document_path "/fake_external_doc_path_${target}"
        )
    endif()

    # Return early gracefully when we're requested to add a format that SBOM generation isn't
    # enabled for.
    if((arg_SBOM_FORMAT STREQUAL "SPDX_V2_TAG_VALUE" OR arg_SBOM_FORMAT STREQUAL "SPDX_V2_JSON")
            AND NOT QT_SBOM_GENERATE_SPDX_V2)
        message(DEBUG "SPDX v2 SBOM generation is not enabled, skipping adding external reference "
            "for target '${target}'."
            "\nSPDX_V2_DOCUMENT_REF_ID: '${arg_SPDX_V2_DOCUMENT_REF_ID}'"
            "\nSPDX_V2_DOCUMENT_NAMESPACE: '${arg_SPDX_V2_DOCUMENT_NAMESPACE}'"
            "\nEXTERNAL_DOCUMENT_FILE_PATH: '${arg_EXTERNAL_DOCUMENT_FILE_PATH}'"
        )
        return()
    endif()

    if(arg_SBOM_FORMAT STREQUAL "CYDX_V1_JSON" AND NOT QT_SBOM_GENERATE_CYDX_V1_6)
        message(DEBUG
            "CycloneDX v1 SBOM generation is not enabled, skipping adding external reference "
            "for target '${target}'."
            "\nCYDX_URN_SERIAL_NUMBER: '${arg_CYDX_URN_SERIAL_NUMBER}'"
            "\nEXTERNAL_DOCUMENT_FILE_PATH: '${arg_EXTERNAL_DOCUMENT_FILE_PATH}'"
        )
        return()
    endif()

    # If an document file path is given and configure time scan opt out is not passed, search for
    # the file and prefer the values extracted from it, rather than what can be passed to the
    # function.
    # If configure time scanning is opted out, we will require some of the information to be passed
    # at explicitly.
    if(arg_EXTERNAL_DOCUMENT_FILE_PATH AND NOT arg_NO_SCAN_FILE_AT_CONFIGURE_TIME)
        set(document_search_path_args "")
        if(arg_EXTERNAL_DOCUMENT_SEARCH_PATHS)
            list(APPEND document_search_path_args
                EXTERNAL_DOCUMENT_SEARCH_PATHS ${arg_EXTERNAL_DOCUMENT_SEARCH_PATHS})
        endif()
        _qt_internal_sbom_get_external_reference_search_paths(document_search_paths
            ${document_search_path_args}
        )

        set(find_file_args "")
        if(document_search_paths)
            list(APPEND find_file_args
                EXTERNAL_DOCUMENT_SEARCH_PATHS ${document_search_paths})
        endif()
        _qt_internal_sbom_find_external_reference_document(document_file_path
            EXTERNAL_DOCUMENT_FILE_PATH "${arg_EXTERNAL_DOCUMENT_FILE_PATH}"
            ${find_file_args}
        )
    elseif(arg_EXTERNAL_DOCUMENT_FILE_PATH)
        # If we don't scan at configure time, we still need to pass the document along at least
        # for spdx v2.
        set(document_file_path "${arg_EXTERNAL_DOCUMENT_FILE_PATH}")
    endif()

    if(arg_SBOM_FORMAT STREQUAL "SPDX_V2_TAG_VALUE" OR arg_SBOM_FORMAT STREQUAL "SPDX_V2_JSON")
        _qt_internal_forward_function_args(
            FORWARD_PREFIX arg
            FORWARD_OUT_VAR external_document_args
            FORWARD_OPTIONS
                NO_SCAN_FILE_AT_CONFIGURE_TIME
            FORWARD_SINGLE
                SBOM_FORMAT
                SPDX_V2_DOCUMENT_REF_ID
                SPDX_V2_DOCUMENT_NAMESPACE
                SPDX_V2_DOCUMENT_SHA1
            FORWARD_MULTI
                EXTERNAL_DOCUMENT_SEARCH_PATHS
        )
        if(document_file_path)
            list(APPEND external_document_args EXTERNAL_DOCUMENT_FILE_PATH "${document_file_path}")
        endif()
        _qt_internal_sbom_add_external_reference_document_spdx_v2(${target}
            ${external_document_args}
        )
    endif()

    if(arg_SBOM_FORMAT STREQUAL "CYDX_V1_JSON")
        _qt_internal_forward_function_args(
            FORWARD_PREFIX arg
            FORWARD_OUT_VAR external_document_args
            FORWARD_OPTIONS
                NO_SCAN_FILE_AT_CONFIGURE_TIME
            FORWARD_SINGLE
                CYDX_URN_SERIAL_NUMBER
                CYDX_URN_BOM_VERSION
        )
        if(document_file_path)
            list(APPEND external_document_args EXTERNAL_DOCUMENT_FILE_PATH "${document_file_path}")
        endif()
        _qt_internal_sbom_add_external_reference_document_cydx_v1(${target}
            ${external_document_args}
        )
    endif()
endfunction()

# Handles adding an SPDX v2 external document reference, either in tag:value or json format.
# NO_SCAN_FILE_AT_CONFIGURE_TIME is an internal option, only used by the Qt build system
# to reference external documents that do not exist yet, but will exist at installation time.
function(_qt_internal_sbom_add_external_reference_document_spdx_v2 target)
    set(opt_args
        NO_SCAN_FILE_AT_CONFIGURE_TIME
    )
    set(single_args
        SBOM_FORMAT
        EXTERNAL_DOCUMENT_FILE_PATH

        SPDX_V2_DOCUMENT_REF_ID
        SPDX_V2_DOCUMENT_NAMESPACE
        SPDX_V2_DOCUMENT_SHA1
    )
    set(multi_args
        EXTERNAL_DOCUMENT_SEARCH_PATHS
    )
    cmake_parse_arguments(PARSE_ARGV 1 arg "${opt_args}" "${single_args}" "${multi_args}")
    _qt_internal_validate_all_args_are_parsed(arg)

    if(NOT arg_SPDX_V2_DOCUMENT_REF_ID)
        message(FATAL_ERROR "SPDX_V2_DOCUMENT_REF_ID must be specified.")
    endif()

    get_target_property(exising_external_document_ref "${target}"
        _qt_sbom_spdx_v2_external_document_ref)

    if(exising_external_document_ref
            AND NOT exising_external_document_ref STREQUAL arg_SPDX_V2_DOCUMENT_REF_ID)
        message(FATAL_ERROR
            "Target '${target}' already has an associated SPDX v2 external document reference "
            "'${exising_external_document_ref}' set, but a new value "
            "'${arg_SPDX_V2_DOCUMENT_REF_ID}' is being set. A target can't have more than one "
            "value for the same SBOM format.")
    endif()

    # Used later.
    if(arg_SBOM_FORMAT STREQUAL "SPDX_V2_TAG_VALUE")
        set(document_path_property_name "_qt_sbom_spdx_v2_document_tag_value_relative_path")
    elseif(arg_SBOM_FORMAT STREQUAL "SPDX_V2_JSON")
        set(document_path_property_name "_qt_sbom_spdx_v2_document_json_relative_path")
    endif()

    # Check that the document ref id is unique for the current project. Error out if it is not
    # unique, otherwise add to the known list. But allow for the use case of adding one of the
    # missing formats, either a tag:value or a json spdx v2 document for the same target,
    # as long as the same ref id is used.
    get_cmake_property(known_external_document_target
        _qt_known_external_documents_${arg_SPDX_V2_DOCUMENT_REF_ID}_target)

    set(should_add_new_ref_id TRUE)
    if(known_external_document_target)
        if(NOT known_external_document_target STREQUAL target)
            message(FATAL_ERROR
                "SPDX v2 external document reference '${arg_SPDX_V2_DOCUMENT_REF_ID}' was already "
                "recorded for target '${known_external_document_target}', but is now being "
                "referenced by a different target '${target}'. An SPDX document reference ID can "
                "only be associated with a single external document target in the current project "
                "SPDX document.")
        endif()

        get_target_property(document_relative_path "${target}" "${document_path_property_name}")

        # If the property is already set, treat it as a duplication error.
        if(document_relative_path)
            message(FATAL_ERROR
                "SPDX v2 external document reference '${arg_SPDX_V2_DOCUMENT_REF_ID}' was already "
                "recorded for target '${target}', format '${arg_SBOM_FORMAT}', but is now being "
                "added again. A given SBOM format can only be recorded once per target.")
        else()
            # We are just updating a new format's relative file path for the same target.
            set(should_add_new_ref_id FALSE)
        endif()
    endif()

    if(should_add_new_ref_id)
        set_property(GLOBAL PROPERTY
            _qt_known_external_documents_${arg_SPDX_V2_DOCUMENT_REF_ID} TRUE)

        set_property(GLOBAL PROPERTY
            _qt_known_external_documents_${arg_SPDX_V2_DOCUMENT_REF_ID}_target "${target}")

        set_property(GLOBAL APPEND PROPERTY
            _qt_known_external_documents "${arg_SPDX_V2_DOCUMENT_REF_ID}")

        set_target_properties("${target}" PROPERTIES
            _qt_sbom_spdx_v2_external_document_ref "${arg_SPDX_V2_DOCUMENT_REF_ID}"
        )
    endif()

    if(arg_EXTERNAL_DOCUMENT_FILE_PATH AND NOT arg_NO_SCAN_FILE_AT_CONFIGURE_TIME)
        _qt_internal_sbom_parse_spdx_v2_document_namespace(
            EXTERNAL_DOCUMENT_FILE_PATH "${arg_EXTERNAL_DOCUMENT_FILE_PATH}"
            SBOM_FORMAT "${arg_SBOM_FORMAT}"
            OUT_VAR_DOCUMENT_NAMESPACE document_namespace
        )

        if(NOT document_namespace)
            message(FATAL_ERROR
                "Failed to extract DocumentNamespace from SPDX v2 external document: "
                "'${arg_EXTERNAL_DOCUMENT_FILE_PATH}'")
        endif()
    else()
        if(NOT arg_SPDX_V2_DOCUMENT_NAMESPACE)
            message(FATAL_ERROR
                "SPDX_V2_DOCUMENT_NAMESPACE must be specified for target '${target}' if "
                "EXTERNAL_DOCUMENT_FILE_PATH is not given or NO_SCAN_FILE_AT_CONFIGURE_TIME is "
                "set.")
        else()
            set(document_namespace "${arg_SPDX_V2_DOCUMENT_NAMESPACE}")
        endif()
    endif()

    get_target_property(existing_document_namespace "${target}"
        _qt_sbom_spdx_repo_document_namespace)

    if(existing_document_namespace
            AND NOT existing_document_namespace STREQUAL document_namespace)
        message(FATAL_ERROR
            "Target '${target}' already has an associated SPDX v2 external document namespace "
            "'${existing_document_namespace}' set, but a new value "
            "'${document_namespace}' is being set. A target can't have more than one "
            "value for the same SBOM format.")
    endif()

    if(should_add_new_ref_id)
        set_target_properties("${target}" PROPERTIES
            _qt_sbom_spdx_repo_document_namespace "${document_namespace}"
        )
    endif()

    # Save the document file path for querying if available.
    if(arg_EXTERNAL_DOCUMENT_FILE_PATH)
        set_target_properties("${target}" PROPERTIES
            "${document_path_property_name}" "${arg_EXTERNAL_DOCUMENT_FILE_PATH}"
        )
    endif()

    # SPDX v2 compared to CycloneDX also requires adding an explicit external document ref to the
    # current's project SBOM document, which needs a namespace and a sha1 value.
    set(forward_args "")
    if(arg_EXTERNAL_DOCUMENT_FILE_PATH)
        list(APPEND forward_args
            EXTERNAL_DOCUMENT_FILE_PATH "${arg_EXTERNAL_DOCUMENT_FILE_PATH}")
    endif()

    if(arg_EXTERNAL_DOCUMENT_SEARCH_PATHS)
        list(APPEND forward_args
            EXTERNAL_DOCUMENT_INSTALL_PREFIXES ${arg_EXTERNAL_DOCUMENT_SEARCH_PATHS})
    endif()

    if(arg_SPDX_V2_DOCUMENT_NAMESPACE)
        list(APPEND forward_args
            EXTERNAL_DOCUMENT_NAMESPACE "${arg_SPDX_V2_DOCUMENT_NAMESPACE}")
    endif()

    if(arg_SPDX_V2_DOCUMENT_SHA1)
        list(APPEND forward_args
            EXTERNAL_DOCUMENT_SHA1 "${arg_SPDX_V2_DOCUMENT_SHA1}")
    endif()

    if(should_add_new_ref_id)
        _qt_internal_sbom_generate_add_external_reference(
            EXTERNAL_DOCUMENT_SPDX_ID "${arg_SPDX_V2_DOCUMENT_REF_ID}"
            SBOM_FORMAT "${arg_SBOM_FORMAT}"
            ${forward_args}
        )
    endif()
endfunction()

# Parses out the SPDX v2 document namespace value out of an external SPDX document.
# Support SPDX v2.3 tag value and json formats for the file path.
# Errors out when the document namespace can't be found.
function(_qt_internal_sbom_parse_spdx_v2_document_namespace)
    set(opt_args "")
    set(single_args
        SBOM_FORMAT
        EXTERNAL_DOCUMENT_FILE_PATH
        OUT_VAR_DOCUMENT_NAMESPACE
    )
    set(multi_args "")
    cmake_parse_arguments(PARSE_ARGV 0 arg "${opt_args}" "${single_args}" "${multi_args}")
    _qt_internal_validate_all_args_are_parsed(arg)

    if(NOT arg_EXTERNAL_DOCUMENT_FILE_PATH)
        message(FATAL_ERROR "EXTERNAL_DOCUMENT_FILE_PATH must be specified.")
    endif()

    if(NOT EXISTS "${arg_EXTERNAL_DOCUMENT_FILE_PATH}")
        message(FATAL_ERROR
            "EXTERNAL_DOCUMENT_FILE_PATH '${arg_EXTERNAL_DOCUMENT_FILE_PATH}' does not exist.")
    endif()

    if(NOT arg_OUT_VAR_DOCUMENT_NAMESPACE)
        message(FATAL_ERROR "OUT_VAR_DOCUMENT_NAMESPACE must be specified.")
    endif()

    if(NOT arg_SBOM_FORMAT)
        message(FATAL_ERROR "SBOM_FORMAT must be specified.")
    endif()

    file(READ "${arg_EXTERNAL_DOCUMENT_FILE_PATH}" ext_content)

    set(extra_error_message "")

    # Support extracting the namespace from both a tag:value and json document.
    if(arg_SBOM_FORMAT STREQUAL "SPDX_V2_TAG_VALUE")
        string(REGEX REPLACE "^.*[\r\n]DocumentNamespace:[ \t]*([^#\r\n]*).*$"
                "\\1" document_namespace "${ext_content}")
    elseif(arg_SBOM_FORMAT STREQUAL "SPDX_V2_JSON")
        string(JSON document_namespace ERROR_VARIABLE document_namespace_error
            GET "${ext_content}" "documentNamespace")
        if(document_namespace_error)
            set(extra_error_message "${document_namespace_error}")
        endif()
    endif()

    if(NOT document_namespace)
        message(FATAL_ERROR
            "Failed to extract DocumentNamespace from SPDX v2 external document: "
            "'${arg_EXTERNAL_DOCUMENT_FILE_PATH}'")
    endif()

    set(${arg_OUT_VAR_DOCUMENT_NAMESPACE} "${document_namespace}" PARENT_SCOPE)
endfunction()

# Handles adding a CycloneDX v1 external document reference in json format.
function(_qt_internal_sbom_add_external_reference_document_cydx_v1 target)
    set(opt_args
        NO_SCAN_FILE_AT_CONFIGURE_TIME
    )
    set(single_args
        EXTERNAL_DOCUMENT_FILE_PATH

        CYDX_URN_SERIAL_NUMBER
        CYDX_URN_BOM_VERSION
    )
    set(multi_args "")
    cmake_parse_arguments(PARSE_ARGV 1 arg "${opt_args}" "${single_args}" "${multi_args}")
    _qt_internal_validate_all_args_are_parsed(arg)

    if(arg_EXTERNAL_DOCUMENT_FILE_PATH AND NOT arg_NO_SCAN_FILE_AT_CONFIGURE_TIME)
        file(READ "${arg_EXTERNAL_DOCUMENT_FILE_PATH}" ext_content)

        string(JSON serial_number ERROR_VARIABLE serial_number_error GET "${ext_content}"
            "serialNumber")
        if(serial_number_error)
            message(FATAL_ERROR
                "Failed to extract CycloneDX serialNumber from external document: "
                "'${arg_EXTERNAL_DOCUMENT_FILE_PATH}'")
        endif()

        string(JSON urn_bom_version ERROR_VARIABLE bom_version_error GET "${ext_content}"
            "version")
        if(bom_version_error)
            message(FATAL_ERROR
                "Failed to extract CycloneDX bom version from external document: "
                "'${arg_EXTERNAL_DOCUMENT_FILE_PATH}'")
        endif()
    else()
        if(NOT arg_CYDX_URN_SERIAL_NUMBER)
            message(FATAL_ERROR "CYDX_URN_SERIAL_NUMBER must be specified for target '${target}' "
                "if EXTERNAL_DOCUMENT_FILE_PATH is not given or NO_SCAN_FILE_AT_CONFIGURE_TIME is "
                "set.")
        endif()
        set(serial_number "${arg_CYDX_URN_SERIAL_NUMBER}")

        if(arg_CYDX_URN_BOM_VERSION)
            set(urn_bom_version "${arg_CYDX_URN_BOM_VERSION}")
        else()
            set(urn_bom_version "1")
        endif()
    endif()

    get_target_property(existing_bom_serial_number "${target}"
        _qt_sbom_cydx_bom_serial_number_uuid)

    if(existing_bom_serial_number AND NOT existing_bom_serial_number STREQUAL serial_number)
        message(FATAL_ERROR
            "Target '${target}' already has an associated CycloneDX external document serial "
            "number '${existing_bom_serial_number}' set, but a new value "
            "'${serial_number}' is being set. A target can't have more than one "
            "value for the same SBOM format.")
    endif()

    # Check that the bom serial number (uuid) is unique for the current project. Error out if it is
    # not unique, otherwise add it to the known list.
    get_cmake_property(known_external_document_target
        _qt_known_external_documents_${serial_number}_cydx_target)

    set(should_add_new_uuid TRUE)
    if(known_external_document_target)
        if(NOT known_external_document_target STREQUAL target)
            message(FATAL_ERROR
                "CycloneDX external document serial number '${serial_number}' was already recorded "
                "for target '${known_external_document_target}', but is now being referenced by a "
                "different target '${target}'. A CycloneDX bom serial number can only be "
                "associated with a single external document target in the current project "
                "CycloneDX document.")
        endif()

        get_target_property(document_relative_path "${target}"
            _qt_sbom_cydx_v1_6_document_json_relative_path)

        # If the property is already set, treat it as an error.
        if(document_relative_path)
            message(FATAL_ERROR
                "CycloneDX external document serial number '${serial_number}' was already recorded "
                "for target '${target}', but is now being added again. The CycloneDX external "
                "reference can only be recorded once per target.")
        else()
            # The serial number was already recorded for this target, nothing new to add.
            set(should_add_new_uuid FALSE)
        endif()
    endif()

    if(should_add_new_uuid)
        set_property(GLOBAL PROPERTY
            _qt_known_external_documents_${serial_number}_cydx TRUE)

        set_property(GLOBAL PROPERTY
            _qt_known_external_documents_${serial_number}_cydx_target "${target}")

        set_property(GLOBAL APPEND PROPERTY
            _qt_known_external_documents_cydx "${serial_number}")
    endif()

    # Always refresh the target's cydx properties, even when re-registering the same serial
    # so a changed bom version isn't silently dropped.
    set_target_properties("${target}" PROPERTIES
        _qt_sbom_cydx_bom_serial_number_uuid "${serial_number}"
        _qt_sbom_cydx_urn_bom_version "${urn_bom_version}"
    )

    # Save the document file path for querying if available.
    if(arg_EXTERNAL_DOCUMENT_FILE_PATH)
        set_target_properties("${target}" PROPERTIES
            "_qt_sbom_cydx_v1_6_document_json_relative_path" "${arg_EXTERNAL_DOCUMENT_FILE_PATH}"
        )
    endif()
endfunction()

# Returns a list of external document search paths, based on confguration options passed.
function(_qt_internal_sbom_get_external_reference_search_paths out_var)
    if(NOT QT_GENERATE_SBOM)
        set(${out_var} "" PARENT_SCOPE)
        return()
    endif()

    set(opt_args
        SEARCH_IN_BUILD_SBOM_DIRS
        SEARCH_IN_QT_PREFIXES
        SEARCH_IN_DESTDIR_INSTALL_PREFIX_AT_INSTALL_TIME
    )
    set(single_args "")
    set(multi_args
        EXTERNAL_DOCUMENT_SEARCH_PATHS
    )
    cmake_parse_arguments(PARSE_ARGV 1 arg "${opt_args}" "${single_args}" "${multi_args}")
    _qt_internal_validate_all_args_are_parsed(arg)

    set(search_paths "")

    # Add the current sbom build dirs as install prefixes, so that we can use ninja 'sbom'
    # in top-level builds. This is needed because the external references will point
    # to sbom docs in different build dirs, not just one.
    # We also need it in case we are converting a json document to a tag/value format in the
    # current build dir of the project, and want it to be found.
    if(arg_SEARCH_IN_BUILD_SBOM_DIRS)
        get_cmake_property(build_sbom_dirs _qt_internal_sbom_dirs)
        if(build_sbom_dirs)
            foreach(build_sbom_dir IN LISTS build_sbom_dirs)
                list(APPEND search_paths "${build_sbom_dir}")
            endforeach()
        endif()
    endif()

    get_cmake_property(project_search_paths _qt_internal_sbom_external_document_search_paths)
    if(project_search_paths)
        list(APPEND search_paths ${project_search_paths})
    endif()

    get_cmake_property(search_in_cmake_paths
        _qt_internal_sbom_auto_search_external_documents_in_paths)
    if(search_in_cmake_paths)
        if(CMAKE_PREFIX_PATH)
            list(APPEND search_paths ${CMAKE_PREFIX_PATH})
        endif()
        if(CMAKE_FRAMEWORK_PATH)
            list(APPEND search_paths ${CMAKE_FRAMEWORK_PATH})
        endif()
    endif()

    # Always append the install time install prefix.
    # The variable is escaped, so it is evaluated during cmake install time, so that the value
    # can be overridden with cmake --install . --prefix <path>.
    if(arg_SEARCH_IN_DESTDIR_INSTALL_PREFIX_AT_INSTALL_TIME)
        list(APPEND search_paths "\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}")
    endif()

    if(arg_EXTERNAL_DOCUMENT_SEARCH_PATHS)
        list(APPEND search_paths ${arg_EXTERNAL_DOCUMENT_SEARCH_PATHS})
    endif()

    if(arg_SEARCH_IN_QT_PREFIXES AND QT6_INSTALL_PREFIX)
        list(APPEND search_paths ${QT6_INSTALL_PREFIX})
    endif()

    if(arg_SEARCH_IN_QT_PREFIXES AND QT_ADDITIONAL_PACKAGES_PREFIX_PATH)
        list(APPEND search_paths ${QT_ADDITIONAL_PACKAGES_PREFIX_PATH})
    endif()

    if(QT_ADDITIONAL_SBOM_DOCUMENT_PATHS)
        list(APPEND search_paths ${QT_ADDITIONAL_SBOM_DOCUMENT_PATHS})
    endif()

    list(REMOVE_DUPLICATES search_paths)

    # On Windows we might get backslash paths from e.g. CMAKE_PREFIX_PATH. Convert them to forward
    # slashes to avoid syntax errors when operating on them in
    # _qt_internal_sbom_generate_add_external_reference.
    set(search_paths_posix "")
    foreach(search_path IN LISTS search_paths)
        file(TO_CMAKE_PATH "${search_path}" search_path_posix)
        list(APPEND search_paths_posix "${search_path_posix}")
    endforeach()

    set(${out_var} "${search_paths_posix}" PARENT_SCOPE)
endfunction()

# Given an absolute or relative path for an external document, searches for it in the given
# EXTERNAL_DOCUMENT_SEARCH_PATHS and returns the found path, or an empty string.
function(_qt_internal_sbom_find_external_reference_document out_var)
    if(NOT QT_GENERATE_SBOM)
        set(${out_var} "" PARENT_SCOPE)
        return()
    endif()

    set(opt_args "")
    set(single_args
        EXTERNAL_DOCUMENT_FILE_PATH
    )
    set(multi_args
        EXTERNAL_DOCUMENT_SEARCH_PATHS
    )
    cmake_parse_arguments(PARSE_ARGV 1 arg "${opt_args}" "${single_args}" "${multi_args}")
    _qt_internal_validate_all_args_are_parsed(arg)

    set(document_file_path "")

    set(maybe_external_document_file_path "${arg_EXTERNAL_DOCUMENT_FILE_PATH}")

    if(IS_ABSOLUTE "${maybe_external_document_file_path}"
            AND EXISTS "${maybe_external_document_file_path}")
        set(document_file_path "${maybe_external_document_file_path}")
        set(${out_var} "${document_file_path}" PARENT_SCOPE)
        return()
    endif()

    set(relative_file_name "${maybe_external_document_file_path}")
    list(JOIN arg_EXTERNAL_DOCUMENT_SEARCH_PATHS "\n" document_dir_paths_per_line)

    foreach(document_dir_path IN LISTS arg_EXTERNAL_DOCUMENT_SEARCH_PATHS)
        set(maybe_document_file_path "${document_dir_path}/${relative_file_name}")
        if(EXISTS "${maybe_document_file_path}")
            set(document_file_path "${maybe_document_file_path}")
            break()
        endif()
    endforeach()

    if(NOT document_file_path OR NOT EXISTS "${document_file_path}")
        message(FATAL_ERROR "Could not find external SBOM document "
            "${maybe_external_document_file_path}"
            " in any of the document search paths: ${document_dir_paths_per_line} "
        )
    endif()

    set(${out_var} "${document_file_path}" PARENT_SCOPE)
endfunction()

# Queries information about the external reference document associated with the given target, and
# sets the values in the parent scope.
function(_qt_internal_sbom_query_external_reference_document target)
    if(NOT QT_GENERATE_SBOM)
        return()
    endif()

    set(opt_args "")
    set(single_args
        OUT_VAR_PROJECT_NAME_LOWERCASE

        OUT_VAR_RELATIVE_SPDX_V2_TAG_VALUE_DOCUMENT_PATH
        OUT_VAR_RELATIVE_SPDX_V2_JSON_DOCUMENT_PATH
        OUT_VAR_RELATIVE_CYDX_V1_6_JSON_DOCUMENT_PATH

        OUT_VAR_SPDX_V2_DOCUMENT_REF_ID
        OUT_VAR_SPDX_V2_DOCUMENT_NAMESPACE

        OUT_VAR_CYDX_URN_SERIAL_NUMBER
        OUT_VAR_CYDX_URN_BOM_VERSION
    )
    set(multi_args "")
    cmake_parse_arguments(PARSE_ARGV 1 arg "${opt_args}" "${single_args}" "${multi_args}")
    _qt_internal_validate_all_args_are_parsed(arg)

    if(NOT TARGET "${target}")
        message(FATAL_ERROR "Target '${target}' does not exist.")
    endif()

    get_target_property(is_external_project_sbom_target
        "${target}" _qt_sbom_is_external_project_sbom_target)

    if(NOT is_external_project_sbom_target)
        message(FATAL_ERROR "Target '${target}' is not an external project SBOM target.")
    endif()

    if(arg_OUT_VAR_PROJECT_NAME_LOWERCASE)
        get_target_property(project_name_lowecase "${target}"
            _qt_sbom_spdx_repo_project_name_lowercase)
        set(${arg_OUT_VAR_PROJECT_NAME_LOWERCASE} "${project_name_lowecase}" PARENT_SCOPE)
    endif()

    if(arg_OUT_VAR_RELATIVE_SPDX_V2_TAG_VALUE_DOCUMENT_PATH)
        get_target_property(document_spdx_v2_tag_value_relative_path "${target}"
            _qt_sbom_spdx_v2_document_tag_value_relative_path)
        set(${arg_OUT_VAR_RELATIVE_SPDX_V2_TAG_VALUE_DOCUMENT_PATH}
            "${document_spdx_v2_tag_value_relative_path}" PARENT_SCOPE)
    endif()

    if(arg_OUT_VAR_RELATIVE_SPDX_V2_JSON_DOCUMENT_PATH)
        get_target_property(document_spdx_v2_json_relative_path "${target}"
            _qt_sbom_spdx_v2_document_json_relative_path)
        set(${arg_OUT_VAR_RELATIVE_SPDX_V2_JSON_DOCUMENT_PATH}
            "${document_spdx_v2_json_relative_path}" PARENT_SCOPE)
    endif()

    if(arg_OUT_VAR_RELATIVE_CYDX_V1_6_JSON_DOCUMENT_PATH)
        get_target_property(document_cydx_v1_6_json_relative_path "${target}"
            _qt_sbom_cydx_v1_6_document_json_relative_path)
        set(${arg_OUT_VAR_RELATIVE_CYDX_V1_6_JSON_DOCUMENT_PATH}
            "${document_cydx_v1_6_json_relative_path}" PARENT_SCOPE)
    endif()

    if(arg_OUT_VAR_SPDX_V2_DOCUMENT_REF_ID)
        get_target_property(spdx_external_document_ref "${target}"
            _qt_sbom_spdx_v2_external_document_ref)
        set(${arg_OUT_VAR_SPDX_V2_DOCUMENT_REF_ID} "${spdx_external_document_ref}" PARENT_SCOPE)
    endif()

    if(arg_OUT_VAR_SPDX_V2_DOCUMENT_NAMESPACE)
        get_target_property(spdx_document_namespace "${target}"
            _qt_sbom_spdx_repo_document_namespace)
        set(${arg_OUT_VAR_SPDX_V2_DOCUMENT_NAMESPACE} "${spdx_document_namespace}" PARENT_SCOPE)
    endif()

    if(arg_OUT_VAR_CYDX_URN_SERIAL_NUMBER)
        get_target_property(cydx_bom_serial_number "${target}" _qt_sbom_cydx_bom_serial_number_uuid)
        set(${arg_OUT_VAR_CYDX_URN_SERIAL_NUMBER} "${cydx_bom_serial_number}" PARENT_SCOPE)
    endif()

    if(arg_OUT_VAR_CYDX_URN_BOM_VERSION)
        get_target_property(cydx_urn_bom_version "${target}" _qt_sbom_cydx_urn_bom_version)
        set(${arg_OUT_VAR_CYDX_URN_BOM_VERSION} "${cydx_urn_bom_version}" PARENT_SCOPE)
    endif()
endfunction()
