New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
CBOR Feature Drop: COSE #2412
base: dev
Are you sure you want to change the base?
CBOR Feature Drop: COSE #2412
Conversation
Hi, thanks for your PR! Just as a quick heads-up so you know I've seen it — I'm going on vacation soon, so I'll be able to properly review it in September. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi, I appreciate the amount of work you've done — especially for the tests. I didn't look into the implementation (yet), focusing on surface API for now.
Regarding failure on CI — it's connected to the fact that Long.toUnsignedString
was added only in Android API 26. However, it is possible to use this declaration on Android with desugaring enabled, so there shouldn't be any problems. You have to crate a special annotation @SuppressAnimalSniffer
(it already exists for core and json, but not for cbor, see e.g. here:
kotlinx.serialization/formats/json/commonMain/src/kotlinx/serialization/json/JsonElement.kt
Line 92 in d192d24
@SuppressAnimalSniffer // Long.toUnsignedString(long) |
kotlinx.serialization/build.gradle
Line 205 in a87b0f1
switch (name) { |
formats/cbor/commonMain/src/kotlinx/serialization/cbor/ByteStringWrapper.kt
Show resolved
Hide resolved
formats/cbor/commonMain/src/kotlinx/serialization/cbor/ByteStringWrapper.kt
Show resolved
Hide resolved
formats/cbor/commonMain/src/kotlinx/serialization/cbor/SerialLabel.kt
Outdated
Show resolved
Hide resolved
@SerialInfo | ||
@Target(AnnotationTarget.PROPERTY) | ||
@ExperimentalSerializationApi | ||
public annotation class ValueTags(@OptIn(ExperimentalUnsignedTypes::class) vararg val tags: ULong) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally, it is recommended to propagate opt-in in the libraries (see here: https://kotlinlang.org/docs/opt-in-requirements.html#propagating-opt-in and here: https://kotlinlang.org/docs/jvm-api-guidelines-backward-compatibility.html#the-requiresoptin-annotation). The problem here is, while ULong
is already stable, ULongArray
which is automatically inserted by vararg
is not.
I'm also not sure if it is very common for a property to have multiple tags. Have you considered using @Repeatable
annotation instead of vararg (it works only with Kotlin 1.9.0 though, because of the #2099) or not providing such functionality at all?
I don't mind propagating ExperimentalUnsignedTypes, however, if this is necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting! To be frank, I did not even think about@Repeatable
, because vararg
seemed intuitive to use with minimum boilerplate when defining serializable data types using multiple tags, as the declaration directly reflects what's happening in the resulting CBOR byte string.
We discussed this internally and neither of us has a string opinion on either way of providing this feature. So if @Repeatable
is preferred, we'll, of course, go with that!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you try it out? If it works for you, I think it may be better that way
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After starting to refactor KeyTags
and ValueTags
to @Repeatable
, I notices that this would make KeyTags
and ValueTags
behave differently from the @CborArray
annotation, which requires the vararg syntax to make sense. IMO this change would therefore only make tagging inconsistent, resulting in API inconsistencies and more diverse (and hence a little more complex) code internally.
Therefore, I am now against this change. Of course, if you still prefer it we'll comply.
formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoding.kt
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't look at the test thoroughly (to understand if there is a need for more test cases), but I highlighted all of the problematic moments from my perspective. Don't forget to press 'show hidden conversations' on Github :)
* Note that `equals()` and `hashCode()` only use `value`, not `serialized`. | ||
*/ | ||
@Serializable(with = ByteStringWrapperSerializer::class) | ||
public class ByteStringWrapper<T>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NB: it seems you need to update apiDump
since this class is no longer data
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sorry, done!
@Serializable(with = ByteStringWrapperSerializer::class) | ||
public class ByteStringWrapper<T>( | ||
public val value: T, | ||
public val serialized: ByteArray = byteArrayOf() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've skimmed through the diff and can't find any usages of this value. Is there any reason to be a val
? In what cases will the user be interested in accessing it? Also, it is very easy to create inconsistent data with this approach (e.g. ByteStringWrapper(original.value, byteArrayOf(garbage))
). If it is not necessary to have it, this class can be a value class
. Or even no class would be needed, as this can be handled with yet another @SerialInfo
annotation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nodh can you take care of this, please?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'll use the CBOR serialization for ISO 18013-5 compliaent mobile driving licences. There verifiers need to check that (the digest of) the serialized byte values appear in another structure (signed by the issuer). So in that case its quite useful to have the bytes as they were serialized (and parsed) in the deserialized ByteStringWrapper
object too.
internal val writeValueTags: Boolean, | ||
internal val verifyKeyTags: Boolean, | ||
internal val verifyValueTags: Boolean, | ||
internal val writeDefiniteLengths: Boolean, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one is not documented as @param
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sorry, done!
@SerialInfo | ||
@Target(AnnotationTarget.CLASS) | ||
@ExperimentalSerializationApi | ||
public annotation class CborArray(@OptIn(ExperimentalUnsignedTypes::class) vararg val tag: ULong) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
vararg val tag
is still undocumented though
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sorry! done!
readProperties++ | ||
descriptor.getElementIndexOrThrow(elemName) | ||
descriptor.getElementIndexOrThrow(elemName).also { index -> | ||
if (cbor.verifyKeyTags) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This block of duplicated code can be extracted to function (perhaps local)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
} else { | ||
if (cbor.verifyKeyTags) { | ||
descriptor.getKeyTags(index)?.let { keyTags -> | ||
if (!(keyTags contentEquals tags)) throw CborDecodingException("CBOR tags $tags do not match declared tags $keyTags") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
General observation (for all throw
expressions): It is would be much easier to debug errors if they also included locations. Here we don't have a json path analog, but appending descriptor.toString()
will already make the job easier significantly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done where we have access to the descriptor (which we do not always have)
readByte() | ||
} | ||
|
||
return (if (collectedTags.isEmpty()) null else collectedTags.toULongArray()).also { collected -> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO also
creates unnecessary nesting here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it needs to be transformed into an ULongArray
for comparison and for returning it. the alternative of adding a temp variable just for two accesses seems a bit cumbersome to me
|
||
return (if (collectedTags.isEmpty()) null else collectedTags.toULongArray()).also { collected -> | ||
tags?.let { | ||
if (!it.contentEquals(collected)) throw CborDecodingException( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that contentEquals
has both nullable receiver and parameter. It results in behavior like this: intArrayOf(1,2,3).contentEquals(null) -> false
. I'm not sure it is what you intended to have here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We only want to compare if tags are actually set. Otherwise, we don't care.
I'll also add that comment to the code.
@@ -633,9 +952,55 @@ private fun Iterable<ByteArray>.flatten(): ByteArray { | |||
|
|||
@OptIn(ExperimentalSerializationApi::class) | |||
private fun SerialDescriptor.isByteString(index: Int): Boolean { | |||
return getElementAnnotations(index).find { it is ByteString } != null | |||
return kotlin.runCatching { getElementAnnotations(index).find { it is ByteString } != null }.getOrDefault(false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why runCatching
is needed here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's only a theoretical possibility, but an IndexOutOfBoundsException
could be thrown. I'll remove it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
well, such a call fails for the WASM target, even though we use runCatching
. I have no explanation
It would be awesome, as currently it's somewhat surprising that the default encoded size of byte arrays is nearly twice as large. |
WASM still fails, because even |
Here are the details:
EDIT: seems it's already been reported and there's really not much we can do: https://youtrack.jetbrains.com/issue/KT-59081/WASM-Cant-catch-index-out-of-bounds-and-divide-by-0-exceptions |
This PR obsoletes #2371 and #2359 as it contains the features of both PRs and many more.
Specifically, this PR contains all feature required to serialize and parse COSE-compliant CBOR (thanks to @nodh). While some canonicalization steps (such as sorting keys) still needs to be performed manually. It does get the job done quite well. Namely, we have successfully used the features introduced here to create and validate ISO/IEC 18013-5:2021-compliant mobile driving license data.
This PR introduces the following features to the CBOR format:
null
complex objects as empty map (to be COSE-compliant)