diff --git a/src/core/BUILD.gn b/src/core/BUILD.gn index 0ac206a7e..95ae5c5ec 100644 --- a/src/core/BUILD.gn +++ b/src/core/BUILD.gn @@ -536,6 +536,8 @@ openthread_core_files = [ "mac/mac_header_ie.hpp", "mac/mac_links.cpp", "mac/mac_links.hpp", + "mac/mac_ltvs.cpp", + "mac/mac_ltvs.hpp", "mac/mac_types.cpp", "mac/mac_types.hpp", "mac/scan_result.cpp", diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 277d4f3b9..d28abd3f6 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -158,6 +158,7 @@ set(COMMON_SOURCES mac/mac_frame.cpp mac/mac_header_ie.cpp mac/mac_links.cpp + mac/mac_ltvs.cpp mac/mac_types.cpp mac/scan_result.cpp mac/sub_mac.cpp diff --git a/src/core/common/data.hpp b/src/core/common/data.hpp index 98ccca3d9..a8af9e67a 100644 --- a/src/core/common/data.hpp +++ b/src/core/common/data.hpp @@ -172,6 +172,14 @@ public: */ void SetLength(LengthType aLength) { mLength = aLength; } + /** + * Indicates whether or not the `Data` is empty (length is zero). + * + * @retval TRUE The `Data` is empty (length is zero). + * @retval FALSE The `Data` is not empty (length is non-zero). + */ + bool IsEmpty(void) const { return mLength == 0; } + /** * Copies the `Data` bytes to a given buffer. * diff --git a/src/core/mac/mac_ltvs.cpp b/src/core/mac/mac_ltvs.cpp new file mode 100644 index 000000000..1c3e4f854 --- /dev/null +++ b/src/core/mac/mac_ltvs.cpp @@ -0,0 +1,274 @@ +/* + * Copyright (c) 2026, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file + * This file implements support for LTVs. + */ + +#include "mac_ltvs.hpp" + +#include "common/bit_utils.hpp" + +namespace ot { +namespace Mac { + +//--------------------------------------------------------------------------------------------------------------------- +// Ltv::ParsedInfo + +Error Ltv::ParsedInfo::ParseFromAndAdvance(FrameData &aFrameData) +{ + Error error = kErrorNone; + bool packed; + uint8_t lenBitSize; + uint8_t headerByte; + + if (aFrameData.IsEmpty()) + { + error = kErrorNotFound; + ExitNow(); + } + + IgnoreError(aFrameData.ReadUint8(headerByte)); + + if (aFrameData.IsEmpty()) + { + // If no bytes remain after reading the header byte, it + // corresponds to a zero-length packed LTV, where all 8 bits + // of `headerByte` represent the Type. Note that no escape + // mask check applies here, so even Type value 255 (all 1s) + // is valid and packed. + + mLength = 0; + mType = headerByte; + mValue = aFrameData.GetBytes(); + ExitNow(); + } + + packed = false; + + lenBitSize = DetermineMinBitSizeFor(aFrameData.GetLength()); + + if (lenBitSize >= BitSizeOf(uint8_t)) + { + mLength = headerByte; + } + else + { + uint8_t lenMask = MaskForBitSize(lenBitSize); + uint8_t typeMask = ~lenMask; + + mLength = (headerByte & lenMask); + + // Check if the Type bits match the escape mask (all 1s in + // upper bits), which indicates that the LTV uses base + // (unpacked) format. + + if ((headerByte & typeMask) != typeMask) + { + mType = (headerByte & typeMask) >> lenBitSize; + packed = true; + } + } + + if (!packed) + { + SuccessOrExit(error = aFrameData.ReadUint8(mType)); + } + + VerifyOrExit(aFrameData.CanRead(mLength), error = kErrorParse); + + mValue = aFrameData.GetBytes(); + aFrameData.SkipOver(mLength); + +exit: + return error; +} + +Error Ltv::ParsedInfo::FindIn(const FrameData &aFrameData, uint8_t aType) +{ + FrameData frameData = aFrameData; + + return FindInAndAdvance(frameData, aType); +} + +Error Ltv::ParsedInfo::FindInAndAdvance(FrameData &aFrameData, uint8_t aType) +{ + Error error; + + do + { + SuccessOrExit(error = ParseFromAndAdvance(aFrameData)); + } while (mType != aType); + +exit: + return error; +} + +//--------------------------------------------------------------------------------------------------------------------- +// Ltv::AppendInfo + +void Ltv::AppendInfo::InitTypeLengthId(uint8_t aType, uint8_t aLength, uint16_t aId) +{ + mType = aType; + mLength = aLength; + mValue = nullptr; + mId = aId; +} + +bool Ltv::AppendInfo::CanUsePacked(uint8_t aLenBitSize) const +{ + // Determines whether the LTV can be encoded in packed format given + // the length field bit size `aLenBitSize`. + + bool canPack = false; + uint8_t remainingBitSize; + + VerifyOrExit(aLenBitSize < BitSizeOf(uint8_t)); + + remainingBitSize = BitSizeOf(uint8_t) - aLenBitSize; + + VerifyOrExit(DetermineMinBitSizeFor(mType) <= remainingBitSize); + + // Type value of all `1` bits cannot be used in the packed format, + // since that value is the "escape mask" that indicates base LTV + // format is used. + + VerifyOrExit(mType != MaskForBitSize(remainingBitSize)); + + canPack = true; + +exit: + return canPack; +} + +void Ltv::AppendInfo::DetermineIfPackable(uint32_t &aTotalLength) +{ + // Determines whether the LTV can be encoded in packed format + // based on its Type value and the cumulative length at this point + // (after this LTV). Also updates `aTotalLength` to include the + // total encoded size of the LTV. + + uint8_t lenBitSize; + + aTotalLength += mLength; + + if (aTotalLength == 0) + { + // If no bytes remain after reading the header byte, it + // corresponds to the special zero-length packed LTV format + // where 8 bits of `headerByte` represent the Type. + // Importantly, the escape mask check does not apply, so even + // Type value 255 (all 1s) is packed. + + mIsPackable = true; + mHeaderByte = mType; + aTotalLength++; + ExitNow(); + } + + lenBitSize = DetermineMinBitSizeFor(aTotalLength); + + if (CanUsePacked(lenBitSize)) + { + mIsPackable = true; + mHeaderByte = mLength | static_cast(mType << lenBitSize); + aTotalLength++; + ExitNow(); + } + + // Base format is used, which requires a separate 1-byte Type field. + // We account for the extra byte in `aTotalLength` and re-evaluate + // `lenBitSize` before using it to set the escape mask (all 1s in + // upper bits). + + aTotalLength++; + lenBitSize = DetermineMinBitSizeFor(aTotalLength); + + mIsPackable = false; + mHeaderByte = mLength | ~MaskForBitSize(lenBitSize); + aTotalLength++; + +exit: + return; +} + +//--------------------------------------------------------------------------------------------------------------------- +// Ltv + +Error Ltv::ValidateAllIn(const FrameData &aFrameData) +{ + FrameData frameData = aFrameData; + ParsedInfo ltvInfo; + Error error; + + do + { + error = ltvInfo.ParseFromAndAdvance(frameData); + } while (error == kErrorNone); + + return (error == kErrorNotFound) ? kErrorNone : error; +} + +Error Ltv::EncodeAndAppend(AppendInfo *aLtvList, uint16_t aNumLtvs, FrameBuilder &aBuilder) +{ + Error error = kErrorNone; + uint32_t totalLength = 0; + + VerifyOrExit(aNumLtvs > 0); + + // We first iterate backward from last to first LTV to evaluate + // packability based on cumulative length. Then append all LTVs + // in `aBuilder`. + + for (uint16_t i = aNumLtvs; i > 0; i--) + { + aLtvList[i - 1].DetermineIfPackable(totalLength); + } + + for (uint16_t i = 0; i < aNumLtvs; i++) + { + AppendInfo *ltv = &aLtvList[i]; + + SuccessOrExit(error = aBuilder.AppendUint8(ltv->mHeaderByte)); + + if (!ltv->mIsPackable) + { + SuccessOrExit(error = aBuilder.AppendUint8(ltv->mType)); + } + + ltv->mValue = static_cast(aBuilder.AppendLength(ltv->mLength)); + VerifyOrExit(ltv->mValue != nullptr, error = kErrorNoBufs); + } + +exit: + return error; +} + +} // namespace Mac +} // namespace ot diff --git a/src/core/mac/mac_ltvs.hpp b/src/core/mac/mac_ltvs.hpp new file mode 100644 index 000000000..8a91a6781 --- /dev/null +++ b/src/core/mac/mac_ltvs.hpp @@ -0,0 +1,379 @@ +/* + * Copyright (c) 2026, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file + * This file includes definitions for generating and processing MAC LTVs. + */ + +#ifndef OT_CORE_MAC_MAC_LTVS_HPP_ +#define OT_CORE_MAC_MAC_LTVS_HPP_ + +#include "openthread-core-config.h" + +#include "common/array.hpp" +#include "common/bit_utils.hpp" +#include "common/const_cast.hpp" +#include "common/error.hpp" +#include "common/frame_builder.hpp" +#include "common/frame_data.hpp" + +namespace ot { +namespace Mac { + +/** + * @addtogroup core-mac + * + * @{ + */ + +/** + * Implements Thread Header IE Length-Type-Value (LTV) processing and encoding. + */ +class Ltv +{ +public: + /** + * Represents LTV information. + */ + class Info + { + public: + /** + * Returns the LTV Type. + * + * @returns The LTV Type. + */ + uint8_t GetType(void) const { return mType; } + + /** + * Returns the LTV length in bytes. + * + * @returns The LTV length in bytes. + */ + uint8_t GetLength(void) const { return mLength; } + + protected: + Info(void) = default; + + uint8_t mType; + uint8_t mLength; + const uint8_t *mValue; + }; + + /** + * Represents parsed LTV information. + * + * All parsing and search methods (`ParseFromAndAdvance()`, `FindIn()`, `FindInAndAdvance()`) automatically handle + * packed and base (unpacked) LTV formats. + */ + class ParsedInfo : public Info + { + public: + /** + * Returns a pointer to the parsed LTV value bytes. + * + * @returns A pointer to the LTV value bytes. + */ + const void *GetValue(void) const { return mValue; } + + /** + * Returns a pointer to the parsed LTV value cast to a specified `ValueType`. + * + * @tparam ValueType The type to cast the value pointer to. + * + * @returns A pointer to the parsed LTV value cast to `const ValueType *`. + */ + template const ValueType *GetValueAs(void) const + { + return reinterpret_cast(GetValue()); + } + + /** + * Validates whether the parsed LTV matches a specific `LtvType` structure and validates its value format. + * + * @tparam LtvType The LTV structure type to validate against. `LtvType` MUST define a `uint8_t kType` + * constant and a static `bool IsValid(const ParsedInfo &aInfo)` method. + * + * @retval kErrorNone The LTV matches `LtvType::kType` and `LtvType::IsValid()` returns `true`. + * @retval kErrorParse The LTV does not match `LtvType::kType` or `LtvType::IsValid()` returns `false`. + */ + template Error ValidateAs(void) const + { + return ((GetType() == LtvType::kType) && LtvType::IsValid(*this)) ? kErrorNone : kErrorParse; + } + + //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + // Parsing or Finding LTVs in a `FrameData`. + + /** + * Parses an LTV element from a given `FrameData` and updates `aFrameData` to skip over it. + * + * @param[in,out] aFrameData The frame data buffer to parse from. + * + * @retval kErrorNone Successfully parsed the LTV element and advanced @p aFrameData. + * @retval kErrorNotFound No LTV element remains in @p aFrameData. + * @retval kErrorParse The LTV element is malformed. + */ + Error ParseFromAndAdvance(FrameData &aFrameData); + + /** + * Searches a `FrameData` for an LTV element matching a given Type. + * + * @param[in] aFrameData The frame data buffer to search within. + * @param[in] aType The LTV Type to search for. + * + * @retval kErrorNone Successfully found an LTV element matching @p aType. + * @retval kErrorNotFound No LTV element matching @p aType was found in @p aFrameData. + * @retval kErrorParse An LTV element in @p aFrameData is malformed. + */ + Error FindIn(const FrameData &aFrameData, uint8_t aType); + + /** + * Searches a `FrameData` for an LTV element matching a given Type and advances `aFrameData` past it. + * + * @param[in,out] aFrameData The frame data buffer to search within and advance. + * @param[in] aType The LTV Type to search for. + * + * @retval kErrorNone Successfully found an LTV element matching @p aType and advanced @p aFrameData. + * @retval kErrorNotFound No LTV element matching @p aType was found in @p aFrameData. + * @retval kErrorParse An LTV element in @p aFrameData is malformed. + */ + Error FindInAndAdvance(FrameData &aFrameData, uint8_t aType); + + /** + * Searches a `FrameData` for an LTV element of a specific `LtvType` and validates it. + * + * @tparam LtvType The LTV structure type to search for and validate against. `LtvType` MUST define a + * `uint8_t kType` constant and a static `bool IsValid(const ParsedInfo &aInfo)` method. + * + * @param[in] aFrameData The frame data buffer to search within. + * + * @retval kErrorNone Successfully found an LTV element matching `LtvType::kType` and validated it. + * @retval kErrorNotFound No LTV element matching `LtvType::kType` was found in @p aFrameData. + * @retval kErrorParse An LTV element in @p aFrameData is malformed or invalid per `LtvType::IsValid()`. + */ + template Error FindAndValidate(const FrameData &aFrameData) + { + Error error; + + SuccessOrExit(error = FindIn(aFrameData, LtvType::kType)); + VerifyOrExit(LtvType::IsValid(*this), error = kErrorParse); + + exit: + return error; + } + + /** + * Searches a `FrameData` for an LTV element of a specific `LtvType`, validates it, and advances `aFrameData` + * past it. + * + * @tparam LtvType The LTV structure type to search for and validate against. `LtvType` MUST define a + * `uint8_t kType` constant and a static `bool IsValid(const ParsedInfo &aInfo)` + * method. + * + * @param[in,out] aFrameData The frame data buffer to search within and advance. + * + * @retval kErrorNone Successfully found an LTV element matching `LtvType::kType`, validated it, and + * advanced @p aFrameData. + * @retval kErrorNotFound No LTV element matching `LtvType::kType` was found in @p aFrameData. + * @retval kErrorParse An LTV element in @p aFrameData is malformed or invalid per `LtvType::IsValid()`. + */ + template Error FindValidateAndAdvance(FrameData &aFrameData) + { + Error error; + + SuccessOrExit(error = FindInAndAdvance(aFrameData, LtvType::kType)); + VerifyOrExit(LtvType::IsValid(*this), error = kErrorParse); + + exit: + return error; + } + }; + + /** + * Represents information used to construct and append an LTV element. + * + * To encode and append one or more LTVs into a `FrameBuilder`: + * + * 1. Prepare an array of `AppendInfo` instances (one per LTV) and initialize each using the `Init...()` methods. + * 2. Call `EncodeAndAppend()` passing the array of `AppendInfo` entries. + * 3. `EncodeAndAppend()` determines whether each LTV can be encoded using the packed or base format. + * It appends the LTV headers to the `FrameBuilder` and allocates space for their value bytes. + * 4. After a successful call to `EncodeAndAppend()`, the caller can use `GetValue()` or `GetValueAs()` on each + * LTV entry to populate its value bytes in the frame. + */ + class AppendInfo : public Info + { + friend class Ltv; + + public: + /** + * Initializes an LTV `AppendInfo` instance with a Type and Length. + * + * @param[in] aType The LTV Type. + * @param[in] aLength The LTV length in bytes. + */ + void InitTypeLength(uint8_t aType, uint8_t aLength) { InitTypeLengthId(aType, aLength, 0); } + + /** + * Initializes an `AppendInfo` instance with a Type, Length, and an identifier. + * + * The @p aId can be used to distinguish between multiple LTV instances of the same Type in an array. + * + * @param[in] aType The LTV Type. + * @param[in] aLength The LTV length in bytes. + * @param[in] aId A caller-specified identifier. + */ + void InitTypeLengthId(uint8_t aType, uint8_t aLength, uint16_t aId); + + /** + * Initializes an `AppendInfo` instance for a specific `LtvType` with a given length and optional identifier. + * + * @tparam LtvType The LTV structure type to initialize for. `LtvType` MUST define a `uint8_t kType` constant. + * + * @param[in] aLength The LTV length in bytes. + * @param[in] aId An optional caller-specified identifier. + */ + template void InitAsWithLength(uint8_t aLength, uint16_t aId = 0) + { + InitTypeLengthId(LtvType::kType, aLength, aId); + } + + /** + * Initializes an `AppendInfo` instance for a specific fixed-length `LtvType` with an optional identifier. + * + * @tparam LtvType The LTV structure type to initialize for. `LtvType` MUST define `uint8_t kType` and + * `uint8_t kLength` constants. + * + * @param[in] aId An optional caller-specified identifier. + */ + template void InitAs(uint16_t aId = 0) + { + InitTypeLengthId(LtvType::kType, LtvType::kLength, aId); + } + + /** + * Returns the optional identifier assigned to this LTV instance. + * + * @returns The caller-specified identifier. + */ + uint16_t GetId(void) const { return mId; } + + /** + * Returns a pointer to the appended LTV value buffer. + * + * The caller can write the LTV value directly into this buffer after a successful call to `EncodeAndAppend()`. + * + * @returns A pointer to the LTV value buffer. + */ + void *GetValue(void) { return AsNonConst(mValue); } + + /** + * Returns a pointer to the appended LTV value buffer cast to a specified `ValueType`. + * + * The caller can update the LTV value using the returned pointer after a successful call to + * `EncodeAndAppend()`. + * + * @tparam ValueType The type to cast the value pointer to. + * + * @returns A pointer to the LTV value buffer cast to `ValueType *`. + */ + template ValueType *GetValueAs(void) { return reinterpret_cast(GetValue()); } + + private: + bool CanUsePacked(uint8_t aLenBitSize) const; + void DetermineIfPackable(uint32_t &aTotalLength); + + uint16_t mId; + uint8_t mHeaderByte; + bool mIsPackable; + }; + + /** + * Validates all LTV elements contained in a `FrameData`. + * + * @param[in] aFrameData The frame data buffer containing LTV elements. + * + * @retval kErrorNone All LTV elements in @p aFrameData were successfully parsed and validated. + * @retval kErrorParse An LTV element in @p aFrameData is malformed. + */ + static Error ValidateAllIn(const FrameData &aFrameData); + + /** + * Encodes and appends a list of LTV elements into a `FrameBuilder`. + * + * This method determines whether each LTV can use the packed or base format based on its Type and cumulative + * remaining length. It appends the LTV headers to @p aBuilder and reserves space for their value bytes. + * + * After this method successfully returns, the LTV value bytes in @p aBuilder are allocated but left uninitialized. + * The caller can populate the value bytes using `GetValue()` or `GetValueAs()` on each entry in @p aLtvList. + * + * @param[in,out] aLtvList An array of `AppendInfo` entries representing the LTVs to append. + * @param[in] aNumLtvs The number of LTV elements in @p aLtvList. + * @param[in,out] aBuilder The `FrameBuilder` instance to append to. + * + * @retval kErrorNone Successfully encoded and appended all LTVs. + * @retval kErrorNoBufs Insufficient space in @p aBuilder to append the LTV elements. + */ + static Error EncodeAndAppend(AppendInfo *aLtvList, uint16_t aNumLtvs, FrameBuilder &aBuilder); + +private: + Ltv(void) = delete; +}; + +//--------------------------------------------------------------------------------------------------------------------- + +/** + * Defines Target ID LTV constants and types and helper methods. + */ +struct TargetIdLtv +{ + static constexpr uint8_t kType = 0x01; ///< Target ID LTV Type. + + static constexpr uint8_t kMinLength = 1; ///< Minimum Target ID LTV length in bytes. + + /** + * Validates whether a parsed LTV is a valid Target ID LTV. + * + * @param[in] aLtv The parsed LTV to validate. + * + * @retval TRUE @p aLtv is a valid Target ID LTV. + * @retval FALSE @p aLtv is not a valid Target ID LTV. + */ + static bool IsValid(const Ltv::Info &aLtv) { return aLtv.GetLength() >= kMinLength; } +}; + +/** + * @} + */ + +} // namespace Mac +} // namespace ot + +#endif // OT_CORE_MAC_MAC_LTVS_HPP_ diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index bf274951e..df7e5b025 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -233,6 +233,7 @@ ot_unit_test(link_metrics_manager) ot_unit_test(link_quality) ot_unit_test(linked_list) ot_unit_test(lowpan) +ot_unit_test(ltv) ot_unit_test(mac_frame) ot_unit_test(macros) ot_unit_test(mdns) diff --git a/tests/unit/test_ltv.cpp b/tests/unit/test_ltv.cpp new file mode 100644 index 000000000..3acf734c1 --- /dev/null +++ b/tests/unit/test_ltv.cpp @@ -0,0 +1,735 @@ +/* + * Copyright (c) 2026, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include "common/code_utils.hpp" +#include "common/frame_builder.hpp" +#include "common/frame_data.hpp" +#include "mac/mac_ltvs.hpp" + +#include "test_platform.h" +#include "test_util.hpp" + +namespace ot { +namespace Mac { + +void TestSingleLtvEncodingAndDecoding(void) +{ + struct TestCase + { + uint8_t mType; + uint8_t mLength; + bool mShouldBePacked; + }; + + static const TestCase kTestCases[] = { + // Zero-length LTVs (len bit size = 1, available type bits = 7) + {0, 0, true}, + {1, 0, true}, + {5, 0, true}, + {64, 0, true}, + {126, 0, true}, + {127, 0, true}, + {128, 0, true}, + {255, 0, true}, + + // Length 1 LTVs (len bit size = 1, available type bits = 7) + {0, 1, true}, + {1, 1, true}, + {3, 1, true}, + {32, 1, true}, + {126, 1, true}, + {127, 1, false}, + {128, 1, false}, + {255, 1, false}, + + // Length 2 LTVs (bitSize = 2, available type bits = 6) + {1, 2, true}, + {15, 2, true}, + {31, 2, true}, + {62, 2, true}, + {63, 2, false}, + {64, 2, false}, + {255, 2, false}, + + // Length 3 LTVs (bitSize = 2, available type bits = 6) + {0, 3, true}, + {62, 3, true}, + {63, 3, false}, + {64, 3, false}, + {128, 3, false}, + + // Length 4 LTVs (bitSize = 3, available type bits = 5) + {7, 4, true}, + {30, 4, true}, + {31, 4, false}, + {32, 4, false}, + {100, 4, false}, + + // Length 7 LTVs (bitSize = 3, available type bits = 5) + {1, 7, true}, + {30, 7, true}, + {31, 7, false}, + {32, 7, false}, + + // Length 15 LTVs (bitSize = 4, available type bits = 4) + {1, 15, true}, + {7, 15, true}, + {14, 15, true}, + {15, 15, false}, + {16, 15, false}, + + // Length 31 LTVs (bitSize = 5, available type bits = 3) + {1, 31, true}, + {3, 31, true}, + {6, 31, true}, + {7, 31, false}, + {15, 31, false}, + + // Length 32 LTVs (bitSize = 6, available type bits = 2) + {0, 32, true}, + {1, 32, true}, + {2, 32, true}, + {3, 32, false}, + {4, 32, false}, + {70, 32, false}, + + // Length 64 LTVs (bitSize = 7, available type bits = 1) + {0, 64, true}, + {1, 64, false}, + {10, 64, false}, + + // Length 127 LTVs (bitSize = 7, available type bits = 1) + {0, 127, true}, + {1, 127, false}, + {127, 127, false}, + + // Large lengths (bitSize >= 8) + {0, 128, false}, + {1, 128, false}, + {10, 128, false}, + {0, 240, false}, + {0, 255, false}, + {255, 255, false}, + }; + + static constexpr uint16_t kMaxBytesToPrint = 7; + + uint8_t buffer[512]; + uint8_t valuePattern[256]; + FrameBuilder builder; + + printf("------------------------------------------------------------------------------\n"); + printf("TestTwoLtvsEncodingAndDecoding\n"); + + printf("TestSingleLtvEncodingAndDecoding\n"); + + for (size_t i = 0; i < sizeof(valuePattern); i++) + { + valuePattern[i] = static_cast(i & 0xff); + } + + for (const TestCase &testCase : kTestCases) + { + Ltv::AppendInfo appendLtv; + Ltv::ParsedInfo parsedLtv; + FrameData frameData; + uint16_t expectedHeaderSize; + uint16_t expectedTotalSize; + + // EncodeAndAppend + + builder.Init(buffer, sizeof(buffer)); + + appendLtv.InitTypeLength(testCase.mType, testCase.mLength); + SuccessOrQuit(Ltv::EncodeAndAppend(&appendLtv, 1, builder)); + + VerifyOrQuit(appendLtv.GetType() == testCase.mType); + VerifyOrQuit(appendLtv.GetLength() == testCase.mLength); + + expectedHeaderSize = testCase.mShouldBePacked ? 1 : 2; + expectedTotalSize = expectedHeaderSize + testCase.mLength; + VerifyOrQuit(builder.GetLength() == expectedTotalSize); + + VerifyOrQuit(appendLtv.GetValue() != nullptr); + memcpy(appendLtv.GetValue(), valuePattern, testCase.mLength); + + printf("type:%-3u length:%-3u -> %-6s encodedLen:%u [", testCase.mType, testCase.mLength, + testCase.mShouldBePacked ? "packed" : "base", builder.GetLength()); + + for (uint16_t i = 0; i < Min(builder.GetLength(), kMaxBytesToPrint); i++) + { + printf(" %02x", buffer[i]); + } + + printf("%s ]\n", builder.GetLength() > kMaxBytesToPrint ? " ..." : ""); + + // ValidateAllIn + + frameData.Init(buffer, builder.GetLength()); + SuccessOrQuit(Ltv::ValidateAllIn(frameData)); + + // ParseFromAndAdvance + + SuccessOrQuit(parsedLtv.ParseFromAndAdvance(frameData)); + + VerifyOrQuit(parsedLtv.GetType() == testCase.mType); + VerifyOrQuit(parsedLtv.GetLength() == testCase.mLength); + VerifyOrQuit(parsedLtv.GetValue() != nullptr); + VerifyOrQuit(memcmp(parsedLtv.GetValue(), valuePattern, testCase.mLength) == 0); + VerifyOrQuit(frameData.IsEmpty()); + VerifyOrQuit(parsedLtv.ParseFromAndAdvance(frameData) == kErrorNotFound); + + // FindIn + + frameData.Init(buffer, builder.GetLength()); + SuccessOrQuit(parsedLtv.FindIn(frameData, testCase.mType)); + VerifyOrQuit(parsedLtv.GetType() == testCase.mType); + VerifyOrQuit(parsedLtv.GetLength() == testCase.mLength); + VerifyOrQuit(frameData.GetLength() == builder.GetLength()); + } + + printf("TestSingleLtvEncodingAndDecoding - PASSED\n"); +} + +void TestTwoLtvsEncodingAndDecoding(void) +{ + struct TestCase + { + uint8_t mType; + uint8_t mLength; + }; + + static const TestCase kTestCases[] = { + // Zero length LTVs + {0, 0}, + {1, 0}, + {5, 0}, + {64, 0}, + {255, 0}, + + // Length 1 LTVs + {0, 1}, + {1, 1}, + {3, 1}, + {126, 1}, + {127, 1}, + {255, 1}, + + // Length 2 LTVs + {1, 2}, + {15, 2}, + {62, 2}, + {63, 2}, + {255, 2}, + + // Length 3 LTVs + {0, 3}, + {15, 3}, + {62, 3}, + {66, 3}, + {129, 3}, + + // Length 5 LTVs + {1, 5}, + {3, 5}, + {7, 5}, + {8, 5}, + {17, 5}, + {30, 5}, + + // Length 7 LTVs + {1, 7}, + {30, 7}, + {31, 7}, + {32, 7}, + + // Length 13 LTVs + {0, 13}, + {1, 13}, + {3, 13}, + {7, 13}, + {8, 13}, + {13, 13}, + {15, 13}, + {16, 13}, + + // Length 31 LTVs + {1, 31}, + {6, 31}, + {7, 31}, + {15, 31}, + + // Misc + {0, 100}, + {1, 100}, + {5, 100}, + {0, 200}, + {1, 200}, + {2, 200}, + {8, 200}, + }; + + static const uint8_t kSecondLtvSizes[] = { + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 29, 30, 31, + 32, 33, 34, 62, 63, 64, 65, 126, 127, 128, 129, 130, 200, 201, 202, 250, 253, 254, 255, + }; + + uint8_t buffer[512]; + uint8_t valPattern1[256]; + uint8_t valPattern2[256]; + FrameBuilder builder; + + printf("------------------------------------------------------------------------------\n"); + printf("TestTwoLtvsEncodingAndDecoding\n"); + + for (size_t i = 0; i < sizeof(valPattern1); i++) + { + valPattern1[i] = static_cast(i + 0x10); + valPattern2[i] = static_cast(i + 0x80); + } + + for (const TestCase &testCase : kTestCases) + { + for (uint8_t secondSize : kSecondLtvSizes) + { + Ltv::AppendInfo appendLtvs[2]; + Ltv::ParsedInfo parsedLtv; + FrameData frameData; + uint8_t secondType; + uint8_t secondLength; + uint16_t firstSize; + + if (secondSize == 1) + { + secondType = 1; + secondLength = 0; + } + else if (secondSize == 2) + { + secondType = 1; + secondLength = 1; + } + else + { + // Will use base (unpacked) format since type is 129 > 127 + secondType = 129; + secondLength = secondSize - 2; + } + + // EncodeAndAppend + + builder.Init(buffer, sizeof(buffer)); + + appendLtvs[0].InitTypeLength(testCase.mType, testCase.mLength); + appendLtvs[1].InitTypeLength(secondType, secondLength); + + SuccessOrQuit(Ltv::EncodeAndAppend(appendLtvs, 2, builder)); + + VerifyOrQuit(appendLtvs[0].GetType() == testCase.mType); + VerifyOrQuit(appendLtvs[0].GetLength() == testCase.mLength); + VerifyOrQuit(appendLtvs[0].GetValue() != nullptr); + memcpy(appendLtvs[0].GetValue(), valPattern1, testCase.mLength); + + VerifyOrQuit(appendLtvs[1].GetType() == secondType); + VerifyOrQuit(appendLtvs[1].GetLength() == secondLength); + VerifyOrQuit(appendLtvs[1].GetValue() != nullptr); + memcpy(appendLtvs[1].GetValue(), valPattern2, secondLength); + + VerifyOrQuit(builder.GetLength() > secondSize); + firstSize = builder.GetLength() - secondSize; + VerifyOrQuit(firstSize > testCase.mLength); + VerifyOrQuit((firstSize == testCase.mLength + 1) || (firstSize == testCase.mLength + 2)); + + fprintf(stderr, "type1:%-3u len1:%-3u | type2:%-3u len2:%-3u -> encoded-len:%-4u = [%u + %u]\n", + testCase.mType, testCase.mLength, secondType, secondLength, builder.GetLength(), firstSize, + secondSize); + + // ValidateAllIn + frameData.Init(buffer, builder.GetLength()); + SuccessOrQuit(Ltv::ValidateAllIn(frameData)); + + // Parse first LTV + SuccessOrQuit(parsedLtv.ParseFromAndAdvance(frameData)); + VerifyOrQuit(parsedLtv.GetType() == testCase.mType); + VerifyOrQuit(parsedLtv.GetLength() == testCase.mLength); + VerifyOrQuit(parsedLtv.GetValue() != nullptr); + VerifyOrQuit(memcmp(parsedLtv.GetValue(), valPattern1, testCase.mLength) == 0); + + // Parse second LTV + SuccessOrQuit(parsedLtv.ParseFromAndAdvance(frameData)); + + VerifyOrQuit(parsedLtv.GetType() == secondType); + VerifyOrQuit(parsedLtv.GetLength() == secondLength); + VerifyOrQuit(parsedLtv.GetValue() != nullptr); + VerifyOrQuit(memcmp(parsedLtv.GetValue(), valPattern2, secondLength) == 0); + + VerifyOrQuit(frameData.IsEmpty()); + VerifyOrQuit(parsedLtv.ParseFromAndAdvance(frameData) == kErrorNotFound); + + frameData.Init(buffer, builder.GetLength()); + + // Test FindIn for first LTV + SuccessOrQuit(parsedLtv.FindIn(frameData, testCase.mType)); + VerifyOrQuit(parsedLtv.GetType() == testCase.mType); + VerifyOrQuit(parsedLtv.GetLength() == testCase.mLength); + + if (secondType != testCase.mType) + { + // Test FindIn for second LTV type + SuccessOrQuit(parsedLtv.FindIn(frameData, secondType)); + VerifyOrQuit(parsedLtv.GetType() == secondType); + VerifyOrQuit(parsedLtv.GetLength() == secondLength); + } + } + } + + printf("TestTwoLtvsEncodingAndDecoding - PASSED\n"); +} + +void TestMultipleLtvsAndFinding(void) +{ + const uint8_t kType1 = 1; + const uint8_t kType2 = 2; + const uint8_t kType3 = 3; + const uint8_t kTypeNonExistent = 99; + + const uint8_t kPatternA[] = {0xaa, 0xaa}; + const uint8_t kPatternB[] = {0xbb, 0xbb, 0xbb}; + const uint8_t kPatternC[] = {0xcc, 0xcc, 0xcc, 0xcc, 0xcc}; + const uint8_t kPatternD[] = {0xdd}; + const uint8_t kPatternF[] = {0xff, 0xff, 0xff, 0xff}; + + uint8_t buffer[256]; + FrameBuilder builder; + Ltv::AppendInfo appendLtvs[6]; + Ltv::ParsedInfo parsedLtv; + FrameData frameData; + + printf("------------------------------------------------------------------------------\n"); + printf("TestMultipleLtvsAndFinding\n"); + + builder.Init(buffer, sizeof(buffer)); + + // Initialize 6 LTVs: + // LTV 0: Type 1, Length 2 (kPatternA) + // LTV 1: Type 2, Length 3 (kPatternB) + // LTV 2: Type 1, Length 5 (kPatternC) + // LTV 3: Type 3, Length 1 (kPatternD) + // LTV 4: Type 1, Length 0 (Empty) + // LTV 5: Type 2, Length 4 (kPatternF) + + appendLtvs[0].InitTypeLengthId(kType1, sizeof(kPatternA), 0); + appendLtvs[1].InitTypeLengthId(kType2, sizeof(kPatternB), 1); + appendLtvs[2].InitTypeLengthId(kType1, sizeof(kPatternC), 2); + appendLtvs[3].InitTypeLengthId(kType3, sizeof(kPatternD), 3); + appendLtvs[4].InitTypeLengthId(kType1, 0, 4); + appendLtvs[5].InitTypeLengthId(kType2, sizeof(kPatternF), 5); + + SuccessOrQuit(Ltv::EncodeAndAppend(appendLtvs, 6, builder)); + + memcpy(appendLtvs[0].GetValue(), kPatternA, sizeof(kPatternA)); + memcpy(appendLtvs[1].GetValue(), kPatternB, sizeof(kPatternB)); + memcpy(appendLtvs[2].GetValue(), kPatternC, sizeof(kPatternC)); + memcpy(appendLtvs[3].GetValue(), kPatternD, sizeof(kPatternD)); + memcpy(appendLtvs[5].GetValue(), kPatternF, sizeof(kPatternF)); + + // Validate overall buffer + frameData.Init(buffer, builder.GetLength()); + SuccessOrQuit(Ltv::ValidateAllIn(frameData)); + + // --- Test FindIn (Non-advancing search) --- + + // Repeated FindIn(kType1) must always return the first instance (LTV 0) + + frameData.Init(buffer, builder.GetLength()); + + for (int i = 0; i < 3; i++) + { + SuccessOrQuit(parsedLtv.FindIn(frameData, kType1)); + VerifyOrQuit(parsedLtv.GetType() == kType1); + VerifyOrQuit(parsedLtv.GetLength() == sizeof(kPatternA)); + VerifyOrQuit(memcmp(parsedLtv.GetValue(), kPatternA, sizeof(kPatternA)) == 0); + VerifyOrQuit(frameData.GetLength() == builder.GetLength()); + } + + // Repeated FindIn(kType2) must always return the first instance (LTV 1) + + for (int i = 0; i < 3; i++) + { + SuccessOrQuit(parsedLtv.FindIn(frameData, kType2)); + VerifyOrQuit(parsedLtv.GetType() == kType2); + VerifyOrQuit(parsedLtv.GetLength() == sizeof(kPatternB)); + VerifyOrQuit(memcmp(parsedLtv.GetValue(), kPatternB, sizeof(kPatternB)) == 0); + VerifyOrQuit(frameData.GetLength() == builder.GetLength()); + } + + // FindIn for non-existent type returns kErrorNotFound without modifying frameData + + VerifyOrQuit(parsedLtv.FindIn(frameData, kTypeNonExistent) == kErrorNotFound); + VerifyOrQuit(frameData.GetLength() == builder.GetLength()); + + // --- Test FindInAndAdvance (Advancing search to find all instances) --- + + // Iterate through all instances of kType1 + frameData.Init(buffer, builder.GetLength()); + + // 1st instance of kType1 (LTV 0) + SuccessOrQuit(parsedLtv.FindInAndAdvance(frameData, kType1)); + VerifyOrQuit(parsedLtv.GetType() == kType1); + VerifyOrQuit(parsedLtv.GetLength() == sizeof(kPatternA)); + VerifyOrQuit(memcmp(parsedLtv.GetValue(), kPatternA, sizeof(kPatternA)) == 0); + + // 2nd instance of kType1 (LTV 2) + SuccessOrQuit(parsedLtv.FindInAndAdvance(frameData, kType1)); + VerifyOrQuit(parsedLtv.GetType() == kType1); + VerifyOrQuit(parsedLtv.GetLength() == sizeof(kPatternC)); + VerifyOrQuit(memcmp(parsedLtv.GetValue(), kPatternC, sizeof(kPatternC)) == 0); + + // 3rd instance of kType1 (LTV 4, length 0) + SuccessOrQuit(parsedLtv.FindInAndAdvance(frameData, kType1)); + VerifyOrQuit(parsedLtv.GetType() == kType1); + VerifyOrQuit(parsedLtv.GetLength() == 0); + + // 4th search for kType1 returns kErrorNotFound + VerifyOrQuit(parsedLtv.FindInAndAdvance(frameData, kType1) == kErrorNotFound); + + // Iterate through all instances of kType2 + frameData.Init(buffer, builder.GetLength()); + + // 1st instance of kType2 (LTV 1) + SuccessOrQuit(parsedLtv.FindInAndAdvance(frameData, kType2)); + VerifyOrQuit(parsedLtv.GetType() == kType2); + VerifyOrQuit(parsedLtv.GetLength() == sizeof(kPatternB)); + VerifyOrQuit(memcmp(parsedLtv.GetValue(), kPatternB, sizeof(kPatternB)) == 0); + + // 2nd instance of kType2 (LTV 5) + SuccessOrQuit(parsedLtv.FindInAndAdvance(frameData, kType2)); + VerifyOrQuit(parsedLtv.GetType() == kType2); + VerifyOrQuit(parsedLtv.GetLength() == sizeof(kPatternF)); + VerifyOrQuit(memcmp(parsedLtv.GetValue(), kPatternF, sizeof(kPatternF)) == 0); + + // 3rd search for kType2 returns kErrorNotFound + VerifyOrQuit(parsedLtv.FindInAndAdvance(frameData, kType2) == kErrorNotFound); + + // Mixed search: find kType3, then search remaining buffer for kType1 and kType2 + frameData.Init(buffer, builder.GetLength()); + + SuccessOrQuit(parsedLtv.FindInAndAdvance(frameData, kType3)); + VerifyOrQuit(parsedLtv.GetType() == kType3); + VerifyOrQuit(parsedLtv.GetLength() == sizeof(kPatternD)); + + SuccessOrQuit(parsedLtv.FindInAndAdvance(frameData, kType1)); + VerifyOrQuit(parsedLtv.GetType() == kType1); + VerifyOrQuit(parsedLtv.GetLength() == 0); + + SuccessOrQuit(parsedLtv.FindInAndAdvance(frameData, kType2)); + VerifyOrQuit(parsedLtv.GetType() == kType2); + VerifyOrQuit(parsedLtv.GetLength() == sizeof(kPatternF)); + + VerifyOrQuit(frameData.IsEmpty()); + + printf("TestMultipleLtvsAndFinding - PASSED\n"); +} + +void TestLtvErrorsAndNegativeCases(void) +{ + uint8_t buffer[256]; + uint8_t smallBuffer[2]; + FrameBuilder builder; + Ltv::AppendInfo appendLtv; + Ltv::ParsedInfo parsedLtv; + FrameData frameData; + + printf("------------------------------------------------------------------------------\n"); + printf("TestLtvErrorsAndNegativeCases\n"); + + // --- EncodeAndAppend buffer overflow (kErrorNoBufs) --- + + // Trying to append a 10-byte LTV into a 2-byte FrameBuilder buffer + builder.Init(smallBuffer, sizeof(smallBuffer)); + appendLtv.InitTypeLength(/* aType */ 1, /* aLength */ 10); + VerifyOrQuit(Ltv::EncodeAndAppend(&appendLtv, 1, builder) == kErrorNoBufs); + + // --- Truncated LTV payload during parsing (kErrorParse) --- + + // Encode a valid LTV with Length = 20 + builder.Init(buffer, sizeof(buffer)); + appendLtv.InitTypeLength(/* aType */ 1, /* aLength */ 20); + SuccessOrQuit(Ltv::EncodeAndAppend(&appendLtv, 1, builder)); + + // Provide a truncated FrameData buffer (header + only 19 payload bytes instead of 20) + frameData.Init(buffer, builder.GetLength() - 1); + VerifyOrQuit(parsedLtv.ParseFromAndAdvance(frameData) == kErrorParse); + + frameData.Init(buffer, builder.GetLength() - 1); + VerifyOrQuit(Ltv::ValidateAllIn(frameData) == kErrorParse); + + printf("TestLtvErrorsAndNegativeCases - PASSED\n"); +} + +void TestAppendInfoGetId(void) +{ + uint8_t buffer[256]; + FrameBuilder builder; + Ltv::AppendInfo appendLtvs[3]; + Ltv::AppendInfo defaultInfo; + FrameData frameData; + + printf("------------------------------------------------------------------------------\n"); + printf("TestAppendInfoGetId\n"); + + // Default InitTypeLength(aType, aLength) sets mId to 0 + defaultInfo.InitTypeLength(/* aType */ 1, /* aLength */ 2); + VerifyOrQuit(defaultInfo.GetId() == 0); + + // Init with explicit aId + appendLtvs[0].InitTypeLengthId(/* aType */ 1, /* aLength */ 2, /* aId */ 0x101); + appendLtvs[1].InitTypeLengthId(/* aType */ 1, /* aLength */ 4, /* aId */ 0x102); + appendLtvs[2].InitTypeLengthId(/* aType */ 2, /* aLength */ 1, /* aId */ 0x201); + + VerifyOrQuit(appendLtvs[0].GetId() == 0x101); + VerifyOrQuit(appendLtvs[1].GetId() == 0x102); + VerifyOrQuit(appendLtvs[2].GetId() == 0x201); + + builder.Init(buffer, sizeof(buffer)); + SuccessOrQuit(Ltv::EncodeAndAppend(appendLtvs, 3, builder)); + + // Verify mId is preserved after EncodeAndAppend + VerifyOrQuit(appendLtvs[0].GetId() == 0x101); + VerifyOrQuit(appendLtvs[1].GetId() == 0x102); + VerifyOrQuit(appendLtvs[2].GetId() == 0x201); + + // Verify caller can locate AppendInfo entries by ID + for (Ltv::AppendInfo <v : appendLtvs) + { + if (ltv.GetId() == 0x101) + { + VerifyOrQuit(ltv.GetType() == 1 && ltv.GetLength() == 2); + } + else if (ltv.GetId() == 0x102) + { + VerifyOrQuit(ltv.GetType() == 1 && ltv.GetLength() == 4); + } + else if (ltv.GetId() == 0x201) + { + VerifyOrQuit(ltv.GetType() == 2 && ltv.GetLength() == 1); + } + else + { + VerifyOrQuit(false); + } + } + + frameData.Init(buffer, builder.GetLength()); + SuccessOrQuit(Ltv::ValidateAllIn(frameData)); + + printf("TestAppendInfoGetId - PASSED\n"); +} + +void TestFindAndValidate(void) +{ + uint8_t buffer[256]; + FrameBuilder builder; + Ltv::AppendInfo appendLtvs[3]; + Ltv::ParsedInfo parsedLtv; + FrameData frameData; + uint8_t targetId[] = {0x12, 0x34, 0x56}; + + printf("------------------------------------------------------------------------------\n"); + printf("TestFindAndValidate\n"); + + builder.Init(buffer, sizeof(buffer)); + + // LTV 0: Type = TargetIdLtv::kType (1), Length = 3 (Valid TargetIdLtv) + // LTV 1: Type = 2, Length = 4 + // LTV 2: Type = TargetIdLtv::kType (1), Length = 0 (Invalid TargetIdLtv, length 0 < kMinLength 1) + appendLtvs[0].InitAsWithLength(sizeof(targetId)); + appendLtvs[1].InitTypeLength(2, 4); + appendLtvs[2].InitAsWithLength(0); + + SuccessOrQuit(Ltv::EncodeAndAppend(appendLtvs, 3, builder)); + memcpy(appendLtvs[0].GetValue(), targetId, sizeof(targetId)); + + // -- Test `ValidateAs<>` along with `ParseFromAndAdvance` -- + + frameData.Init(buffer, builder.GetLength()); + + // LTV 0 is a valid TargetIdLtv + SuccessOrQuit(parsedLtv.ParseFromAndAdvance(frameData)); + SuccessOrQuit(parsedLtv.ValidateAs()); + + // LTV 1 has different type (Type 2), so ValidateAs returns kErrorParse + SuccessOrQuit(parsedLtv.ParseFromAndAdvance(frameData)); + VerifyOrQuit(parsedLtv.ValidateAs() == kErrorParse); + + // LTV 2 has matching type (TargetIdLtv::kType) but invalid length (0), so ValidateAs returns kErrorParse + SuccessOrQuit(parsedLtv.ParseFromAndAdvance(frameData)); + VerifyOrQuit(parsedLtv.ValidateAs() == kErrorParse); + + // -- Test `FindAndValidate` on valid first TargetIdLtv -- + + frameData.Init(buffer, builder.GetLength()); + SuccessOrQuit(parsedLtv.FindAndValidate(frameData)); + VerifyOrQuit(parsedLtv.GetType() == TargetIdLtv::kType); + VerifyOrQuit(parsedLtv.GetLength() == sizeof(targetId)); + VerifyOrQuit(memcmp(parsedLtv.GetValue(), targetId, sizeof(targetId)) == 0); + + VerifyOrQuit(frameData.GetLength() == builder.GetLength()); // Not Advanced + + // -- Test `FindValidateAndAdvance` -- + + frameData.Init(buffer, builder.GetLength()); + + SuccessOrQuit(parsedLtv.FindValidateAndAdvance(frameData)); + VerifyOrQuit(parsedLtv.GetType() == TargetIdLtv::kType); + VerifyOrQuit(parsedLtv.GetLength() == sizeof(targetId)); + + VerifyOrQuit(frameData.GetLength() < builder.GetLength()); + + // Next search for TargetIdLtv finds LTV 2 (Length 0) which should fail validation + VerifyOrQuit(parsedLtv.FindAndValidate(frameData) == kErrorParse); + VerifyOrQuit(parsedLtv.FindValidateAndAdvance(frameData) == kErrorParse); + + printf("TestFindAndValidate - PASSED\n"); +} + +} // namespace Mac +} // namespace ot + +int main(void) +{ + ot::Mac::TestSingleLtvEncodingAndDecoding(); + ot::Mac::TestTwoLtvsEncodingAndDecoding(); + ot::Mac::TestMultipleLtvsAndFinding(); + ot::Mac::TestLtvErrorsAndNegativeCases(); + ot::Mac::TestAppendInfoGetId(); + ot::Mac::TestFindAndValidate(); + + printf("\nAll tests passed\n"); + return 0; +}