Skip to content
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

Implement copyElement Method for the TlvWriter Class in Kotlin #26377

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/controller/java/src/chip/tlv/TlvReader.kt
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,19 @@ class TlvReader(bytes: ByteArray) : Iterable<Element> {
return Element(tag, value)
}

/**
* Reads the next element from the TLV. Unlike nextElement() this method leaves the TLV reader
* positioned at the same element and doesn't advance it to the next element.
*
* @throws TlvParsingException if the TLV data was invalid
*/
fun peekElement(): Element {
val currentIndex = index
val element = nextElement()
index = currentIndex
return element
}

/**
* Reads the encoded Long value and advances to the next element.
*
Expand Down
68 changes: 62 additions & 6 deletions src/controller/java/src/chip/tlv/TlvWriter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,14 @@ class TlvWriter(initialCapacity: Int = 32) {
"Invalid use of context tag at index ${bytes.size()}: can only be used within a " +
"structure or a list"
}
}

if (containerDepth > 0 && containerType[containerDepth - 1] is ArrayType) {
} else if (containerType[containerDepth - 1] is ArrayType) {
require(tag is AnonymousTag) {
"Invalid element tag at index ${bytes.size()}: elements of an array SHALL be anonymous"
}
} else if (containerType[containerDepth - 1] is StructureType && type !is EndOfContainerType) {
require(tag !is AnonymousTag) {
"Invalid element tag at index ${bytes.size()}: elements of a structure cannot be anonymous"
}
}

if (tag is ContextSpecificTag) {
Expand Down Expand Up @@ -326,6 +328,62 @@ class TlvWriter(initialCapacity: Int = 32) {
return put(Element(AnonymousTag, EndOfContainerValue))
}

/**
* Copies a TLV element from a reader object into the writer.
*
* This method encodes a new TLV element whose type, tag and value are taken from a TlvReader
* object. When the method is called, the supplied reader object is expected to be positioned on
* the source TLV element. The newly encoded element will have the same type, tag and contents as
* the input container. If the supplied element is a TLV container (structure, array or list), the
* entire contents of the container will be copied.
*
* @param reader a TlvReader object positioned at a Tlv element whose tag, type and value should
* be copied. If this method is executed successfully, the reader will be positioned at the end
* of the element that was copied.
*/
fun copyElement(reader: TlvReader): TlvWriter {
return copyElement(reader.peekElement().tag, reader)
}

/**
* Copies a TLV element from a reader object into the writer.
*
* This method encodes a new TLV element whose type and value are taken from a TLVReader object.
* When the method is called, the supplied reader object is expected to be positioned on the
* source TLV element. The newly encoded element will have the same type and contents as the input
* container, however the tag will be set to the specified argument. If the supplied element is a
* TLV container (structure, array or list), the entire contents of the container will be copied.
*
* @param tag the TLV tag to be encoded with the element.
* @param reader a TlvReader object positioned at a Tlv element whose type and value should be
* copied. If this method is executed successfully, the reader will be positioned at the end of
* the element that was copied.
*/
fun copyElement(tag: Tag, reader: TlvReader): TlvWriter {
var depth = 0
do {
val element = reader.nextElement()
val value = element.value

when (depth) {
0 -> {
require(value !is EndOfContainerValue) {
"The TlvReader is positioned at invalid element: EndOfContainer"
}
put(Element(tag, value))
}
else -> put(element)
}

if (value is EndOfContainerValue) {
depth--
} else if (value is StructureValue || value is ArrayValue || value is ListValue) {
depth++
}
} while (depth > 0)
return this
}

/** Returns the total number of bytes written since the writer was initialized. */
fun getLengthWritten(): Int {
return bytes.size()
Expand All @@ -334,9 +392,7 @@ class TlvWriter(initialCapacity: Int = 32) {
/** Verifies that all open containers are closed. */
fun validateTlv(): TlvWriter {
if (containerDepth > 0) {
throw TlvEncodingException(
"Invalid Tlv data at index ${bytes.size()}: $containerDepth containers are not closed"
)
throw TlvEncodingException("Invalid Tlv data: $containerDepth containers are not closed")
}
return this
}
Expand Down
113 changes: 97 additions & 16 deletions src/controller/java/tests/chip/tlv/TlvReadWriteTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ private val testTlvSampleData: ByteArray =
.map { it.toByte() }
.toByteArray()

private val testVendorId: UShort = 0xAABBu
private val testProductId: UShort = 0xCCDDu
private const val TEST_VENDOR_ID: UShort = 0xAABBu
private const val TEST_PRODUCT_ID: UShort = 0xCCDDu

private val testLargeString: String =
"""
Expand All @@ -86,8 +86,8 @@ class TlvReadWriteTest {
@Test
fun testTlvSampleData_write() {
TlvWriter().apply {
startStructure(FullyQualifiedTag(6, testVendorId, testProductId, 1u))
put(FullyQualifiedTag(6, testVendorId, testProductId, 2u), true)
startStructure(FullyQualifiedTag(6, TEST_VENDOR_ID, TEST_PRODUCT_ID, 1u))
put(FullyQualifiedTag(6, TEST_VENDOR_ID, TEST_PRODUCT_ID, 2u), true)
put(ImplicitProfileTag(2, 2u), false)
startArray(ContextSpecificTag(0))
put(AnonymousTag, 42)
Expand All @@ -97,15 +97,15 @@ class TlvReadWriteTest {
startStructure(AnonymousTag)
endStructure()
startList(AnonymousTag)
putNull(FullyQualifiedTag(6, testVendorId, testProductId, 17u))
putNull(FullyQualifiedTag(6, TEST_VENDOR_ID, TEST_PRODUCT_ID, 17u))
putNull(ImplicitProfileTag(4, 900000u))
putNull(AnonymousTag)
startStructure(ImplicitProfileTag(4, 4000000000u))
put(CommonProfileTag(4, 70000u), testLargeString)
endStructure()
endList()
endArray()
put(FullyQualifiedTag(6, testVendorId, testProductId, 5u), "This is a test")
put(FullyQualifiedTag(6, TEST_VENDOR_ID, TEST_PRODUCT_ID, 5u), "This is a test")
put(ImplicitProfileTag(2, 65535u), 17.9f)
put(ImplicitProfileTag(4, 65536u), 17.9)
endStructure()
Expand All @@ -117,8 +117,8 @@ class TlvReadWriteTest {
@Test
fun testTlvSampleData_read() {
TlvReader(testTlvSampleData).apply {
enterStructure(FullyQualifiedTag(6, testVendorId, testProductId, 1u))
assertThat(getBool(FullyQualifiedTag(6, testVendorId, testProductId, 2u))).isEqualTo(true)
enterStructure(FullyQualifiedTag(6, TEST_VENDOR_ID, TEST_PRODUCT_ID, 1u))
assertThat(getBool(FullyQualifiedTag(6, TEST_VENDOR_ID, TEST_PRODUCT_ID, 2u))).isEqualTo(true)
assertThat(getBool(ImplicitProfileTag(2, 2u))).isEqualTo(false)
enterArray(ContextSpecificTag(0))
assertThat(getInt(AnonymousTag)).isEqualTo(42)
Expand All @@ -128,15 +128,15 @@ class TlvReadWriteTest {
enterStructure(AnonymousTag)
exitContainer()
enterList(AnonymousTag)
getNull(FullyQualifiedTag(6, testVendorId, testProductId, 17u))
getNull(FullyQualifiedTag(6, TEST_VENDOR_ID, TEST_PRODUCT_ID, 17u))
getNull(ImplicitProfileTag(4, 900000u))
getNull(AnonymousTag)
enterStructure(ImplicitProfileTag(4, 4000000000u))
assertThat(getUtf8String(CommonProfileTag(4, 70000u))).isEqualTo(testLargeString)
exitContainer()
exitContainer()
exitContainer()
assertThat(getUtf8String(FullyQualifiedTag(6, testVendorId, testProductId, 5u)))
assertThat(getUtf8String(FullyQualifiedTag(6, TEST_VENDOR_ID, TEST_PRODUCT_ID, 5u)))
.isEqualTo("This is a test")
assertThat(getFloat(ImplicitProfileTag(2, 65535u))).isEqualTo(17.9f)
assertThat(getDouble(ImplicitProfileTag(4, 65536u))).isEqualTo(17.9)
Expand All @@ -149,13 +149,13 @@ class TlvReadWriteTest {
@Test
fun testTlvSampleData_read_useSkipElementAndExitContinerInTheMiddle() {
TlvReader(testTlvSampleData).apply {
enterStructure(FullyQualifiedTag(6, testVendorId, testProductId, 1u))
enterStructure(FullyQualifiedTag(6, TEST_VENDOR_ID, TEST_PRODUCT_ID, 1u))
skipElement()
assertThat(getBool(ImplicitProfileTag(2, 2u))).isEqualTo(false)
enterArray(ContextSpecificTag(0))
assertThat(getInt(AnonymousTag)).isEqualTo(42)
exitContainer()
assertThat(getUtf8String(FullyQualifiedTag(6, testVendorId, testProductId, 5u)))
assertThat(getUtf8String(FullyQualifiedTag(6, TEST_VENDOR_ID, TEST_PRODUCT_ID, 5u)))
.isEqualTo("This is a test")
assertThat(getFloat(ImplicitProfileTag(2, 65535u))).isEqualTo(17.9f)
assertThat(getDouble(ImplicitProfileTag(4, 65536u))).isEqualTo(17.9)
Expand All @@ -165,6 +165,77 @@ class TlvReadWriteTest {
}
}

@Test
fun testTlvSampleData_copyElement() {
val reader = TlvReader(testTlvSampleData)
val encoding = TlvWriter().copyElement(reader).validateTlv().getEncoded()
assertThat(encoding).isEqualTo(testTlvSampleData)
}

@Test
fun testTlvSampleData_copyElementWithTag() {
val reader = TlvReader(testTlvSampleData)
val encoding =
TlvWriter()
.copyElement(FullyQualifiedTag(6, TEST_VENDOR_ID, TEST_PRODUCT_ID, 1u), reader)
.validateTlv()
.getEncoded()
assertThat(encoding).isEqualTo(testTlvSampleData)
}

@Test
fun testCopyElement_throwsIllegalArgumentException() {
val encoding =
TlvWriter().startStructure(AnonymousTag).endStructure().validateTlv().getEncoded()
val reader = TlvReader(encoding)
reader.skipElement()

// Throws exception because the reader is positioned at the end of container element
assertFailsWith<IllegalArgumentException> { TlvWriter().copyElement(reader) }
}

@Test
fun testCopyElement_replaceTag() {
val tag = CommonProfileTag(2, 1000u)
val encoding =
TlvWriter().startStructure(AnonymousTag).endStructure().validateTlv().getEncoded()
val expectedEncoding = TlvWriter().startStructure(tag).endStructure().validateTlv().getEncoded()

assertThat(TlvWriter().copyElement(tag, TlvReader(encoding)).validateTlv().getEncoded())
.isEqualTo(expectedEncoding)
}

@Test
fun testCopyElementUInt_replaceTag() {
val value = 42U
val tag1 = CommonProfileTag(2, 1u)
val tag2 = CommonProfileTag(2, 2u)
val encoding = TlvWriter().put(tag1, value).validateTlv().getEncoded()
val expectedEncoding = TlvWriter().put(tag2, value).validateTlv().getEncoded()

assertThat(TlvWriter().copyElement(tag2, TlvReader(encoding)).validateTlv().getEncoded())
.isEqualTo(expectedEncoding)
}

@Test
fun testTlvSampleData_copyElementsOneByOne() {
val reader = TlvReader(testTlvSampleData)
reader.skipElement()
val encoding =
TlvWriter()
.startStructure(FullyQualifiedTag(6, TEST_VENDOR_ID, TEST_PRODUCT_ID, 1u))
.copyElement(reader)
.copyElement(reader)
.copyElement(reader)
.copyElement(FullyQualifiedTag(6, TEST_VENDOR_ID, TEST_PRODUCT_ID, 5u), reader)
.copyElement(reader)
.copyElement(reader)
.endStructure()
.validateTlv()
.getEncoded()
assertThat(encoding).isEqualTo(testTlvSampleData)
}

@Test
fun testData_IntMinMax() {
val encodedTlv =
Expand Down Expand Up @@ -378,7 +449,7 @@ class TlvReadWriteTest {

// Throws exception because the encoded value has AnonymousTag tag
assertFailsWith<IllegalArgumentException> {
TlvReader(encoding).getLong(FullyQualifiedTag(6, testVendorId, testProductId, 5u))
TlvReader(encoding).getLong(FullyQualifiedTag(6, TEST_VENDOR_ID, TEST_PRODUCT_ID, 5u))
}
}

Expand Down Expand Up @@ -820,6 +891,16 @@ class TlvReadWriteTest {
}
}

@Test
fun encodeAnonymousTagInStructure_throwsIllegalArgumentException() {
// Anonymous tag 1, Unsigned Integer, 1-octet value, {1 = 42U}
TlvWriter().apply {
startStructure(AnonymousTag)
// anonymous tags are not allowed within structure elements
assertFailsWith<IllegalArgumentException> { put(AnonymousTag, 42U) }
}
}

@Test
fun encodeContextTag_withinList() {
// Context tag 1, Unsigned Integer, 1-octet value, [[1 = 42U]]
Expand Down Expand Up @@ -847,7 +928,7 @@ class TlvReadWriteTest {
val value = 42U
var tag = ContextSpecificTag(1)

// Array elements SHALL be of anonumous type
// Array elements SHALL be of anonymous type
TlvWriter().apply {
startArray(AnonymousTag)
assertFailsWith<IllegalArgumentException> { put(tag, value) }
Expand Down Expand Up @@ -965,7 +1046,7 @@ class TlvReadWriteTest {

@Test
fun putSignedLongArray() {
// Anonumous Array of Signed Integers, [42, -17, -170000, 40000000000]
// Anonymous Array of Signed Integers, [42, -17, -170000, 40000000000]
val values = longArrayOf(42, -17, -170000, 40000000000)
val encoding = "16 00 2a 00 ef 02 f0 67 fd ff 03 00 90 2f 50 09 00 00 00 18".octetsToByteArray()

Expand All @@ -986,7 +1067,7 @@ class TlvReadWriteTest {

@Test
fun putUnsignedLongArray() {
// Anonumous Array of Signed Integers, [42, 170000, 40000000000]
// Anonymous Array of Signed Integers, [42, 170000, 40000000000]
val values = longArrayOf(42, 170000, 40000000000)
val encoding = "16 04 2a 06 10 98 02 00 07 00 90 2f 50 09 00 00 00 18".octetsToByteArray()

Expand Down