Skip to content

Commit

Permalink
Add lenientNbtNames option
Browse files Browse the repository at this point in the history
  • Loading branch information
BenWoodworth committed Sep 14, 2024
1 parent 5be8c7a commit deaae5f
Show file tree
Hide file tree
Showing 13 changed files with 140 additions and 29 deletions.
3 changes: 3 additions & 0 deletions src/commonMain/kotlin/BedrockNbtConfiguration.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ package net.benwoodworth.knbt
public class BedrockNbtConfiguration internal constructor(
override val encodeDefaults: Boolean,
override val ignoreUnknownKeys: Boolean,
override val lenientNbtNames: Boolean,
override val compression: NbtCompression,
override val compressionLevel: Int?,
) : BinaryNbtFormatConfiguration() {
override fun toString(): String =
"BedrockNbtConfiguration(" +
"encodeDefaults=$encodeDefaults" +
", ignoreUnknownKeys=$ignoreUnknownKeys" +
", lenientNbtNames=$lenientNbtNames" +
", compression=$compression" +
", compressionLevel=$compressionLevel" +
")"
Expand All @@ -25,6 +27,7 @@ public class BedrockNbtBuilder internal constructor(nbt: BedrockNbt?) : BinaryNb
configuration = BedrockNbtConfiguration(
encodeDefaults = encodeDefaults,
ignoreUnknownKeys = ignoreUnknownKeys,
lenientNbtNames = lenientNbtNames,
compression = getConfiguredCompression(),
compressionLevel = compressionLevel,
),
Expand Down
3 changes: 3 additions & 0 deletions src/commonMain/kotlin/BedrockNetworkNbtConfiguration.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package net.benwoodworth.knbt
public class BedrockNetworkNbtConfiguration internal constructor(
override val encodeDefaults: Boolean,
override val ignoreUnknownKeys: Boolean,
override val lenientNbtNames: Boolean,
override val compression: NbtCompression,
override val compressionLevel: Int?,
public val protocolVersion: Int,
Expand All @@ -11,6 +12,7 @@ public class BedrockNetworkNbtConfiguration internal constructor(
"BedrockNetworkNbtConfiguration(" +
"encodeDefaults=$encodeDefaults" +
", ignoreUnknownKeys=$ignoreUnknownKeys" +
", lenientNbtNames=$lenientNbtNames" +
", compression=$compression" +
", compressionLevel=$compressionLevel" +
", protocolVersion=$protocolVersion" +
Expand Down Expand Up @@ -44,6 +46,7 @@ public class BedrockNetworkNbtBuilder internal constructor(nbt: BedrockNetworkNb
configuration = BedrockNetworkNbtConfiguration(
encodeDefaults = encodeDefaults,
ignoreUnknownKeys = ignoreUnknownKeys,
lenientNbtNames = lenientNbtNames,
compression = getConfiguredCompression(),
compressionLevel = compressionLevel,
protocolVersion = getConfiguredProtocolVersion(),
Expand Down
3 changes: 3 additions & 0 deletions src/commonMain/kotlin/JavaNbtConfiguration.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ package net.benwoodworth.knbt
public class JavaNbtConfiguration internal constructor(
override val encodeDefaults: Boolean,
override val ignoreUnknownKeys: Boolean,
override val lenientNbtNames: Boolean,
override val compression: NbtCompression,
override val compressionLevel: Int?,
) : BinaryNbtFormatConfiguration() {
override fun toString(): String =
"JavaNbtConfiguration(" +
"encodeDefaults=$encodeDefaults" +
", ignoreUnknownKeys=$ignoreUnknownKeys" +
", lenientNbtNames=$lenientNbtNames" +
", compression=$compression" +
", compressionLevel=$compressionLevel" +
")"
Expand All @@ -25,6 +27,7 @@ public class JavaNbtBuilder internal constructor(nbt: JavaNbt?) : BinaryNbtForma
configuration = JavaNbtConfiguration(
encodeDefaults = encodeDefaults,
ignoreUnknownKeys = ignoreUnknownKeys,
lenientNbtNames = lenientNbtNames,
compression = getConfiguredCompression(),
compressionLevel = compressionLevel,
),
Expand Down
3 changes: 3 additions & 0 deletions src/commonMain/kotlin/JavaNetworkNbtConfiguration.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package net.benwoodworth.knbt
public class JavaNetworkNbtConfiguration internal constructor(
override val encodeDefaults: Boolean,
override val ignoreUnknownKeys: Boolean,
override val lenientNbtNames: Boolean,
override val compression: NbtCompression,
override val compressionLevel: Int?,
public val protocolVersion: Int,
Expand All @@ -11,6 +12,7 @@ public class JavaNetworkNbtConfiguration internal constructor(
"JavaNetworkNbtConfiguration(" +
"encodeDefaults=$encodeDefaults" +
", ignoreUnknownKeys=$ignoreUnknownKeys" +
", lenientNbtNames=$lenientNbtNames" +
", compression=$compression" +
", compressionLevel=$compressionLevel" +
", protocolVersion=$protocolVersion" +
Expand Down Expand Up @@ -58,6 +60,7 @@ public class JavaNetworkNbtBuilder internal constructor(nbt: JavaNetworkNbt?) :
configuration = JavaNetworkNbtConfiguration(
encodeDefaults = encodeDefaults,
ignoreUnknownKeys = ignoreUnknownKeys,
lenientNbtNames = lenientNbtNames,
compression = getConfiguredCompression(),
compressionLevel = compressionLevel,
protocolVersion = protocolVersion,
Expand Down
1 change: 1 addition & 0 deletions src/commonMain/kotlin/Nbt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public open class Nbt internal constructor(
configuration = NbtConfiguration(
encodeDefaults = NbtFormatDefaults.encodeDefaults,
ignoreUnknownKeys = NbtFormatDefaults.ignoreUnknownKeys,
lenientNbtNames = NbtFormatDefaults.lenientNbtNames,
),
serializersModule = NbtFormatDefaults.serializersModule,
)
Expand Down
3 changes: 3 additions & 0 deletions src/commonMain/kotlin/NbtConfiguration.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ package net.benwoodworth.knbt
public class NbtConfiguration internal constructor(
override val encodeDefaults: Boolean,
override val ignoreUnknownKeys: Boolean,
override val lenientNbtNames: Boolean,
) : NbtFormatConfiguration() {
override fun toString(): String =
"NbtConfiguration(" +
"encodeDefaults=$encodeDefaults" +
", ignoreUnknownKeys=$ignoreUnknownKeys" +
", lenientNbtNames=$lenientNbtNames" +
")"
}

Expand All @@ -17,6 +19,7 @@ public class NbtBuilder(nbt: NbtFormat? = null) : NbtFormatBuilder(nbt) {
configuration = NbtConfiguration(
encodeDefaults = encodeDefaults,
ignoreUnknownKeys = ignoreUnknownKeys,
lenientNbtNames = lenientNbtNames,
),
serializersModule = serializersModule,
)
Expand Down
10 changes: 10 additions & 0 deletions src/commonMain/kotlin/NbtFormatConfiguration.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import kotlinx.serialization.modules.SerializersModule
public abstract class NbtFormatConfiguration internal constructor() {
public abstract val encodeDefaults: Boolean
public abstract val ignoreUnknownKeys: Boolean
public abstract val lenientNbtNames: Boolean

abstract override fun toString(): String
}
Expand All @@ -15,6 +16,7 @@ public abstract class NbtFormatConfiguration internal constructor() {
internal object NbtFormatDefaults {
const val encodeDefaults: Boolean = false
const val ignoreUnknownKeys: Boolean = false
const val lenientNbtNames: Boolean = false
val serializersModule: SerializersModule = EmptySerializersModule()
}

Expand All @@ -34,6 +36,14 @@ public abstract class NbtFormatBuilder internal constructor(nbt: NbtFormat?) {
public var ignoreUnknownKeys: Boolean =
nbt?.configuration?.ignoreUnknownKeys ?: NbtFormatDefaults.ignoreUnknownKeys

/**
* Specifies whether a root [NbtName] mismatch should be ignored when deserializing named [NbtFormat]s
* instead of throwing [SerializationException].
* `false` by default.
*/
public var lenientNbtNames: Boolean =
nbt?.configuration?.lenientNbtNames ?: NbtFormatDefaults.lenientNbtNames

/**
* Module with contextual and polymorphic serializers to be used in the resulting [NbtFormat] instance.
*/
Expand Down
1 change: 1 addition & 0 deletions src/commonMain/kotlin/StringifiedNbt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public open class StringifiedNbt internal constructor(
configuration = StringifiedNbtConfiguration(
encodeDefaults = NbtFormatDefaults.encodeDefaults,
ignoreUnknownKeys = NbtFormatDefaults.ignoreUnknownKeys,
lenientNbtNames = NbtFormatDefaults.lenientNbtNames,
prettyPrint = StringifiedNbtDefaults.prettyPrint,
prettyPrintIndent = StringifiedNbtDefaults.prettyPrintIndent,
),
Expand Down
3 changes: 3 additions & 0 deletions src/commonMain/kotlin/StringifiedNbtConfiguration.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package net.benwoodworth.knbt
public class StringifiedNbtConfiguration internal constructor(
override val encodeDefaults: Boolean,
override val ignoreUnknownKeys: Boolean,
override val lenientNbtNames: Boolean,
public val prettyPrint: Boolean,
@ExperimentalNbtApi
public val prettyPrintIndent: String,
Expand All @@ -12,6 +13,7 @@ public class StringifiedNbtConfiguration internal constructor(
"StringifiedNbtConfiguration(" +
"encodeDefaults=$encodeDefaults" +
", ignoreUnknownKeys=$ignoreUnknownKeys" +
", lenientNbtNames=$lenientNbtNames" +
", prettyPrint=$prettyPrint" +
", prettyPrintIndent='$prettyPrintIndent'" +
")"
Expand Down Expand Up @@ -63,6 +65,7 @@ public class StringifiedNbtBuilder internal constructor(nbt: StringifiedNbt?) :
configuration = StringifiedNbtConfiguration(
encodeDefaults = encodeDefaults,
ignoreUnknownKeys = ignoreUnknownKeys,
lenientNbtNames = lenientNbtNames,
prettyPrint = prettyPrint,
prettyPrintIndent = prettyPrintIndent,
),
Expand Down
14 changes: 9 additions & 5 deletions src/commonMain/kotlin/internal/NbtReaderDecoder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,15 @@ internal abstract class BaseNbtDecoder : AbstractNbtDecoder() {
if (verifiedNbtName) return
verifiedNbtName = true

if (!nbt.capabilities.namedRoot || !context.isSerializingRootValue) return // No need to verify
if (!nbt.capabilities.namedRoot || nbt.configuration.lenientNbtNames || !context.isSerializingRootValue) {
return // No need to verify
}

if (descriptor.nbtName != decodedTagName && !descriptor.nbtNameIsDynamic) {
val message = "Encountered root NBT name '$decodedTagName', but expected '${descriptor.nbtName}'.\n" +
"Use 'lenientNbtNames = true' in NBT builder to ignore mismatched names."

val name = descriptor.nbtName
if (name != decodedTagName && !descriptor.nbtNameIsDynamic) {
throw NbtDecodingException(context, "Expected tag named '$name', but got '$decodedTagName'")
throw NbtDecodingException(context, message)
}
}

Expand Down Expand Up @@ -307,7 +311,7 @@ private class ClassNbtDecoder(
override val decodedTagType: NbtTagType
get() = compoundEntryInfo.type

override val decodedTagName: String?
override val decodedTagName: String? // TODO Remove
get() = compoundEntryInfo.name

init {
Expand Down
60 changes: 60 additions & 0 deletions src/commonTest/kotlin/NbtFormatConfigurationTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@ package net.benwoodworth.knbt
import com.benwoodworth.parameterize.parameter
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerializationException
import net.benwoodworth.knbt.internal.NbtDecodingException
import net.benwoodworth.knbt.test.filter
import net.benwoodworth.knbt.test.parameterizeTest
import net.benwoodworth.knbt.test.parameters.parameterOfDecoderVerifyingNbt
import net.benwoodworth.knbt.test.parameters.parameterOfSerializableTypeEdgeCases
import net.benwoodworth.knbt.test.parameters.serializer
import net.benwoodworth.knbt.test.withNbtName
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
Expand Down Expand Up @@ -62,4 +67,59 @@ class NbtFormatConfigurationTest {
)
}
}

@Test
fun should_throw_for_mismatched_root_name() = parameterizeTest(recordFailures = 100) {
val rootName = "expected_root_name"
val encodedRootName = "encoded_root_name"

val nbt by parameterOfDecoderVerifyingNbt()
.filter { it.capabilities.namedRoot }

val serializableType by parameterOfSerializableTypeEdgeCases()

val failure = assertFailsWith<NbtDecodingException> {
nbt.verifyDecoder(
serializableType.serializer().withNbtName(rootName),
NbtNamed(encodedRootName, serializableType.valueTag),
)
}

assertEquals(
"Encountered root NBT name '$encodedRootName', but expected '$rootName'.\n" +
"Use '${NbtFormatBuilder::lenientNbtNames.name} = true' in NBT builder to ignore mismatched names.",
failure.message,
"message"
)
}

@Test
fun should_not_throw_for_mismatched_root_name_in_unnamed_formats() = parameterizeTest {
val rootName = "expected_root_name"
val encodedRootName = "encoded_root_name"

val nbt by parameterOfDecoderVerifyingNbt()
.filter { !it.capabilities.namedRoot }

val serializableType by parameterOfSerializableTypeEdgeCases()

nbt.verifyDecoder(
serializableType.serializer().withNbtName(rootName),
NbtNamed(encodedRootName, serializableType.valueTag),
)
}

@Test
fun should_not_throw_for_mismatched_root_name_with_lenient_nbt_names() = parameterizeTest {
val rootName = "expected_root_name"
val encodedRootName = "encoded_root_name"

val nbt by parameterOfDecoderVerifyingNbt { lenientNbtNames = true }
val serializableType by parameterOfSerializableTypeEdgeCases()

nbt.verifyDecoder(
serializableType.serializer().withNbtName(rootName),
NbtNamed(encodedRootName, serializableType.valueTag),
)
}
}
24 changes: 0 additions & 24 deletions src/commonTest/kotlin/NbtNameTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -164,30 +164,6 @@ class NbtNameTest {
it as List<SerializableValueWithNbtName<Any?>> // KT-68606: Remove cast
}

@Test
fun decoding_value_with_static_NBT_name_should_fail_with_different_name() = parameterizeTest {
val nbt by parameterOfDecoderVerifyingNbt()
.filter { it.capabilities.namedRoot }

val value by parameter(valuesWithStaticNbtNames)
val name = value.serializer.descriptor.nbtName

val differentlyNamedNbtTag = buildNbtCompound("different_than_$name") {
// No elements, since the decoder should fail before reaching this point anyway
}

val failure = assertFailsWith<NbtDecodingException> {
nbt.verifyDecoder(value.serializer, differentlyNamedNbtTag)
}

assertEquals(
"Expected tag named '$name', but got '${differentlyNamedNbtTag.name}'",
failure.message,
"failure message"
)
}


@Test
fun should_not_fail_decoding_a_different_NBT_name_when_dynamic() = parameterizeTest {
val nbt by parameterOfDecoderVerifyingNbt()
Expand Down
41 changes: 41 additions & 0 deletions src/commonTest/kotlin/test/NbtNameUtil.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package net.benwoodworth.knbt.test

import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import net.benwoodworth.knbt.NbtName

private class WithNbtNameSerializer<T>( // TODO Add in previous commit
val serializer: KSerializer<T>,
nbtName: String
) : KSerializer<T> {
override val descriptor: SerialDescriptor =
serializer.descriptor.withNbtName(nbtName)

override fun serialize(encoder: Encoder, value: T): Unit =
encoder.encodeSerializableValue(serializer, value)

override fun deserialize(decoder: Decoder): T =
decoder.decodeSerializableValue(serializer)
}

@OptIn(ExperimentalSerializationApi::class)
private data class WithNbtNameSerialDescriptor(
val serialDescriptor: SerialDescriptor,
val nbtName: String,
) : SerialDescriptor by serialDescriptor {
override val serialName: String = "WithNbtName<${serialDescriptor.serialName}>($nbtName)"

override val annotations: List<Annotation> =
serialDescriptor.annotations
.filter { it !is NbtName }
.plus(NbtName(nbtName))
}

fun <T> KSerializer<T>.withNbtName(nbtName: String): KSerializer<T> =
WithNbtNameSerializer(this, nbtName)

fun SerialDescriptor.withNbtName(nbtName: String): SerialDescriptor =
WithNbtNameSerialDescriptor(this, nbtName)

0 comments on commit deaae5f

Please sign in to comment.