diff --git a/src/lib/core/DataModelTypes.h b/src/lib/core/DataModelTypes.h index a25eb3e53ebd3b..fb79584c5c6988 100644 --- a/src/lib/core/DataModelTypes.h +++ b/src/lib/core/DataModelTypes.h @@ -1,6 +1,6 @@ /* * - * Copyright (c) 2021 Project CHIP Authors + * Copyright (c) 2021-2023 Project CHIP Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,6 +41,7 @@ typedef uint64_t FabricId; typedef uint8_t FabricIndex; typedef uint32_t FieldId; typedef uint16_t ListIndex; +typedef uint16_t LocalizedStringIdentifier; typedef uint32_t TransactionId; typedef uint16_t KeysetId; typedef uint8_t InteractionModelRevision; diff --git a/src/lib/core/TLVReader.cpp b/src/lib/core/TLVReader.cpp index 335e44b457f6f1..3c5e3e9c2a8f69 100644 --- a/src/lib/core/TLVReader.cpp +++ b/src/lib/core/TLVReader.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -298,10 +299,13 @@ CHIP_ERROR TLVReader::Get(ByteSpan & v) return CHIP_NO_ERROR; } +namespace { +constexpr int kUnicodeInformationSeparator1 = 0x1F; +constexpr size_t kMaxLocalizedStringIdentifierLen = 2 * sizeof(LocalizedStringIdentifier); +} // namespace + CHIP_ERROR TLVReader::Get(CharSpan & v) { - constexpr int kUnicodeInformationSeparator1 = 0x1F; - if (!TLVTypeIsUTF8String(ElementType())) { return CHIP_ERROR_WRONG_TLV_TYPE; @@ -324,6 +328,49 @@ CHIP_ERROR TLVReader::Get(CharSpan & v) return CHIP_NO_ERROR; } +CHIP_ERROR TLVReader::Get(Optional & lsid) +{ + lsid.ClearValue(); + VerifyOrReturnError(TLVTypeIsUTF8String(ElementType()), CHIP_ERROR_WRONG_TLV_TYPE); + + const uint8_t * bytes; + ReturnErrorOnFailure(GetDataPtr(bytes)); // Does length sanity checks + + uint32_t len = GetLength(); + + const uint8_t * infoSeparator1 = static_cast(memchr(bytes, kUnicodeInformationSeparator1, len)); + if (infoSeparator1 == nullptr) + { + return CHIP_NO_ERROR; + } + + const uint8_t * lsidPtr = infoSeparator1 + 1; + len -= static_cast(lsidPtr - bytes); + + const uint8_t * infoSeparator2 = static_cast(memchr(lsidPtr, kUnicodeInformationSeparator1, len)); + if (infoSeparator2 != nullptr) + { + len = static_cast(infoSeparator2 - lsidPtr); + } + if (len == 0) + { + return CHIP_NO_ERROR; + } + VerifyOrReturnError(len <= kMaxLocalizedStringIdentifierLen, CHIP_ERROR_INVALID_TLV_ELEMENT); + // Leading zeroes are not allowed. + VerifyOrReturnError(static_cast(lsidPtr[0]) != '0', CHIP_ERROR_INVALID_TLV_ELEMENT); + + char idStr[kMaxLocalizedStringIdentifierLen] = { '0', '0', '0', '0' }; + memcpy(&idStr[kMaxLocalizedStringIdentifierLen - len], lsidPtr, len); + + LocalizedStringIdentifier id; + VerifyOrReturnError(Encoding::UppercaseHexToUint16(idStr, sizeof(idStr), id) == sizeof(LocalizedStringIdentifier), + CHIP_ERROR_INVALID_TLV_ELEMENT); + + lsid.SetValue(id); + return CHIP_NO_ERROR; +} + CHIP_ERROR TLVReader::GetBytes(uint8_t * buf, size_t bufSize) { if (!TLVTypeIsString(ElementType())) diff --git a/src/lib/core/TLVReader.h b/src/lib/core/TLVReader.h index f41b8c30ca6773..29f5812039187b 100644 --- a/src/lib/core/TLVReader.h +++ b/src/lib/core/TLVReader.h @@ -27,6 +27,9 @@ #pragma once +#include +#include + #include "TLVCommon.h" #include "TLVWriter.h" @@ -472,6 +475,22 @@ class DLL_EXPORT TLVReader */ CHIP_ERROR Get(CharSpan & v); + /** + * Get the Localized String Identifier contained in the current element.. + * + * The method takes what's after the first Information Separator 1 , and until end of string + * or second , and return the hex-decoded string identifier, if one was there. + * + * @param[out] lsid Optional Localized String Identifier. Returns empty + * if the value is not found or it was invalidly encoded. + * + * @retval #CHIP_NO_ERROR If the method succeeded. + * @retval #CHIP_ERROR_WRONG_TLV_TYPE If the current element is not a TLV character string, or + * the reader is not positioned on an element. + * @retval #CHIP_ERROR_INVALID_TLV_ELEMENT If the Localized String Identifier is malformed. + */ + CHIP_ERROR Get(Optional & lsid); + /** * Get the value of the current element as an enum value, if it's an integer * value that fits in the enum type. diff --git a/src/lib/core/tests/TestTLV.cpp b/src/lib/core/tests/TestTLV.cpp index ec38b9463ecda7..0ea46d463bcc3e 100644 --- a/src/lib/core/tests/TestTLV.cpp +++ b/src/lib/core/tests/TestTLV.cpp @@ -2789,6 +2789,8 @@ void CheckTLVByteSpan(nlTestSuite * inSuite, void * inContext) NL_TEST_ASSERT(inSuite, memcmp(readerSpan.data(), bytesBuffer, sizeof(bytesBuffer)) == 0); } +#define IS1_CHAR "\x1F" + void CheckTLVCharSpan(nlTestSuite * inSuite, void * inContext) { struct CharSpanTestCase @@ -2799,12 +2801,14 @@ void CheckTLVCharSpan(nlTestSuite * inSuite, void * inContext) // clang-format off static CharSpanTestCase sCharSpanTestCases[] = { - // Test String Expected String from Get() - // ========================================================================================= - { "This is a test case #0", "This is a test case #0" }, - { "This is a test case #1\x1fTest Localized String Identifier", "This is a test case #1" }, - { "This is a test case #2 \x1f abc \x1f def", "This is a test case #2 " }, - { "This is a test case #3\x1f", "This is a test case #3" }, + // Test String Expected String from Get() + // ================================================================================================== + { "This is a test case #0", "This is a test case #0" }, + { "This is a test case #1" IS1_CHAR "Test Localized String Identifier", "This is a test case #1" }, + { "This is a test case #2 " IS1_CHAR "abc" IS1_CHAR "def", "This is a test case #2 " }, + { "This is a test case #3" IS1_CHAR, "This is a test case #3" }, + { "Thé" IS1_CHAR, "Thé" }, + { IS1_CHAR " abc " IS1_CHAR " def", "" }, }; // clang-format on @@ -2836,6 +2840,110 @@ void CheckTLVCharSpan(nlTestSuite * inSuite, void * inContext) } } +void CheckTLVGetLocalizedStringIdentifier(nlTestSuite * inSuite, void * inContext) +{ + struct CharSpanTestCase + { + const char * testString; + Optional expectedLSID; + CHIP_ERROR expectedResult; + }; + + // clang-format off + static CharSpanTestCase sCharSpanTestCases[] = { + // Test String Expected LocalizedStringIdentifier from Get() Expected Return + // ============================================================================================================================================= + { "This is a test case #0", chip::Optional(), CHIP_NO_ERROR }, + { "This is a test case #1" IS1_CHAR "0123", chip::Optional(), CHIP_ERROR_INVALID_TLV_ELEMENT }, + { "This is a test case #2" IS1_CHAR "123" IS1_CHAR "3210", chip::Optional(0x123), CHIP_NO_ERROR }, + { "This is a test case #3" IS1_CHAR "012", chip::Optional(), CHIP_ERROR_INVALID_TLV_ELEMENT }, + { "This is a test case #3" IS1_CHAR "12", chip::Optional(0x12), CHIP_NO_ERROR }, + { "Thé" IS1_CHAR "", chip::Optional(), CHIP_NO_ERROR }, + { "Thé" IS1_CHAR "7", chip::Optional(0x7), CHIP_NO_ERROR }, + { "Thé" IS1_CHAR "1FA", chip::Optional(0x1FA), CHIP_NO_ERROR }, + { "" IS1_CHAR "1FA", chip::Optional(0x1FA), CHIP_NO_ERROR }, + { "Thé" IS1_CHAR "1FAB", chip::Optional(0x1FAB), CHIP_NO_ERROR }, + { "Thé" IS1_CHAR "1FAb", chip::Optional(), CHIP_ERROR_INVALID_TLV_ELEMENT }, + { "Thé" IS1_CHAR "1FABC", chip::Optional(), CHIP_ERROR_INVALID_TLV_ELEMENT }, + { "Thé" IS1_CHAR "1FA" IS1_CHAR "", chip::Optional(0x1FA), CHIP_NO_ERROR }, + { "Thé" IS1_CHAR "1FA" IS1_CHAR "F8sa===", chip::Optional(0x1FA), CHIP_NO_ERROR }, + }; + // clang-format on + + for (auto & testCase : sCharSpanTestCases) + { + uint8_t backingStore[100]; + TLVWriter writer; + TLVReader reader; + CHIP_ERROR err = CHIP_NO_ERROR; + + writer.Init(backingStore); + + err = writer.PutString(ProfileTag(TestProfile_1, 1), testCase.testString); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + + err = writer.Finalize(); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + + reader.Init(backingStore, writer.GetLengthWritten()); + err = reader.Next(); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + + Optional readerLSID; + err = reader.Get(readerLSID); + NL_TEST_ASSERT(inSuite, testCase.expectedResult == err); + NL_TEST_ASSERT(inSuite, testCase.expectedLSID == readerLSID); + } + + // Error case: A case of TLVReader buffer underrun. + // Expected error after Next() call is: CHIP_ERROR_TLV_UNDERRUN + { + uint8_t backingStore[100]; + TLVWriter writer; + TLVReader reader; + CHIP_ERROR err = CHIP_NO_ERROR; + + writer.Init(backingStore); + + err = writer.PutString(ProfileTag(TestProfile_1, 1), sCharSpanTestCases[2].testString); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + + err = writer.Finalize(); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + + reader.Init(backingStore, writer.GetLengthWritten() - 1); + err = reader.Next(); + NL_TEST_ASSERT(inSuite, err == CHIP_ERROR_TLV_UNDERRUN); + } + + // Error case: the reader is on a bytestring, not utf-8 string. + // Expected error after Get(Optional &) call is: CHIP_ERROR_WRONG_TLV_TYPE + { + uint8_t backingStore[100]; + TLVWriter writer; + TLVReader reader; + CHIP_ERROR err = CHIP_NO_ERROR; + + writer.Init(backingStore); + + err = writer.PutBytes(ProfileTag(TestProfile_1, 1), reinterpret_cast(sCharSpanTestCases[2].testString), + static_cast(strlen(sCharSpanTestCases[2].testString))); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + + err = writer.Finalize(); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + + reader.Init(backingStore, writer.GetLengthWritten()); + err = reader.Next(); + NL_TEST_ASSERT(inSuite, err == CHIP_NO_ERROR); + + Optional readerLSID; + err = reader.Get(readerLSID); + NL_TEST_ASSERT(inSuite, err == CHIP_ERROR_WRONG_TLV_TYPE); + NL_TEST_ASSERT(inSuite, readerLSID == Optional()); + } +} + void CheckTLVSkipCircular(nlTestSuite * inSuite, void * inContext) { const size_t bufsize = 40; // large enough s.t. 2 elements fit, 3rd causes eviction @@ -4514,6 +4622,7 @@ static const nlTest sTests[] = NL_TEST_DEF("CHIP TLV Skip non-contiguous", CheckTLVSkipCircular), NL_TEST_DEF("CHIP TLV ByteSpan", CheckTLVByteSpan), NL_TEST_DEF("CHIP TLV CharSpan", CheckTLVCharSpan), + NL_TEST_DEF("CHIP TLV Get LocalizedStringIdentifier", CheckTLVGetLocalizedStringIdentifier), NL_TEST_DEF("CHIP TLV Scoped Buffer", CheckTLVScopedBuffer), NL_TEST_DEF("CHIP TLV Check reserve", CheckCloseContainerReserve), NL_TEST_DEF("CHIP TLV Reader Fuzz Test", TLVReaderFuzzTest),