diff --git a/examples/java-matter-controller/BUILD.gn b/examples/java-matter-controller/BUILD.gn index d26b0340ab15b0..cdb67f1b8ed41d 100644 --- a/examples/java-matter-controller/BUILD.gn +++ b/examples/java-matter-controller/BUILD.gn @@ -38,6 +38,9 @@ kotlin_binary("java-matter-controller") { output_name = "java-matter-controller" deps = [ ":java", + "${chip_root}/src/controller/java:tlv_read_write_test", + "${chip_root}/src/controller/java:tlv_reader_test", + "${chip_root}/src/controller/java:tlv_writer_test", "${chip_root}/third_party/java_deps:kotlin-stdlib", ] diff --git a/src/controller/java/BUILD.gn b/src/controller/java/BUILD.gn index 29226c87ba38d5..27d14372b9c959 100644 --- a/src/controller/java/BUILD.gn +++ b/src/controller/java/BUILD.gn @@ -158,6 +158,49 @@ kotlin_library("tlv") { kotlinc_flags = [ "-Xlint:deprecation" ] } +kotlin_library("tlv_reader_test") { + output_name = "TlvReaderTest.jar" + + deps = [ + ":tlv", + "${chip_root}/third_party/java_deps:junit-4", + "${chip_root}/third_party/java_deps:truth", + ] + + sources = [ "tests/chip/tlv/TlvReaderTest.kt" ] + + kotlinc_flags = [ "-Xlint:deprecation" ] +} + +kotlin_library("tlv_writer_test") { + output_name = "TlvWriterTest.jar" + + deps = [ + ":tlv", + "${chip_root}/third_party/java_deps:junit-4", + "${chip_root}/third_party/java_deps:truth", + ] + + sources = [ "tests/chip/tlv/TlvWriterTest.kt" ] + + kotlinc_flags = [ "-Xlint:deprecation" ] +} + +kotlin_library("tlv_read_write_test") { + output_name = "TlvReadWriteTest.jar" + + deps = [ + ":tlv", + "${chip_root}/third_party/java_deps:junit-4", + "${chip_root}/third_party/java_deps:kotlin-test", + "${chip_root}/third_party/java_deps:truth", + ] + + sources = [ "tests/chip/tlv/TlvReadWriteTest.kt" ] + + kotlinc_flags = [ "-Xlint:deprecation" ] +} + android_library("java") { output_name = "CHIPController.jar" diff --git a/src/controller/java/tests/chip/tlv/TlvReadWriteTest.kt b/src/controller/java/tests/chip/tlv/TlvReadWriteTest.kt new file mode 100644 index 00000000000000..fbf60827a278a4 --- /dev/null +++ b/src/controller/java/tests/chip/tlv/TlvReadWriteTest.kt @@ -0,0 +1,1035 @@ +/* + * + * Copyright (c) 2023 Project CHIP Authors + * Copyright (c) 2019-2023 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package chip.tlv + +import com.google.common.truth.Truth.assertThat +import com.google.protobuf.ByteString +import kotlin.test.assertFailsWith +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +// TLV Encoded structure taken from the C++ TLV unit test of the Matter SDK +private val testTlvSampleData: ByteArray = + (""" + 0xD5, 0xBB, 0xAA, 0xDD, 0xCC, 0x01, 0x00, 0xC9, 0xBB, 0xAA, 0xDD, 0xCC, 0x02, 0x00, 0x88, 0x02, + 0x00, 0x36, 0x00, 0x00, 0x2A, 0x00, 0xEF, 0x02, 0xF0, 0x67, 0xFD, 0xFF, 0x07, 0x00, 0x90, 0x2F, + 0x50, 0x09, 0x00, 0x00, 0x00, 0x15, 0x18, 0x17, 0xD4, 0xBB, 0xAA, 0xDD, 0xCC, 0x11, 0x00, 0xB4, + 0xA0, 0xBB, 0x0D, 0x00, 0x14, 0xB5, 0x00, 0x28, 0x6B, 0xEE, 0x6D, 0x70, 0x11, 0x01, 0x00, 0x0E, + 0x01, 0x53, 0x54, 0x41, 0x52, 0x54, 0x2E, 0x2E, 0x2E, 0x21, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, + 0x37, 0x38, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x40, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, + 0x37, 0x38, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x23, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, + 0x37, 0x38, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x24, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, + 0x37, 0x38, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x25, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, + 0x37, 0x38, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x5E, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, + 0x37, 0x38, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x26, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, + 0x37, 0x38, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x2A, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, + 0x37, 0x38, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, + 0x37, 0x28, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, + 0x37, 0x29, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, + 0x37, 0x2D, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, + 0x37, 0x3D, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, + 0x37, 0x5B, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, + 0x37, 0x5D, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, + 0x37, 0x3B, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, + 0x37, 0x27, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x2E, 0x2E, 0x2E, 0x45, 0x4E, 0x44, 0x18, + 0x18, 0x18, 0xCC, 0xBB, 0xAA, 0xDD, 0xCC, 0x05, 0x00, 0x0E, 0x54, 0x68, 0x69, 0x73, 0x20, 0x69, + 0x73, 0x20, 0x61, 0x20, 0x74, 0x65, 0x73, 0x74, 0x8A, 0xFF, 0xFF, 0x33, 0x33, 0x8F, 0x41, 0xAB, + 0x00, 0x00, 0x01, 0x00, 0x66, 0x66, 0x66, 0x66, 0x66, 0xE6, 0x31, 0x40, 0x18 +""") + .trimIndent() + .replace("0x", "") + .replace(", ", "") + .replace(",", "") + .replace("\n", "") + .chunked(2) + .map { it.toInt(16) and 0xFF } + .map { it.toByte() } + .toByteArray() + +private val testVendorId: UShort = 0xAABBu +private val testProductId: UShort = 0xCCDDu + +private val testLargeString: String = + """ + START... + !123456789ABCDEF@123456789ABCDEF#123456789ABCDEF$123456789ABCDEF + %123456789ABCDEF^123456789ABCDEF&123456789ABCDEF*123456789ABCDEF + 01234567(9ABCDEF01234567)9ABCDEF01234567-9ABCDEF01234567=9ABCDEF + 01234567[9ABCDEF01234567]9ABCDEF01234567;9ABCDEF01234567'9ABCDEF + ...END + """ + .trimIndent() + .replace("\n", "") + +@RunWith(JUnit4::class) +class TlvReadWriteTest { + private fun String.octetsToByteArray(): ByteArray = + replace(" ", "").chunked(2).map { it.toInt(16) and 0xFF }.map { it.toByte() }.toByteArray() + + @Test + fun testTlvSampleData_write() { + TlvWriter().apply { + startStructure(FullyQualifiedTag(6, testVendorId, testProductId, 1u)) + put(FullyQualifiedTag(6, testVendorId, testProductId, 2u), true) + put(ImplicitProfileTag(2, 2u), false) + startArray(ContextSpecificTag(0)) + put(AnonymousTag, 42) + put(AnonymousTag, -17) + put(AnonymousTag, -170000) + put(AnonymousTag, 40000000000UL) + startStructure(AnonymousTag) + endStructure() + startList(AnonymousTag) + putNull(FullyQualifiedTag(6, testVendorId, testProductId, 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(ImplicitProfileTag(2, 65535u), 17.9f) + put(ImplicitProfileTag(4, 65536u), 17.9) + endStructure() + validateTlv() + assertThat(getEncoded()).isEqualTo(testTlvSampleData) + } + } + + @Test + fun testTlvSampleData_read() { + TlvReader(testTlvSampleData).apply { + enterStructure(FullyQualifiedTag(6, testVendorId, testProductId, 1u)) + assertThat(getBool(FullyQualifiedTag(6, testVendorId, testProductId, 2u))).isEqualTo(true) + assertThat(getBool(ImplicitProfileTag(2, 2u))).isEqualTo(false) + enterArray(ContextSpecificTag(0)) + assertThat(getInt(AnonymousTag)).isEqualTo(42) + assertThat(getInt(AnonymousTag)).isEqualTo(-17) + assertThat(getInt(AnonymousTag)).isEqualTo(-170000) + assertThat(getULong(AnonymousTag)).isEqualTo(40000000000UL) + enterStructure(AnonymousTag) + exitContainer() + enterList(AnonymousTag) + getNull(FullyQualifiedTag(6, testVendorId, testProductId, 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))) + .isEqualTo("This is a test") + assertThat(getFloat(ImplicitProfileTag(2, 65535u))).isEqualTo(17.9f) + assertThat(getDouble(ImplicitProfileTag(4, 65536u))).isEqualTo(17.9) + exitContainer() + assertThat(getLengthRead()).isEqualTo(testTlvSampleData.size) + assertThat(isEndOfTlv()).isEqualTo(true) + } + } + + @Test + fun testTlvSampleData_read_useSkipElementAndExitContinerInTheMiddle() { + TlvReader(testTlvSampleData).apply { + enterStructure(FullyQualifiedTag(6, testVendorId, testProductId, 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))) + .isEqualTo("This is a test") + assertThat(getFloat(ImplicitProfileTag(2, 65535u))).isEqualTo(17.9f) + assertThat(getDouble(ImplicitProfileTag(4, 65536u))).isEqualTo(17.9) + exitContainer() + assertThat(getLengthRead()).isEqualTo(testTlvSampleData.size) + assertThat(isEndOfTlv()).isEqualTo(true) + } + } + + @Test + fun testData_IntMinMax() { + val encodedTlv = + TlvWriter() + .apply { + put(AnonymousTag, Byte.MIN_VALUE.toByte()) + put(AnonymousTag, Byte.MAX_VALUE.toByte()) + put(AnonymousTag, Short.MIN_VALUE.toShort()) + put(AnonymousTag, Short.MAX_VALUE.toShort()) + put(AnonymousTag, Int.MIN_VALUE.toInt()) + put(AnonymousTag, Int.MAX_VALUE.toInt()) + put(AnonymousTag, Long.MIN_VALUE.toLong()) + put(AnonymousTag, Long.MAX_VALUE.toLong()) + put(AnonymousTag, UByte.MAX_VALUE.toUByte()) + put(AnonymousTag, UShort.MAX_VALUE.toUShort()) + put(AnonymousTag, UInt.MAX_VALUE.toUInt()) + put(AnonymousTag, ULong.MAX_VALUE.toULong()) + } + .validateTlv() + .getEncoded() + + TlvReader(encodedTlv).apply { + assertThat(getByte(AnonymousTag)).isEqualTo(Byte.MIN_VALUE) + assertThat(getByte(AnonymousTag)).isEqualTo(Byte.MAX_VALUE) + assertThat(getShort(AnonymousTag)).isEqualTo(Short.MIN_VALUE) + assertThat(getShort(AnonymousTag)).isEqualTo(Short.MAX_VALUE) + assertThat(getInt(AnonymousTag)).isEqualTo(Int.MIN_VALUE) + assertThat(getInt(AnonymousTag)).isEqualTo(Int.MAX_VALUE) + assertThat(getLong(AnonymousTag)).isEqualTo(Long.MIN_VALUE) + assertThat(getLong(AnonymousTag)).isEqualTo(Long.MAX_VALUE) + assertThat(getUByte(AnonymousTag)).isEqualTo(UByte.MAX_VALUE) + assertThat(getUShort(AnonymousTag)).isEqualTo(UShort.MAX_VALUE) + assertThat(getUInt(AnonymousTag)).isEqualTo(UInt.MAX_VALUE) + assertThat(getULong(AnonymousTag)).isEqualTo(ULong.MAX_VALUE) + } + } + + @Test + fun encodeBoolean_false() { + // Boolean false + val value = false + val encoding = "08".octetsToByteArray() + + TlvWriter().apply { + put(AnonymousTag, value) + validateTlv() + assertThat(getEncoded()).isEqualTo(encoding) + } + + TlvReader(encoding).apply { assertThat(getBool(AnonymousTag)).isEqualTo(value) } + } + + @Test + fun encodeBoolean_true() { + // Boolean true + val value = true + val encoding = "09".octetsToByteArray() + + TlvWriter().apply { + put(AnonymousTag, value) + assertThat(getEncoded()).isEqualTo(encoding) + } + + TlvReader(encoding).apply { assertThat(getBool(AnonymousTag)).isEqualTo(value) } + } + + @Test + fun encodeSignedInt_1BytePositive() { + // Signed Integer, 1-octet + val value = 42 + val encoding = "00 2a".octetsToByteArray() + + TlvWriter().apply { + put(AnonymousTag, value) + assertThat(getEncoded()).isEqualTo(encoding) + } + + TlvReader(encoding).apply { assertThat(getInt(AnonymousTag)).isEqualTo(value) } + } + + @Test + fun encodeSignedInt_1ByteNegative() { + // Signed Integer, 1-octet + val value = -17 + val encoding = "00 ef".octetsToByteArray() + + TlvWriter().apply { + put(AnonymousTag, value) + assertThat(getEncoded()).isEqualTo(encoding) + } + + TlvReader(encoding).apply { assertThat(getInt(AnonymousTag)).isEqualTo(value) } + } + + @Test + fun encodeUnsignedInt_1Byte() { + // Unsigned Integer, 1-octet + val value = 42u + val encoding = "04 2a".octetsToByteArray() + + TlvWriter().apply { + put(AnonymousTag, value) + assertThat(getEncoded()).isEqualTo(encoding) + } + + TlvReader(encoding).apply { assertThat(getUInt(AnonymousTag)).isEqualTo(value) } + } + + @Test + fun encodeUnsignedInt_1Byte_usePutUnsigned() { + // Unsigned Integer, 1-octet + val value = 42 + val encoding = "04 2a".octetsToByteArray() + + TlvWriter().apply { + putUnsigned(AnonymousTag, value) + assertThat(getEncoded()).isEqualTo(encoding) + } + } + + @Test + fun encodeSignedInt_1Byte2octet() { + // Signed Integer, 1-byte encoded as 2-octet + val value = 42 + val encoding = "01 2a 00".octetsToByteArray() + + // Note: the current implementation follows the minimum encoding policy, which encodes this + // value as 1-octet. Testing only decoding. + + TlvReader(encoding).apply { assertThat(getInt(AnonymousTag)).isEqualTo(value) } + } + + @Test + fun encodeSignedInt_2Bytes() { + // Signed Integer, 2-octet + val value = 4242 + val encoding = "01 92 10".octetsToByteArray() + + TlvWriter().apply { + put(AnonymousTag, value) + assertThat(getEncoded()).isEqualTo(encoding) + } + + TlvReader(encoding).apply { assertThat(getInt(AnonymousTag)).isEqualTo(value) } + } + + @Test + fun encodeSignedInt_4Bytes() { + // Signed Integer, 4-octet + val value = -170000 + val encoding = "02 f0 67 fd ff".octetsToByteArray() + + TlvWriter().apply { + put(AnonymousTag, value) + assertThat(getEncoded()).isEqualTo(encoding) + } + + TlvReader(encoding).apply { assertThat(getInt(AnonymousTag)).isEqualTo(value) } + } + + @Test + fun encodeSignedInt_8Bytes() { + // Signed Integer (Long), 8-octet + val value = 40000000000 + val encoding = "03 00 90 2f 50 09 00 00 00".octetsToByteArray() + + TlvWriter().apply { + put(AnonymousTag, value) + assertThat(getEncoded()).isEqualTo(encoding) + } + + TlvReader(encoding).apply { assertThat(getLong(AnonymousTag)).isEqualTo(value) } + } + + @Test + fun encodeUnsignedInt_8Bytes_usePutUnsigned() { + // Unsigned Integer (Long), 8-octet + val value = 40000000000 + val encoding = "07 00 90 2f 50 09 00 00 00".octetsToByteArray() + + TlvWriter().apply { + putUnsigned(AnonymousTag, value) + assertThat(getEncoded()).isEqualTo(encoding) + } + + TlvReader(encoding).apply { assertThat(getULong(AnonymousTag)).isEqualTo(value.toULong()) } + } + + @Test + fun encodeSignedInt_8Bytes_useGetULong_throwsIllegalArgumentException() { + // Signed Integer (Long), 8-octet + val encoding = "03 00 90 2f 50 09 00 00 00".octetsToByteArray() + + // Throws exception because the encoded value is Long and not ULong as requested by getULong() + assertFailsWith { TlvReader(encoding).getULong(AnonymousTag) } + } + + @Test + fun encodeSignedInt_8Bytes_useGetInt_throwsTlvParsingException() { + // Signed Integer (Long), 8-octet + val encoding = "03 00 90 2f 50 09 00 00 00".octetsToByteArray() + + // Throws exception because the encoded value is out of range of Signed Int + assertFailsWith { TlvReader(encoding).getInt(AnonymousTag) } + } + + @Test + fun encodeSignedInt_8Bytes_getFullyQualifiedTag_throwsIllegalArgumentException() { + // Signed Integer (Long), 8-octet + val encoding = "03 00 90 2f 50 09 00 00 00".octetsToByteArray() + + // Throws exception because the encoded value has AnonymousTag tag + assertFailsWith { + TlvReader(encoding).getLong(FullyQualifiedTag(6, testVendorId, testProductId, 5u)) + } + } + + @Test + fun encodeUtf8String_hello() { + // UTF-8 String, 1-octet length, "Hello!" + val value = "Hello!" + val encoding = "0c 06 48 65 6c 6c 6f 21".octetsToByteArray() + + TlvWriter().apply { + put(AnonymousTag, value) + assertThat(getEncoded()).isEqualTo(encoding) + } + + TlvReader(encoding).apply { assertThat(getUtf8String(AnonymousTag)).isEqualTo(value) } + } + + @Test + fun encodeUtf8String_tschuh() { + // UTF-8 String, 1-octet length, "Tschüs" + val value = "Tschüs" + val encoding = "0c 07 54 73 63 68 c3 bc 73".octetsToByteArray() + + TlvWriter().apply { + put(AnonymousTag, value) + assertThat(getEncoded()).isEqualTo(encoding) + } + + TlvReader(encoding).apply { assertThat(getUtf8String(AnonymousTag)).isEqualTo(value) } + } + + @Test + fun encodeOctetString() { + // Octet String, 1-octet length, octets 00 01 02 03 04 + val value = ByteString.fromHex("00 01 02 03 04".replace(" ", "")) + val encoding = "10 05 00 01 02 03 04".octetsToByteArray() + + TlvWriter().apply { + put(AnonymousTag, value) + assertThat(getEncoded()).isEqualTo(encoding) + } + + TlvReader(encoding).apply { assertThat(getByteString(AnonymousTag)).isEqualTo(value) } + } + + @Test + fun encodeNull() { + // Null + val encoding = "14".octetsToByteArray() + + TlvWriter().apply { + putNull(AnonymousTag) + assertThat(getEncoded()).isEqualTo(encoding) + } + + TlvReader(encoding).apply { getNull(AnonymousTag) } + } + + @Test + fun encodeFloat_0() { + // Single precision floating point 0.0 + val value = 0.0f + val encoding = "0a 00 00 00 00".octetsToByteArray() + + TlvWriter().apply { + put(AnonymousTag, value) + assertThat(getEncoded()).isEqualTo(encoding) + } + + TlvReader(encoding).apply { assertThat(getFloat(AnonymousTag)).isEqualTo(value) } + } + + @Test + fun encodeFloat_1third() { + // Single precision floating point (1.0 / 3.0) + val value = 1 / 3.toFloat() + val encoding = "0a ab aa aa 3e".octetsToByteArray() + + TlvWriter().apply { + put(AnonymousTag, value) + assertThat(getEncoded()).isEqualTo(encoding) + } + + TlvReader(encoding).apply { assertThat(getFloat(AnonymousTag)).isEqualTo(value) } + } + + @Test + fun encodeFloat_17_9() { + // Single precision floating point 17.9 + val value = 17.9f + val encoding = "0a 33 33 8f 41".octetsToByteArray() + + TlvWriter().apply { + put(AnonymousTag, value) + assertThat(getEncoded()).isEqualTo(encoding) + } + + TlvReader(encoding).apply { assertThat(getFloat(AnonymousTag)).isEqualTo(value) } + } + + @Test + fun encodeFloat_positiveInfinity() { + // Single precision floating point infinity (∞) + val value = Float.POSITIVE_INFINITY + val encoding = "0a 00 00 80 7f".octetsToByteArray() + + TlvWriter().apply { + put(AnonymousTag, value) + assertThat(getEncoded()).isEqualTo(encoding) + } + + TlvReader(encoding).apply { assertThat(getFloat(AnonymousTag)).isEqualTo(value) } + } + + @Test + fun encodeFloat_negativeInfinity() { + // Single precision floating point negative infinity (-∞) + val value = Float.NEGATIVE_INFINITY + val encoding = "0a 00 00 80 ff".octetsToByteArray() + + TlvWriter().apply { + put(AnonymousTag, value) + assertThat(getEncoded()).isEqualTo(encoding) + } + } + + @Test + fun encodeDouble_0() { + // Double precision floating point 0.0 + val value = 0.0 + val encoding = "0b 00 00 00 00 00 00 00 00".octetsToByteArray() + + TlvWriter().apply { + put(AnonymousTag, value) + assertThat(getEncoded()).isEqualTo(encoding) + } + + TlvReader(encoding).apply { assertThat(getDouble(AnonymousTag)).isEqualTo(value) } + } + + @Test + fun encodeDouble_1third() { + // Double precision floating point (1.0 / 3.0) + val value = 1 / 3.toDouble() + val encoding = "0b 55 55 55 55 55 55 d5 3f".octetsToByteArray() + + TlvWriter().apply { + put(AnonymousTag, value) + assertThat(getEncoded()).isEqualTo(encoding) + } + + TlvReader(encoding).apply { assertThat(getDouble(AnonymousTag)).isEqualTo(value) } + } + + @Test + fun encodeDouble_17_9() { + // Double precision floating point 17.9 + val value = 17.9 + val encoding = "0b 66 66 66 66 66 e6 31 40".octetsToByteArray() + + TlvWriter().apply { + put(AnonymousTag, value) + assertThat(getEncoded()).isEqualTo(encoding) + } + + TlvReader(encoding).apply { assertThat(getDouble(AnonymousTag)).isEqualTo(value) } + } + + @Test + fun encodeDouble_positiveInfinity() { + // Double precision floating point infinity (∞) + val value = Double.POSITIVE_INFINITY + val encoding = "0b 00 00 00 00 00 00 f0 7f".octetsToByteArray() + + TlvWriter().apply { + put(AnonymousTag, value) + assertThat(getEncoded()).isEqualTo(encoding) + } + + TlvReader(encoding).apply { assertThat(getDouble(AnonymousTag)).isEqualTo(value) } + } + + @Test + fun encodeDouble_negativeInfinity() { + // Double precision floating point negative infinity (-∞) + val value = Double.NEGATIVE_INFINITY + val encoding = "0b 00 00 00 00 00 00 f0 ff".octetsToByteArray() + + TlvWriter().apply { + put(AnonymousTag, value) + assertThat(getEncoded()).isEqualTo(encoding) + } + + TlvReader(encoding).apply { assertThat(getDouble(AnonymousTag)).isEqualTo(value) } + } + + @Test + fun encodeStructure_empty() { + // Empty Structure, {} + val encoding = "15 18".octetsToByteArray() + + TlvWriter().apply { + startStructure(AnonymousTag) + endStructure() + assertThat(getEncoded()).isEqualTo(encoding) + } + + TlvReader(encoding).apply { + enterStructure(AnonymousTag) + exitContainer() + } + } + + @Test + fun encodeStructure_empty_testEndOfContainer() { + // Empty Structure, {} + val encoding = "15 18".octetsToByteArray() + + TlvReader(encoding).apply { + assertThat(isEndOfContainer()).isEqualTo(false) + enterStructure(AnonymousTag) + assertThat(isEndOfContainer()).isEqualTo(true) + exitContainer() + assertFailsWith { isEndOfContainer() } + } + } + + @Test + fun encodeStructure_notClosed_throwsTlvEncodingException() { + // Open Structure, { + TlvWriter().apply { + startStructure(AnonymousTag) + assertFailsWith { validateTlv() } + } + } + + @Test + fun encodeArray_empty() { + // Empty Array, [] + val encoding = "16 18".octetsToByteArray() + + TlvWriter().apply { + startArray(AnonymousTag) + endArray() + assertThat(getEncoded()).isEqualTo(encoding) + } + + TlvReader(encoding).apply { + enterArray(AnonymousTag) + exitContainer() + } + } + + @Test + fun encodeArray_empty_closeUnopennedArray_throwsIllegalArgumentException() { + // Empty Array, []] + TlvWriter().apply { + startArray(AnonymousTag) + endArray() + // trying to closed container that is not openned + assertFailsWith { endArray() } + } + } + + @Test + fun encodeList_empty() { + // Empty List, [] + val encoding = "17 18".octetsToByteArray() + + TlvWriter().apply { + startList(AnonymousTag) + endList() + assertThat(getEncoded()).isEqualTo(encoding) + } + + TlvReader(encoding).apply { + enterList(AnonymousTag) + exitContainer() + } + } + + @Test + fun encodeStructure_intsWithContextTags() { + // Structure, two context specific tags, Signed Integer, 1 octet values, {0 = 42, 1 = -17} + val value0 = 42 + val value1 = -17 + val encoding = "15 20 00 2a 20 01 ef 18".octetsToByteArray() + + TlvWriter().apply { + startStructure(AnonymousTag) + put(ContextSpecificTag(0), value0) + put(ContextSpecificTag(1), value1) + endStructure() + assertThat(getEncoded()).isEqualTo(encoding) + } + + TlvReader(encoding).apply { + enterStructure(AnonymousTag) + assertThat(getByte(ContextSpecificTag(0))).isEqualTo(value0) + assertThat(getByte(ContextSpecificTag(1))).isEqualTo(value1) + exitContainer() + } + } + + @Test + fun encodeArray_ints() { + // Array, Signed Integer, 1-octet values, [0, 1, 2, 3, 4] + val encoding = "16 00 00 00 01 00 02 00 03 00 04 18".octetsToByteArray() + + TlvWriter().apply { + startArray(AnonymousTag) + put(AnonymousTag, 0) + put(AnonymousTag, 1) + put(AnonymousTag, 2) + put(AnonymousTag, 3) + put(AnonymousTag, 4) + endArray() + assertThat(getEncoded()).isEqualTo(encoding) + } + + TlvReader(encoding).apply { + enterArray(AnonymousTag) + assertThat(getInt(AnonymousTag)).isEqualTo(0) + assertThat(getInt(AnonymousTag)).isEqualTo(1) + assertThat(getInt(AnonymousTag)).isEqualTo(2) + assertThat(getInt(AnonymousTag)).isEqualTo(3) + assertThat(getInt(AnonymousTag)).isEqualTo(4) + exitContainer() + } + } + + @Test + fun encodeList_mixedInts() { + // List, mix of anonymous and context tags, Signed Integer, 1 octet values, + // [[1, 0 = 42, 2, 3, 0 = -17]] + val encoding = "17 00 01 20 00 2a 00 02 00 03 20 00 ef 18".octetsToByteArray() + + TlvWriter().apply { + startList(AnonymousTag) + put(AnonymousTag, 1) + put(ContextSpecificTag(0), 42) + put(AnonymousTag, 2) + put(AnonymousTag, 3) + put(ContextSpecificTag(0), -17) + endList() + assertThat(getEncoded()).isEqualTo(encoding) + } + + TlvReader(encoding).apply { + enterList(AnonymousTag) + assertThat(getInt(AnonymousTag)).isEqualTo(1) + assertThat(getInt(ContextSpecificTag(0))).isEqualTo(42) + assertThat(getInt(AnonymousTag)).isEqualTo(2) + assertThat(getInt(AnonymousTag)).isEqualTo(3) + assertThat(getInt(ContextSpecificTag(0))).isEqualTo(-17) + exitContainer() + } + } + + @Test + fun encodeArray_mixedValues() { + // Array, mix of element types, [42, -170000, {}, 17.9, "Hello!"] + val encoding = + "16 00 2a 02 f0 67 fd ff 15 18 0a 33 33 8f 41 0c 06 48 65 6c 6c 6f 21 18".octetsToByteArray() + + TlvWriter().apply { + startArray(AnonymousTag) + put(AnonymousTag, 42) + put(AnonymousTag, -170000) + startStructure(AnonymousTag) + endStructure() + put(AnonymousTag, 17.9f) + put(AnonymousTag, "Hello!") + endArray() + assertThat(getEncoded()).isEqualTo(encoding) + } + + TlvReader(encoding).apply { + enterArray(AnonymousTag) + assertThat(getInt(AnonymousTag)).isEqualTo(42) + assertThat(getInt(AnonymousTag)).isEqualTo(-170000) + enterStructure(AnonymousTag) + exitContainer() + assertThat(getFloat(AnonymousTag)).isEqualTo(17.9f) + assertThat(getUtf8String(AnonymousTag)).isEqualTo("Hello!") + exitContainer() + } + } + + @Test + fun encodeAanonymousTag() { + // Anonymous tag, Unsigned Integer, 1-octet value, 42U + val value = 42U + var tag = AnonymousTag + var encoding = "04 2a".octetsToByteArray() + + TlvWriter().apply { + put(tag, value) + assertThat(getEncoded()).isEqualTo(encoding) + } + + TlvReader(encoding).apply { assertThat(getUInt(tag)).isEqualTo(value) } + } + + @Test + fun encodeContextTag_withinStructure() { + // Context tag 1, Unsigned Integer, 1-octet value, {1 = 42U} + val value = 42U + var tag = ContextSpecificTag(1) + var encoding = "15 24 01 2a 18".octetsToByteArray() + + TlvWriter().apply { + startStructure(AnonymousTag) + put(tag, value) + endStructure() + assertThat(getEncoded()).isEqualTo(encoding) + } + + TlvReader(encoding).apply { + enterStructure(AnonymousTag) + assertThat(getUInt(tag)).isEqualTo(value) + exitContainer() + } + } + + @Test + fun encodeContextTag_invalidContextTag_throwsIllegalArgumentException() { + // Context tag 1, Unsigned Integer, 1-octet value, {1 = 42U} + val value1 = 42U + val value2 = 17000 + var tag1 = ContextSpecificTag(UByte.MAX_VALUE.toInt()) + var tag2 = ContextSpecificTag(UByte.MAX_VALUE.toInt() + 1) + + TlvWriter().apply { + startStructure(AnonymousTag) + put(tag1, value1) + // tag2 exeeds valid context specific tag value + assertFailsWith { put(tag2, value2) } + } + } + + @Test + fun encodeContextTag_withinList() { + // Context tag 1, Unsigned Integer, 1-octet value, [[1 = 42U]] + val value = 42U + var tag = ContextSpecificTag(1) + var encoding = "17 24 01 2a 18".octetsToByteArray() + + TlvWriter().apply { + startList(AnonymousTag) + put(tag, value) + endList() + assertThat(getEncoded()).isEqualTo(encoding) + } + + TlvReader(encoding).apply { + enterList(AnonymousTag) + assertThat(getUInt(tag)).isEqualTo(value) + exitContainer() + } + } + + @Test + fun encodeContextTag_withinArray_throwsIllegalArgumentException() { + // Context tag 1, Unsigned Integer, 1-octet value, [1 = 42U] + val value = 42U + var tag = ContextSpecificTag(1) + + // Array elements SHALL be of anonumous type + TlvWriter().apply { + startArray(AnonymousTag) + assertFailsWith { put(tag, value) } + } + } + + @Test + fun encodeContextTag_notInContainer_throwsIllegalArgumentException() { + // Context tag 1, Unsigned Integer, 1-octet value, 1 = 42U + val value = 42U + var tag = ContextSpecificTag(1) + + // Context tag can only be used within a Structure or a List + assertFailsWith { TlvWriter().put(tag, value) } + } + + @Test + fun encodeCommonProfileTag2() { + // Common profile tag 1, Unsigned Integer, 1-octet value, Matter::1 = 42U + val value = 42U + var tag = CommonProfileTag(2, 1u) + var encoding = "44 01 00 2a".octetsToByteArray() + + TlvWriter().apply { + put(tag, value) + assertThat(getEncoded()).isEqualTo(encoding) + } + + TlvReader(encoding).apply { assertThat(getUInt(tag)).isEqualTo(value) } + } + + @Test + fun encodeCommonProfileTag4() { + // Common profile tag 100000, Unsigned Integer, 1-octet value, Matter::100000 = 42U + val value = 42U + var tag = CommonProfileTag(4, 100000u) + var encoding = "64 a0 86 01 00 2a".octetsToByteArray() + + TlvWriter().apply { + put(tag, value) + assertThat(getEncoded()).isEqualTo(encoding) + } + + TlvReader(encoding).apply { assertThat(getUInt(tag)).isEqualTo(value) } + } + + @Test + fun encodeFullyQualifiedTag6() { + // Fully qualified tag, Vendor ID 0xFFF1/65521, profile number 0xDEED/57069, 2-octet tag 1, + // Unsigned Integer, 1-octet value 42, 65521::57069:1 = 42U + val value = 42U + var tag = FullyQualifiedTag(6, 0xFFF1u, 0xDEEDu, 1u) + var encoding = "c4 f1 ff ed de 01 00 2a".octetsToByteArray() + + TlvWriter().apply { + put(tag, value) + assertThat(getEncoded()).isEqualTo(encoding) + } + + TlvReader(encoding).apply { assertThat(getUInt(tag)).isEqualTo(value) } + } + + @Test + fun encodeFullyQualifiedTag8() { + // Fully qualified tag, Vendor ID 0xFFF1/65521, profile number 0xDEED/57069, 4-octet tag + // 0xAA55FEED/2857762541, Unsigned Integer, 1-octet value 42, 65521::57069:2857762541 = 42U + val value = 42U + var tag = FullyQualifiedTag(8, 0xFFF1u, 0xDEEDu, 0xAA55FEEDu) + var encoding = "e4 f1 ff ed de ed fe 55 aa 2a".octetsToByteArray() + + TlvWriter().apply { + put(tag, value) + assertThat(getEncoded()).isEqualTo(encoding) + } + + TlvReader(encoding).apply { assertThat(getUInt(tag)).isEqualTo(value) } + } + + @Test + fun encodeFullyQualifiedTags_withStructure() { + // Structure with the fully qualified tag, Vendor ID 0xFFF1/65521, profile number 0xDEED/57069, + // 2-octet tag 1. The structure contains a single element labeled using a fully qualified tag + // under the same profile, with 2-octet tag 0xAA55/43605.65521::57069:1 = {65521::57069:43605 = + // 42U} + val value = 42U + val structTag = FullyQualifiedTag(6, 0xFFF1u, 0xDEEDu, 1u) + var valueTag = FullyQualifiedTag(6, 0xFFF1u, 57069u, 43605u) + var encoding = "d5 f1 ff ed de 01 00 c4 f1 ff ed de 55 aa 2a 18".octetsToByteArray() + + TlvWriter().apply { + startStructure(structTag) + put(valueTag, value) + endStructure() + assertThat(getEncoded()).isEqualTo(encoding) + } + + TlvReader(encoding).apply { + enterStructure(structTag) + assertThat(getUInt(valueTag)).isEqualTo(value) + exitContainer() + } + } + + @Test + fun encodeArray_empty_useputSignedLongArray() { + // Empty Array, [] + val values = longArrayOf() + val encoding = "16 18".octetsToByteArray() + + TlvWriter().apply { + putSignedLongArray(AnonymousTag, values) + assertThat(getEncoded()).isEqualTo(encoding) + } + } + + @Test + fun putSignedLongArray() { + // Anonumous 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() + + TlvWriter().apply { + putSignedLongArray(AnonymousTag, values) + assertThat(getEncoded()).isEqualTo(encoding) + } + + TlvReader(encoding).apply { + enterArray(AnonymousTag) + assertThat(getLong(AnonymousTag)).isEqualTo(values[0]) + assertThat(getLong(AnonymousTag)).isEqualTo(values[1]) + assertThat(getLong(AnonymousTag)).isEqualTo(values[2]) + assertThat(getLong(AnonymousTag)).isEqualTo(values[3]) + exitContainer() + } + } + + @Test + fun putUnsignedLongArray() { + // Anonumous 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() + + TlvWriter().apply { + putUnsignedLongArray(AnonymousTag, values) + assertThat(getEncoded()).isEqualTo(encoding) + } + + TlvReader(encoding).apply { + enterArray(AnonymousTag) + assertThat(getULong(AnonymousTag)).isEqualTo(values[0].toULong()) + assertThat(getULong(AnonymousTag)).isEqualTo(values[1].toULong()) + assertThat(isEndOfContainer()).isFalse() + assertThat(getULong(AnonymousTag)).isEqualTo(values[2].toULong()) + assertThat(isEndOfContainer()).isTrue() + assertThat(isEndOfTlv()).isFalse() + exitContainer() + assertThat(isEndOfTlv()).isTrue() + } + } + + @Test + fun putByteStringArray() { + // Anonumous Array of Signed Integers, [{00 01 02 03 04}, {FF}, {4A EF 88}] + val values = + listOf( + ByteString.fromHex("0001020304"), + ByteString.fromHex("FF"), + ByteString.fromHex("4AEF88") + ) + val encoding = "16 10 05 00 01 02 03 04 10 01 FF 10 03 4A EF 88 18".octetsToByteArray() + + TlvWriter().apply { + putByteStringArray(AnonymousTag, values) + assertThat(getEncoded()).isEqualTo(encoding) + } + + TlvReader(encoding).apply { + enterArray(AnonymousTag) + assertThat(getByteString(AnonymousTag)).isEqualTo(values[0]) + assertThat(getByteString(AnonymousTag)).isEqualTo(values[1]) + assertThat(getByteString(AnonymousTag)).isEqualTo(values[2]) + exitContainer() + } + } +} diff --git a/src/controller/java/tests/chip/tlv/TlvReaderTest.kt b/src/controller/java/tests/chip/tlv/TlvReaderTest.kt new file mode 100644 index 00000000000000..f7a6b4dbbe0b25 --- /dev/null +++ b/src/controller/java/tests/chip/tlv/TlvReaderTest.kt @@ -0,0 +1,271 @@ +/* + * + * Copyright (c) 2023 Project CHIP Authors + * Copyright (c) 2019-2023 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package chip.tlv + +import com.google.common.truth.Truth.assertThat +import com.google.protobuf.ByteString +import java.math.BigInteger +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.Assert.assertThrows + +// Extracted from a Newman device during a pairing flow. Represents a fabric +// ID and keys for the fabric 7885a14c693bf1cb. +private val fabricConfig = + """ + D50000050001002701CBF13B694CA18578360 + 21525010110240201300310149BF1430B26F5 + E4BBF380D3DB855BA1300414E0E8BAA1CAC6E + 8E7216D720BD13C61C5E0E7B9012405002406 + 00181818 + """ + .trimIndent() + .replace("\n", "") + .chunked(2) + .map { it.toInt(16) and 0xFF } + .map { it.toByte() } + .toByteArray() + +@RunWith(JUnit4::class) +class TlvReaderTest { + @Test + fun parsingFabricConfig_extractsFabricId() { + val reader = TlvReader(fabricConfig) + assertThat(reader.nextElement().value).isInstanceOf(StructureValue::class.java) + + reader.nextElement().apply { + assertThat(tag).isInstanceOf(ContextSpecificTag::class.java) + assertThat((tag as ContextSpecificTag).tagNumber).isEqualTo(1) + assertThat(value).isInstanceOf(UnsignedIntValue::class.java) + assertThat((value as UnsignedIntValue).value) + .isEqualTo(BigInteger("7885a14c693bf1cb", 16).toLong()) + } + + reader.nextElement().apply { + assertThat(tag).isInstanceOf(ContextSpecificTag::class.java) + assertThat((tag as ContextSpecificTag).tagNumber).isEqualTo(2) + assertThat(value).isInstanceOf(ArrayValue::class.java) + } + + reader.nextElement().apply { + assertThat(tag).isInstanceOf(AnonymousTag::class.java) + assertThat(value).isInstanceOf(StructureValue::class.java) + } + + reader.nextElement().apply { + assertThat(tag).isInstanceOf(ContextSpecificTag::class.java) + assertThat((tag as ContextSpecificTag).tagNumber).isEqualTo(1) + assertThat(value).isInstanceOf(UnsignedIntValue::class.java) + assertThat((value as UnsignedIntValue).value).isEqualTo(0x1001) + } + + reader.nextElement().apply { + assertThat(tag).isInstanceOf(ContextSpecificTag::class.java) + assertThat((tag as ContextSpecificTag).tagNumber).isEqualTo(2) + assertThat(value).isInstanceOf(UnsignedIntValue::class.java) + assertThat((value as UnsignedIntValue).value).isEqualTo(0x01) + } + + reader.nextElement().apply { + assertThat(tag).isInstanceOf(ContextSpecificTag::class.java) + assertThat((tag as ContextSpecificTag).tagNumber).isEqualTo(3) + assertThat(value).isInstanceOf(ByteStringValue::class.java) + assertThat((value as ByteStringValue).value) + .isEqualTo(ByteString.fromHex("149BF1430B26F5E4BBF380D3DB855BA1")) + } + + reader.nextElement().apply { + assertThat(tag).isInstanceOf(ContextSpecificTag::class.java) + assertThat((tag as ContextSpecificTag).tagNumber).isEqualTo(4) + assertThat(value).isInstanceOf(ByteStringValue::class.java) + assertThat((value as ByteStringValue).value) + .isEqualTo(ByteString.fromHex("E0E8BAA1CAC6E8E7216D720BD13C61C5E0E7B901")) + } + + reader.nextElement().apply { + assertThat(tag).isInstanceOf(ContextSpecificTag::class.java) + assertThat((tag as ContextSpecificTag).tagNumber).isEqualTo(5) + assertThat(value).isInstanceOf(UnsignedIntValue::class.java) + assertThat((value as UnsignedIntValue).value).isEqualTo(0) + } + + reader.nextElement().apply { + assertThat(tag).isInstanceOf(ContextSpecificTag::class.java) + assertThat((tag as ContextSpecificTag).tagNumber).isEqualTo(6) + assertThat(value).isInstanceOf(UnsignedIntValue::class.java) + assertThat((value as UnsignedIntValue).value).isEqualTo(0) + } + + reader.nextElement().apply { assertThat(value).isInstanceOf(EndOfContainerValue::class.java) } + + reader.nextElement().apply { assertThat(value).isInstanceOf(EndOfContainerValue::class.java) } + + reader.nextElement().apply { assertThat(value).isInstanceOf(EndOfContainerValue::class.java) } + } + + @Test + fun parsingContextSpecificTagWithUnsignedIntMsbSet_extractsUnsignedInt() { + // ContextSpecificTag with UnsignedIntValue set to 0xFFF2(65522) + val config = "2500F2FF".chunked(2).map { it.toInt(16).toByte() }.toByteArray() + val reader = TlvReader(config) + + reader.nextElement().apply { + assertThat(tag).isInstanceOf(ContextSpecificTag::class.java) + assertThat((tag as ContextSpecificTag).tagNumber).isEqualTo(0) + assertThat(value).isInstanceOf(UnsignedIntValue::class.java) + assertThat((value as UnsignedIntValue).value).isEqualTo(65522) + } + } + + @Test + fun iterator_overAllElements_returnsExpectedNumberOfElements() { + // Fabric configuration contains a total of 13 elements, + // including container and end of container element types. + assertThat(TlvReader(fabricConfig).count()).isEqualTo(13) + } + + @Test + fun onReset_readerStartsAtBeginning() { + val reader = TlvReader(fabricConfig) + val initialElements = reader.take(3) + + reader.reset() + + assertThat(reader.take(3)).isEqualTo(initialElements) + } + + @Test + fun onInvalidData_throwsTlvParsingException() { + val reader = TlvReader(byteArrayOf(0x5)) + assertThrows(TlvParsingException::class.java) { reader.nextElement() } + } + + @Test + fun parseByteArray_extractsUtf8String() { + // Common-profile 2-bytes Tag with UTF-8 String 1-byte type + val control = 0b01001100.toByte() + val length = 0b101.toByte() + val v = "value".toByteArray(Charsets.UTF_8) + + val reader = TlvReader(byteArrayOf(control, 0x0, 0x0, length, *v)) + reader.nextElement().apply { + assertThat(tag).isInstanceOf(CommonProfileTag::class.java) + assertThat((tag as CommonProfileTag).size).isEqualTo(2) + assertThat(value).isInstanceOf(Utf8StringValue::class.java) + assertThat((value as Utf8StringValue).value).isEqualTo("value") + } + } + + @Test + fun parseByteArray_extractsByteString() { + // Common-profile 2-bytes Tag with ByteString 1-byte type + val control = 0b01010000.toByte() + val length = 0b10000.toByte() + val v = ByteString.copyFromUtf8("byte_string_utf8") + assertThat(v.toByteArray().size).isEqualTo(16) + + val reader = TlvReader(byteArrayOf(control, 0x0, 0x0, length, *v.toByteArray())) + reader.nextElement().apply { + assertThat(tag).isInstanceOf(CommonProfileTag::class.java) + assertThat((tag as CommonProfileTag).size).isEqualTo(2) + assertThat(value).isInstanceOf(ByteStringValue::class.java) + assertThat((value as ByteStringValue).value) + .isEqualTo(ByteString.copyFromUtf8("byte_string_utf8")) + } + } + + @Test + fun parseByteArray_extractsArray() { + // Common-profile 2-bytes Tag with UTF-8 String 1-byte type + val stringControl = 0b01001100.toByte() + val stringLength = 0b101.toByte() + val stringValue = "value".toByteArray(Charsets.UTF_8) + val stringBytes = byteArrayOf(stringControl, 0x0, 0x0, stringLength, *stringValue) + + // Anonymous Tag with UTF-8 String 2-byte type (length [0x110, 0x0] in little endian). + val stringControl2 = 0b00001101.toByte() + val stringLength2 = 0b110.toByte() + val stringValue2 = "value2".toByteArray(Charsets.UTF_8) + val stringBytes2 = byteArrayOf(stringControl2, stringLength2, 0x0, *stringValue2) + + // Anonymous Tag with UTF-8 String 1-byte type + val stringControl3 = 0b00001100.toByte() + val stringLength3 = 0b110.toByte() + val stringValue3 = "value3".toByteArray(Charsets.UTF_8) + val stringBytes3 = byteArrayOf(stringControl3, stringLength3, *stringValue3) + + // Put String2 and String3 into the array. + val arrayControl = 0b01010110.toByte() + val arrayValue = byteArrayOf(*stringBytes2, *stringBytes3) + val arrayBytes = byteArrayOf(arrayControl, 0x0, 0x0, *arrayValue) + + // Common-profile 2-bytes Tag with UTF-8 String 1-byte type + val stringControl4 = 0b01001100.toByte() + val stringLength4 = 0b110.toByte() + val stringValue4 = "value4".toByteArray(Charsets.UTF_8) + val stringBytes4 = byteArrayOf(stringControl4, 0x0, 0x0, stringLength4, *stringValue4) + + val reader = + TlvReader(byteArrayOf(*stringBytes, *arrayBytes, 0b00011000.toByte(), *stringBytes4)) + reader.nextElement().apply { + assertThat(tag).isInstanceOf(CommonProfileTag::class.java) + assertThat((tag as CommonProfileTag).size).isEqualTo(2) + assertThat(value).isInstanceOf(Utf8StringValue::class.java) + assertThat((value as Utf8StringValue).value).isEqualTo("value") + } + + reader.nextElement().apply { + assertThat(tag).isInstanceOf(CommonProfileTag::class.java) + assertThat((tag as CommonProfileTag).size).isEqualTo(2) + assertThat(value).isInstanceOf(ArrayValue::class.java) + } + + reader.nextElement().apply { + assertThat(tag).isInstanceOf(AnonymousTag::class.java) + assertThat(value).isInstanceOf(Utf8StringValue::class.java) + assertThat((value as Utf8StringValue).value).isEqualTo("value2") + } + + reader.nextElement().apply { + assertThat(tag).isInstanceOf(AnonymousTag::class.java) + assertThat(value).isInstanceOf(Utf8StringValue::class.java) + assertThat((value as Utf8StringValue).value).isEqualTo("value3") + } + + assertThat(reader.nextElement().value).isInstanceOf(EndOfContainerValue::class.java) + + reader.nextElement().apply { + assertThat(tag).isInstanceOf(CommonProfileTag::class.java) + assertThat((tag as CommonProfileTag).size).isEqualTo(2) + assertThat(value).isInstanceOf(Utf8StringValue::class.java) + assertThat((value as Utf8StringValue).value).isEqualTo("value4") + } + } + + @Test + fun contextSpecificTag_parsesUnsignedByte() { + // Context-Specific tag with tag number 254 (0xFE) and value 2 + val bytes = byteArrayOf(0x24, 0xFE.toByte(), 0x02) + val tlvReader = TlvReader(bytes) + val tag = tlvReader.nextElement().tag + assertThat(tag).isInstanceOf(ContextSpecificTag::class.java) + assertThat((tag as ContextSpecificTag).tagNumber).isEqualTo(0xFE) + } +} \ No newline at end of file diff --git a/src/controller/java/tests/chip/tlv/TlvWriterTest.kt b/src/controller/java/tests/chip/tlv/TlvWriterTest.kt new file mode 100644 index 00000000000000..a45c0bfd093837 --- /dev/null +++ b/src/controller/java/tests/chip/tlv/TlvWriterTest.kt @@ -0,0 +1,69 @@ +/* + * + * Copyright (c) 2023 Project CHIP Authors + * Copyright (c) 2019-2023 Google LLC. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package chip.tlv + +import com.google.common.truth.Truth.assertThat +import com.google.protobuf.ByteString +import java.math.BigInteger +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +// Extracted from a Newman device during a pairing flow. Represents a fabric +// ID and keys for the fabric 7885a14c693bf1cb. +private val fabricConfig = + """ + D50000050001002701CBF13B694CA18578360 + 21525010110240201300310149BF1430B26F5 + E4BBF380D3DB855BA1300414E0E8BAA1CAC6E + 8E7216D720BD13C61C5E0E7B9012405002406 + 00181818 + """ + .trimIndent() + .replace("\n", "") + .chunked(2) + .map { it.toInt(16) and 0xFF } + .map { it.toByte() } + .toByteArray() + +@RunWith(JUnit4::class) +class TlvWriterTest { + @Test + fun encodingFabricConfig_allElements() { + val encodedTlv = + TlvWriter(fabricConfig.size) + .startStructure(FullyQualifiedTag(6, 0u, 5u, 1u)) + .put(ContextSpecificTag(1), BigInteger("7885a14c693bf1cb", 16).toLong().toULong()) + .startArray(ContextSpecificTag(2)) + .startStructure(AnonymousTag) + .put(ContextSpecificTag(1), 0x1001u) + .put(ContextSpecificTag(2), 0x01u) + .put(ContextSpecificTag(3), ByteString.fromHex("149BF1430B26F5E4BBF380D3DB855BA1")) + .put(ContextSpecificTag(4), ByteString.fromHex("E0E8BAA1CAC6E8E7216D720BD13C61C5E0E7B901")) + .put(ContextSpecificTag(5), 0u) + .put(ContextSpecificTag(6), 0u) + .endStructure() + .endArray() + .endStructure() + .validateTlv() + .getEncoded() + + assertThat(encodedTlv).isEqualTo(fabricConfig) + } +} \ No newline at end of file diff --git a/third_party/java_deps/BUILD.gn b/third_party/java_deps/BUILD.gn index 0a821fad9bba90..a5e7c0629d94be 100644 --- a/third_party/java_deps/BUILD.gn +++ b/third_party/java_deps/BUILD.gn @@ -29,6 +29,18 @@ java_prebuilt("kotlin-stdlib") { jar_path = "artifacts/kotlin-stdlib-1.8.10.jar" } +java_prebuilt("kotlin-test") { + jar_path = "artifacts/kotlin-test-1.8.10.jar" +} + java_prebuilt("protobuf-java") { jar_path = "artifacts/protobuf-java-3.22.0.jar" } + +java_prebuilt("truth") { + jar_path = "artifacts/truth-1.1.3.jar" +} + +java_prebuilt("junit-4") { + jar_path = "artifacts/junit-4.13.2.jar" +} diff --git a/third_party/java_deps/set_up_java_deps.sh b/third_party/java_deps/set_up_java_deps.sh index de2d2c848f2b37..911a53a638708d 100755 --- a/third_party/java_deps/set_up_java_deps.sh +++ b/third_party/java_deps/set_up_java_deps.sh @@ -20,4 +20,7 @@ mkdir -p third_party/java_deps/artifacts curl --fail --location --silent --show-error https://repo1.maven.org/maven2/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.jar -o third_party/java_deps/artifacts/jsr305-3.0.2.jar curl --fail --location --silent --show-error https://repo1.maven.org/maven2/org/json/json/20220924/json-20220924.jar -o third_party/java_deps/artifacts/json-20220924.jar curl --fail --location --silent --show-error https://repo1.maven.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/1.8.10/kotlin-stdlib-1.8.10.jar -o third_party/java_deps/artifacts/kotlin-stdlib-1.8.10.jar +curl --fail --location --silent --show-error https://repo1.maven.org/maven2/org/jetbrains/kotlin/kotlin-test/1.8.10/kotlin-test-1.8.10.jar -o third_party/java_deps/artifacts/kotlin-test-1.8.10.jar curl --fail --location --silent --show-error https://repo1.maven.org/maven2/com/google/protobuf/protobuf-java/3.22.0/protobuf-java-3.22.0.jar -o third_party/java_deps/artifacts/protobuf-java-3.22.0.jar +curl --fail --location --silent --show-error https://repo1.maven.org/maven2/com/google/truth/truth/1.1.3/truth-1.1.3.jar -o third_party/java_deps/artifacts/truth-1.1.3.jar +curl --fail --location --silent --show-error https://repo1.maven.org/maven2/junit/junit/4.13.2/junit-4.13.2.jar -o third_party/java_deps/artifacts/junit-4.13.2.jar