diff --git a/model/src/main/kotlin/FileFormat.kt b/model/src/main/kotlin/FileFormat.kt index f11d5e8b4191..faf00fb4f326 100644 --- a/model/src/main/kotlin/FileFormat.kt +++ b/model/src/main/kotlin/FileFormat.kt @@ -22,9 +22,10 @@ package org.ossreviewtoolkit.model import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.convertValue -import com.fasterxml.jackson.module.kotlin.readValue +import com.fasterxml.jackson.module.kotlin.readValues import java.io.File +import java.io.IOException import org.ossreviewtoolkit.utils.common.safeMkdirs @@ -94,9 +95,20 @@ fun File.mapper() = FileFormat.forFile(this).mapper fun File.readTree(): JsonNode = mapper().readTree(this) /** - * Use the Jackson mapper returned from [File.mapper] to read an object of type [T] from this file. + * Use the Jackson mapper returned from [File.mapper] to read a single object of type [T] from this file. Throw an + * [IOException] if not exactly one value is contained in the file, e.g. in case of multiple YAML documents per file. */ -inline fun File.readValue(): T = mapper().readValue(this) +inline fun File.readValue(): T { + val mapper = mapper() + val parser = mapper.factory.createParser(this) + + val values = mapper.readValues(parser).readAll().also { + if (it.isEmpty()) throw IOException("No object found in file '$this'.") + if (it.size > 1) throw IOException("Multiple top-level objects found in file '$this'.") + } + + return values.first() +} /** * Use the Jackson mapper returned from [File.mapper] to read an object of type [T] from this file, or return null if diff --git a/model/src/test/kotlin/FileFormatTest.kt b/model/src/test/kotlin/FileFormatTest.kt index 82ce3060ba33..5831615c5f0d 100644 --- a/model/src/test/kotlin/FileFormatTest.kt +++ b/model/src/test/kotlin/FileFormatTest.kt @@ -22,6 +22,7 @@ package org.ossreviewtoolkit.model import com.fasterxml.jackson.core.JsonParseException import io.kotest.assertions.throwables.shouldThrow +import io.kotest.assertions.throwables.shouldThrowWithMessage import io.kotest.core.spec.style.WordSpec import io.kotest.engine.spec.tempfile import io.kotest.matchers.file.shouldHaveFileSize @@ -58,6 +59,28 @@ class FileFormatTest : WordSpec({ file.readValue() } } + + "refuse to read multiple documents per file" { + val file = tempfile(null, ".yml").apply { + @Suppress("MaxLineLength") + writeText( + """ + --- + id: "Maven:dom4j:dom4j:1.6.1" + source_artifact_url: "https://repo.maven.apache.org/maven2/dom4j/dom4j/1.6.1/dom4j-1.6.1-sources.jar" + --- + id: "Maven:dom4j:dom4j:1.6.1" + source_artifact_url: "/dom4j-1.6.1-sources.jar" + """.trimIndent() + ) + } + + shouldThrowWithMessage( + "Multiple top-level objects found in file '$file'." + ) { + file.readValue() + } + } } "File.readValueOrNull()" should {