diff --git a/.github/workflows/java-tests.yaml b/.github/workflows/java-tests.yaml index f56b1c42d77c5c..8c2e2aa1189a06 100644 --- a/.github/workflows/java-tests.yaml +++ b/.github/workflows/java-tests.yaml @@ -79,7 +79,8 @@ jobs: chip.tlv.TlvReadWriteTest \ chip.tlv.TlvReaderTest \ chip.jsontlv.JsonToTlvToJsonTest \ - chip.onboardingpayload.ManualCodeTest + chip.onboardingpayload.ManualCodeTest \ + chip.onboardingpayload.QRCodeTest - name: Build Java Matter Controller and all clusters app run: | scripts/run_in_build_env.sh './scripts/build_python.sh --install_virtual_env out/venv' diff --git a/kotlin-detect-config.yaml b/kotlin-detect-config.yaml index 27d90e0795e282..f6d723a9b638ac 100644 --- a/kotlin-detect-config.yaml +++ b/kotlin-detect-config.yaml @@ -40,6 +40,7 @@ style: examples/android/CHIPTest/app/src/androidTest/java/com/tcl/chip/chiptest/ExampleInstrumentedTest.kt" - "**/src/controller/java/tests/chip/jsontlv/JsonToTlvToJsonTest.kt" - "**/src/controller/java/tests/chip/onboardingpayload/ManualCodeTest.kt" + - "**/src/controller/java/tests/chip/onboardingpayload/QRCodeTest.kt" - "**/src/controller/java/tests/chip/tlv/TlvReaderTest.kt" - "**/src/controller/java/tests/chip/tlv/TlvReadWriteTest.kt" - "**/src/controller/java/tests/chip/tlv/TlvWriterTest.kt" @@ -128,6 +129,7 @@ style: - "**/src/controller/java/src/chip/onboardingpayload/OnboardingPayload.kt" - "**/src/controller/java/src/chip/onboardingpayload/QRCodeBasicOnboardingPayloadGenerator.kt" - "**/src/controller/java/tests/chip/onboardingpayload/ManualCodeTest.kt" + - "**/src/controller/java/tests/chip/onboardingpayload/QRCodeTest.kt" UnusedParameter: excludes: - "**/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/bluetooth/BluetoothManager.kt" @@ -201,6 +203,7 @@ exceptions: - "**/examples/java-matter-controller/java/src/com/matter/controller/commands/common/Argument.kt" - "**/examples/java-matter-controller/java/src/com/matter/controller/Main.kt" - "**/src/controller/java/tests/chip/onboardingpayload/ManualCodeTest.kt" + - "**/src/controller/java/tests/chip/onboardingpayload/QRCodeTest.kt" SwallowedException: excludes: - "**/examples/android/CHIPTool/app/src/main/java/com/google/chip/chiptool/clusterclient/SensorClientFragment.kt" @@ -221,6 +224,7 @@ naming: - "**/src/controller/java/tests/chip/tlv/TlvReaderTest.kt" - "**/src/controller/java/tests/chip/jsontlv/JsonToTlvToJsonTest.kt" - "**/src/controller/java/tests/chip/onboardingpayload/ManualCodeTest.kt" + - "**/src/controller/java/tests/chip/onboardingpayload/QRCodeTest.kt" - "**/examples/android/CHIPTest/app/src/test/java/com/tcl/chip/chiptest/ExampleUnitTest.kt" ConstructorParameterNaming: excludes: @@ -264,6 +268,7 @@ complexity: - "**/src/controller/java/src/chip/tlv/TlvWriter.kt" - "**/src/controller/java/tests/chip/jsontlv/JsonToTlvToJsonTest.kt" - "**/src/controller/java/tests/chip/onboardingpayload/ManualCodeTest.kt" + - "**/src/controller/java/tests/chip/onboardingpayload/QRCodeTest.kt" - "**/src/controller/java/tests/chip/tlv/TlvReadWriteTest.kt" NestedBlockDepth: excludes: @@ -282,6 +287,7 @@ complexity: - "**/src/controller/java/src/chip/onboardingpayload/ManualOnboardingPayloadParser.kt" - "**/src/controller/java/tests/chip/jsontlv/JsonToTlvToJsonTest.kt" - "**/src/controller/java/tests/chip/onboardingpayload/ManualCodeTest.kt" + - "**/src/controller/java/tests/chip/onboardingpayload/QRCodeTest.kt" - "**/src/controller/java/tests/chip/tlv/TlvReaderTest.kt" LargeClass: excludes: diff --git a/src/controller/java/BUILD.gn b/src/controller/java/BUILD.gn index 60f3f954343c9d..22e3cc4fa79eb7 100644 --- a/src/controller/java/BUILD.gn +++ b/src/controller/java/BUILD.gn @@ -275,10 +275,25 @@ kotlin_library("onboardingpayload_manual_code_test") { kotlinc_flags = [ "-Xlint:deprecation" ] } +kotlin_library("onboardingpayload_qr_code_test") { + output_name = "OnboardingPayloadQRCodeTest.jar" + + deps = [ + ":onboarding_payload", + "${chip_root}/third_party/java_deps:junit-4", + "${chip_root}/third_party/java_deps:truth", + ] + + sources = [ "tests/chip/onboardingpayload/QRCodeTest.kt" ] + + kotlinc_flags = [ "-Xlint:deprecation" ] +} + group("unit_tests") { deps = [ ":json_to_tlv_to_json_test", ":onboardingpayload_manual_code_test", + ":onboardingpayload_qr_code_test", ":tlv_read_write_test", ":tlv_reader_test", ":tlv_writer_test", diff --git a/src/controller/java/src/chip/onboardingpayload/QRCodeOnboardingPayloadParser.kt b/src/controller/java/src/chip/onboardingpayload/QRCodeOnboardingPayloadParser.kt index 72f85ae72476c4..18d91e8b2b4f45 100644 --- a/src/controller/java/src/chip/onboardingpayload/QRCodeOnboardingPayloadParser.kt +++ b/src/controller/java/src/chip/onboardingpayload/QRCodeOnboardingPayloadParser.kt @@ -24,39 +24,6 @@ import java.util.concurrent.atomic.AtomicInteger * to a OnboardingPayload object */ class QRCodeOnboardingPayloadParser(private val mBase38Representation: String) { - private fun extractPayload(inString: String): String { - var chipSegment = "" - val delimiter = '%' - val startIndices = mutableListOf() - startIndices.add(0) - - for (i in inString.indices) { - if (inString[i] == delimiter) { - startIndices.add(i + 1) - } - } - - // Find the first string between delimiters that starts with kQRCodePrefix - for (i in 0 until startIndices.size) { - val startIndex = startIndices[i] - val endIndex = if (i == startIndices.size - 1) inString.length else startIndices[i + 1] - 1 - val length = if (endIndex != inString.length) endIndex - startIndex else inString.length - val segment = inString.substring(startIndex, startIndex + length) - - // Find a segment that starts with kQRCodePrefix - if (segment.startsWith(kQRCodePrefix) && segment.length > kQRCodePrefix.length) { - chipSegment = segment - break - } - } - - if (chipSegment.length > 0) { - return chipSegment.substring(kQRCodePrefix.length) // strip out prefix before returning - } - - return chipSegment - } - fun populatePayload(outPayload: OnboardingPayload) { var indexToReadFrom: AtomicInteger = AtomicInteger(0) @@ -113,5 +80,14 @@ class QRCodeOnboardingPayloadParser(private val mBase38Representation: String) { index.addAndGet(numberOfBitsToRead) return dest } + + fun extractPayload(inString: String?): String { + return inString + ?.split('%') + ?.filter { s -> s.startsWith(kQRCodePrefix) } + ?.firstOrNull() + ?.substring(kQRCodePrefix.length) + ?: "" + } } } diff --git a/src/controller/java/tests/chip/onboardingpayload/QRCodeTest.kt b/src/controller/java/tests/chip/onboardingpayload/QRCodeTest.kt new file mode 100644 index 00000000000000..1f854758f18951 --- /dev/null +++ b/src/controller/java/tests/chip/onboardingpayload/QRCodeTest.kt @@ -0,0 +1,321 @@ +/* + * + * 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.onboardingpayload + +import com.google.common.truth.Truth.assertThat +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class QRCodeTest { + private fun getDefaultPayload(): OnboardingPayload { + val payload = OnboardingPayload() + + payload.version = 0 + payload.vendorId = 12 + payload.productId = 1 + payload.setupPinCode = 2048 + payload.commissioningFlow = CommissioningFlow.STANDARD.value + payload.discoveryCapabilities.add(DiscoveryCapability.SOFT_AP) + payload.setLongDiscriminatorValue(128) + + return payload + } + + private fun checkWriteRead( + inPayload: OnboardingPayload, + allowInvalidPayload: Boolean = false + ): Boolean { + var generator = QRCodeOnboardingPayloadGenerator(inPayload) + generator.setAllowInvalidPayload(allowInvalidPayload) + var result = generator.payloadBase38Representation() + + var outPayload = OnboardingPayload() + QRCodeOnboardingPayloadParser(result).populatePayload(outPayload) + + return inPayload == outPayload + } + + private fun compareBinary(payload: OnboardingPayload, expectedBinary: String): Boolean { + var generator = QRCodeOnboardingPayloadGenerator(payload) + var result = generator.payloadBase38Representation() + val resultBinary = toBinaryRepresentation(result) + + return (expectedBinary == resultBinary) + } + + private fun toBinaryRepresentation(base38Result: String): String { + // Remove the kQRCodePrefix + val resultWithoutPrefix = base38Result.removePrefix(kQRCodePrefix) + + // Decode the base38 encoded String + val buffer = base38Decode(resultWithoutPrefix) + + // Convert it to binary + val binaryResult = StringBuilder() + for (i in buffer.size downTo 1) { + binaryResult.append(buffer[i - 1].toString(2).padStart(8, '0')) + } + + // Insert spaces after each block + var pos = binaryResult.length + + pos -= kVersionFieldLengthInBits + binaryResult.insert(pos, " ") + + pos -= kVendorIDFieldLengthInBits + binaryResult.insert(pos, " ") + + pos -= kProductIDFieldLengthInBits + binaryResult.insert(pos, " ") + + pos -= kCommissioningFlowFieldLengthInBits + binaryResult.insert(pos, " ") + + pos -= kRendezvousInfoFieldLengthInBits + binaryResult.insert(pos, " ") + + pos -= kPayloadDiscriminatorFieldLengthInBits + binaryResult.insert(pos, " ") + + pos -= kSetupPINCodeFieldLengthInBits + binaryResult.insert(pos, " ") + + pos -= kPaddingFieldLengthInBits + binaryResult.insert(pos, " ") + + return binaryResult.toString() + } + + /* + * Test Rendezvous Flags + */ + @Test + fun testRendezvousFlags() { + val inPayload = getDefaultPayload() + + // Not having a value in rendezvousInformation is not allowed for a QR code. + inPayload.setRendezvousInformation(0L) + assertEquals(true, checkWriteRead(inPayload)) + + inPayload.setRendezvousInformation(1L shl 0) + assertEquals(true, checkWriteRead(inPayload)) + + inPayload.setRendezvousInformation(1L shl 1) + assertEquals(true, checkWriteRead(inPayload)) + + inPayload.setRendezvousInformation(1L shl 2) + assertEquals(true, checkWriteRead(inPayload)) + + inPayload.setRendezvousInformation((1L shl 0) or (1L shl 2)) + assertEquals(true, checkWriteRead(inPayload)) + + inPayload.setRendezvousInformation((1L shl 1) or (1L shl 2)) + assertEquals(true, checkWriteRead(inPayload)) + + inPayload.setRendezvousInformation((1L shl 0) or (1L shl 1) or (1L shl 2)) + assertEquals(true, checkWriteRead(inPayload)) + } + + /* + * Test Commissioning Flow + */ + @Test + fun testCommissioningFlow() { + val inPayload = getDefaultPayload() + + inPayload.commissioningFlow = CommissioningFlow.STANDARD.value + assertEquals(true, checkWriteRead(inPayload)) + + inPayload.commissioningFlow = CommissioningFlow.USER_ACTION_REQUIRED.value + assertEquals(true, checkWriteRead(inPayload)) + + inPayload.commissioningFlow = CommissioningFlow.CUSTOM.value + assertEquals(true, checkWriteRead(inPayload)) + } + + /* + * Test Maximum Values + */ + @Test + fun testMaximumValues() { + val inPayload = getDefaultPayload() + + inPayload.version = (1 shl kVersionFieldLengthInBits) - 1 + inPayload.vendorId = 0xFFFF + inPayload.productId = 0xFFFF + inPayload.commissioningFlow = CommissioningFlow.CUSTOM.value + + inPayload.setRendezvousInformation((1L shl 0) and (1L shl 1) and (1L shl 2)) + inPayload.setLongDiscriminatorValue((1 shl kVersionFieldLengthInBits) - 1) + inPayload.setupPinCode = (1L shl kVersionFieldLengthInBits) - 1 + + assertEquals(true, checkWriteRead(inPayload, allowInvalidPayload = true)) + } + + /* + * Test Payload Byte Array Representation + */ + @Test + fun testPayloadByteArrayRep() { + val payload = getDefaultPayload() + + val expected = + " 0000 000000000000000100000000000 000010000000 00000001 00 0000000000000001 0000000000001100 000" + assertEquals(true, compareBinary(payload, expected)) + } + + /* + * Test Bitset Length + */ + @Test + fun testBitsetLen() { + assertEquals(true, kTotalPayloadDataSizeInBits % 8 == 0) + } + + /* + * Test Setup Payload Verify + */ + @Test + fun testSetupPayloadVerify() { + var payload = getDefaultPayload() + assertEquals(true, payload.isValidQRCodePayload()) + + // test invalid commissioning flow + payload = getDefaultPayload() + payload.commissioningFlow = CommissioningFlow.CUSTOM.value + assertEquals(true, payload.isValidQRCodePayload()) + + // test invalid version + payload = getDefaultPayload() + payload.version = (1 shl kVersionFieldLengthInBits) + assertEquals(false, payload.isValidQRCodePayload()) + + // test invalid setup PIN + payload = getDefaultPayload() + payload.setupPinCode = (1L shl kSetupPINCodeFieldLengthInBits) + assertEquals(false, payload.isValidQRCodePayload()) + } + + /* + * Test Invalid QR Code Payload - Wrong Character Set + */ + @Test + fun testInvalidQRCodePayload_wrongCharacterSet() { + var invalidString = kDefaultPayloadQRCode + invalidString = invalidString.dropLast(1) + " " // space is not contained in the base38 alphabet + + var payload = OnboardingPayload() + + try { + QRCodeOnboardingPayloadParser(invalidString).populatePayload(payload) + assertThat(false) + } catch (e: Exception) { + println("Expected exception occurred: ${e.message}") + } + } + + /* + * Test Invalid QR Code Payload - Wrong Length + */ + @Test + fun testInvalidQRCodePayload_wrongLength() { + var invalidString = kDefaultPayloadQRCode + invalidString = invalidString.dropLast(1) + + var payload = OnboardingPayload() + + try { + QRCodeOnboardingPayloadParser(invalidString).populatePayload(payload) + assertThat(false) + } catch (e: Exception) { + println("Expected exception occurred: ${e.message}") + } + } + + /* + * Test Payload Equality + */ + @Test + fun testPayloadEquality() { + val payload = getDefaultPayload() + val equalPayload = getDefaultPayload() + assertEquals(true, payload == equalPayload) + } + + /* + * Test Payload Inequality + */ + @Test + fun testPayloadInEquality() { + val payload = getDefaultPayload() + val unequalPayload = getDefaultPayload() + + unequalPayload.setLongDiscriminatorValue(28) + unequalPayload.setupPinCode = 121233 + + assertEquals(false, payload == unequalPayload) + } + + /* + * Test QRCode to Payload Generation + */ + @Test + fun testQRCodeToPayloadGeneration() { + val payload = getDefaultPayload() + var generator = QRCodeOnboardingPayloadGenerator(payload) + var base38Rep = generator.payloadBase38Representation() + + var resultingPayload = OnboardingPayload() + QRCodeOnboardingPayloadParser(base38Rep).populatePayload(resultingPayload) + + assertEquals(true, resultingPayload.isValidQRCodePayload()) + assertEquals(true, payload == resultingPayload) + } + + /* + * Test Extract Payload + */ + @Test + fun testExtractPayload() { + assertEquals("ABC", QRCodeOnboardingPayloadParser.extractPayload("MT:ABC")) + assertEquals("", QRCodeOnboardingPayloadParser.extractPayload("MT:")) + assertEquals("", QRCodeOnboardingPayloadParser.extractPayload("H:")) + assertEquals("", QRCodeOnboardingPayloadParser.extractPayload("ASMT:")) + assertEquals("ABC", QRCodeOnboardingPayloadParser.extractPayload("Z%MT:ABC%")) + assertEquals("ABC", QRCodeOnboardingPayloadParser.extractPayload("%Z%MT:ABC%")) + assertEquals("ABC", QRCodeOnboardingPayloadParser.extractPayload("%Z%MT:ABC%DDD")) + assertEquals("ABC", QRCodeOnboardingPayloadParser.extractPayload("MT:ABC%DDD")) + assertEquals("ABC", QRCodeOnboardingPayloadParser.extractPayload("MT:ABC%")) + assertEquals("", QRCodeOnboardingPayloadParser.extractPayload("ABC")) + assertEquals("ABC", QRCodeOnboardingPayloadParser.extractPayload("Z%MT:ABC")) + assertEquals("ABC", QRCodeOnboardingPayloadParser.extractPayload("%Z%MT:ABC")) + assertEquals("", QRCodeOnboardingPayloadParser.extractPayload("%MT:")) + assertEquals("", QRCodeOnboardingPayloadParser.extractPayload("%MT:%")) + assertEquals("", QRCodeOnboardingPayloadParser.extractPayload("A%")) + assertEquals("", QRCodeOnboardingPayloadParser.extractPayload("MT:%")) + assertEquals("ABC", QRCodeOnboardingPayloadParser.extractPayload("%MT:ABC")) + } + + companion object { + const val kDefaultPayloadQRCode: String = "MT:M5L90MP500K64J00000" + } +}