Skip to content

Commit

Permalink
Implement copyElement Method for the TlvWriter Class in Kotlin (#26377)
Browse files Browse the repository at this point in the history
- Added helper method peekElement() to the TlvReader class
  - Added new method and new testcases.
  - Some other minor fixes and cleanups.
  • Loading branch information
emargolis authored and pull[bot] committed Jul 27, 2023
1 parent 937930a commit 2330208
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 22 deletions.
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

0 comments on commit 2330208

Please sign in to comment.