diff --git a/plugins/scanners/fossid/src/main/kotlin/FossId.kt b/plugins/scanners/fossid/src/main/kotlin/FossId.kt index 536a40c81c5b8..c24a0c4ff113a 100644 --- a/plugins/scanners/fossid/src/main/kotlin/FossId.kt +++ b/plugins/scanners/fossid/src/main/kotlin/FossId.kt @@ -467,7 +467,7 @@ class FossId internal constructor( val scanCodeAndId = if (existingScan == null) { logger.info { "No scan found for $url and revision $revision. Creating scan..." } - val scanCode = namingProvider.createScanCode(projectName) + val scanCode = namingProvider.createScanCode(projectName = projectName, branch = revision) val newUrl = urlProvider.getUrl(url) val scanId = createScan(projectCode, scanCode, newUrl, revision) @@ -512,10 +512,6 @@ class FossId internal constructor( if (defaultBranch != null) "Default branch is '$defaultBranch'." else "There is no default remote branch." } - // If a scan for the default branch is created, put the default branch name in the scan code (the - // FossIdNamingProvider must also have a scan pattern that makes use of it). - val branchLabel = projectRevision.takeIf { defaultBranch == projectRevision }.orEmpty() - if (projectRevision == null) { logger.warn { "No project revision has been given." } } else { @@ -547,13 +543,13 @@ class FossId internal constructor( logger.info { "No scan found for $mappedUrlWithoutCredentials and revision $revision. Creating origin scan..." } - namingProvider.createScanCode(projectName, DeltaTag.ORIGIN, branchLabel) + namingProvider.createScanCode(projectName, DeltaTag.ORIGIN, revision) } else { logger.info { "Scan '${existingScan.code}' found for $mappedUrlWithoutCredentials and revision $revision." } logger.info { "Existing scan has for reference(s): ${existingScan.comment.orEmpty()}. Creating delta scan..." } - namingProvider.createScanCode(projectName, DeltaTag.DELTA, branchLabel) + namingProvider.createScanCode(projectName, DeltaTag.DELTA, revision) } val scanId = createScan(projectCode, scanCode, mappedUrl, revision, projectRevision.orEmpty()) diff --git a/plugins/scanners/fossid/src/main/kotlin/FossIdNamingProvider.kt b/plugins/scanners/fossid/src/main/kotlin/FossIdNamingProvider.kt index 596380369f411..3220c795b82b4 100644 --- a/plugins/scanners/fossid/src/main/kotlin/FossIdNamingProvider.kt +++ b/plugins/scanners/fossid/src/main/kotlin/FossIdNamingProvider.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021 The ORT Project Authors (see ) + * Copyright (C) 2024 The ORT Project Authors (see ) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,6 +36,7 @@ import org.apache.logging.log4j.kotlin.logger * * **projectName**: The name of the project (i.e. the part of the URL before .git). * * **currentTimestamp**: The current time. * * **deltaTag** (scan code only): If delta scans is enabled, this qualifies the scan as an *origin* scan or a *delta* + * * **branch**: branch name (revision) given to scan * scan. */ class FossIdNamingProvider( @@ -46,6 +47,8 @@ class FossIdNamingProvider( companion object { @JvmStatic val FORMATTER: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss") + + const val MAX_SCAN_CODE_LEN = 254 } fun createProjectCode(projectName: String): String = @@ -58,15 +61,42 @@ class FossIdNamingProvider( fun createScanCode(projectName: String, deltaTag: FossId.DeltaTag? = null, branch: String = ""): String { var defaultPattern = "#projectName_#currentTimestamp" - val builtins = mutableMapOf("#projectName" to projectName, "#branch" to branch) + val builtins = mutableMapOf("#projectName" to projectName) deltaTag?.let { defaultPattern += "_#deltaTag" builtins += "#deltaTag" to deltaTag.name.lowercase() } - val pattern = namingScanPattern ?: defaultPattern - return replaceNamingConventionVariables(pattern, builtins, namingConventionVariables) + if (branch.isNotBlank()) { + val branchName = normalizeBranchName(branch, defaultPattern, builtins) + defaultPattern += "_#branch" + builtins += "#branch" to branchName + } + + return replaceNamingConventionVariables( + namingScanPattern ?: defaultPattern, builtins, namingConventionVariables + ) + } + + /** + * Replaces non-standard characters in branch name and trimming it's length to one that will not exceed + * maximum length of FossID scan ID, when combined with rest of variables + */ + private fun normalizeBranchName( + branch: String, + defaultPattern: String, + scanCodeVariables: Map + ): String { + val noBranchScanCodeLength = + replaceNamingConventionVariables( + namingScanPattern ?: defaultPattern, + scanCodeVariables, + namingConventionVariables + ).length + + val maxBranchNameLength = MAX_SCAN_CODE_LEN - noBranchScanCodeLength + return branch.replace(Regex("[^a-zA-Z0-9-_]"), "_").take(maxBranchNameLength) } /** diff --git a/plugins/scanners/fossid/src/test/kotlin/FossIdNamingProviderTest.kt b/plugins/scanners/fossid/src/test/kotlin/FossIdNamingProviderTest.kt new file mode 100644 index 0000000000000..c31ee880ae460 --- /dev/null +++ b/plugins/scanners/fossid/src/test/kotlin/FossIdNamingProviderTest.kt @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2021 The ORT Project Authors (see ) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +package org.ossreviewtoolkit.plugins.scanners.fossid + +import io.kotest.core.spec.style.WordSpec +import io.kotest.matchers.equals.shouldBeEqual +import io.kotest.matchers.ints.shouldBeLessThanOrEqual + +import io.mockk.every +import io.mockk.mockkStatic + +import java.time.LocalDateTime + +class FossIdNamingProviderTest : WordSpec({ + "createScanCode" should { + val namingProvider = FossIdNamingProvider(null, null, emptyMap()) + + val mockedDateTime = LocalDateTime.of(2024, 4, 1, 10, 0) + val expectedTimestamp = "20240401_100000" + + "create code without branch name, when it's empty" { + mockkStatic(LocalDateTime::class) { + every { LocalDateTime.now() } returns mockedDateTime + + namingProvider.createScanCode( + "example-project-name", null, "" + ) shouldBeEqual "example-project-name_$expectedTimestamp" + } + } + + "create code with branch name" { + mockkStatic(LocalDateTime::class) { + every { LocalDateTime.now() } returns mockedDateTime + + namingProvider.createScanCode( + "example-project-name", null, "CODE-2233_Red-dots-added-to-layout" + ) shouldBeEqual "example-project-name_" + expectedTimestamp + "_CODE-2233_Red-dots-added-to-layout" + } + } + + "create code with branch name and delta tag" { + mockkStatic(LocalDateTime::class) { + every { LocalDateTime.now() } returns mockedDateTime + + namingProvider.createScanCode( + "example-project-name", FossId.DeltaTag.DELTA, "CODE-2233_Red-dots-added-to-layout" + ) shouldBeEqual "example-project-name_" + expectedTimestamp + + "_delta_CODE-2233_Red-dots-added-to-layout" + } + } + + "remove all non-standard signs from branch name when creating code" { + mockkStatic(LocalDateTime::class) { + every { LocalDateTime.now() } returns mockedDateTime + + namingProvider.createScanCode( + "example-project-name", null, "feature/CODE-12%%$@@&^_SOME_*&^#!*text!!" + ) shouldBeEqual "example-project-name_" + + expectedTimestamp + "_feature_CODE-12________SOME_______text__" + } + } + + "truncate very long scan id to fit maximum length accepted by FossID (255 chars)" { + val veryLongBranchName = + "origin/feature/CODE-123321_some_more_detailed_description_of_what_that_feature_doing_just_to_make_" + + "it_as_descriptive_as_it's_possible_otherwise_this_test_is_pointless_so_lets_add_some_more_" + + "characters_to_test_it_properly_to_avoid_any_mistake_so_lets_add_some_even_more_to_it_yolo" + + mockkStatic(LocalDateTime::class) { + every { LocalDateTime.now() } returns mockedDateTime + + namingProvider.createScanCode( + "example-project-name", FossId.DeltaTag.DELTA, veryLongBranchName + ).length shouldBeLessThanOrEqual 255 + } + } + } +})