From d952ad90856c7a6501f35a680654ed39fbcfaaf4 Mon Sep 17 00:00:00 2001 From: Abtin Keshavarzian Date: Wed, 3 Sep 2025 22:29:15 -0700 Subject: [PATCH] [bit-utils] add `CountMatchingBits` utility function (#11893) This change introduces a new utility function `CountMatchingBits()` to calculate the number of matching leading bits between two byte arrays. This new fn replaces the now-removed `Ip6::Prefix::MatchLength()`. The previous implementation was specific to the `Ip6::Prefix` class. The new generic function is placed in `common/bit_utils` and is used to update `Ip6::Prefix`, `Ip6::Address`, `Ip4::Cidr`, and `PrefixTlv`. A new unit test `test_bit_utils` is added with comprehensive tests for the new function. The existing tests for `CountBitsInMask` are also moved into this new test file. --- src/core/BUILD.gn | 1 + src/core/CMakeLists.txt | 1 + src/core/common/bit_utils.cpp | 82 +++++++++++++++ src/core/common/bit_utils.hpp | 18 ++++ src/core/net/ip4_types.cpp | 3 +- src/core/net/ip6_address.cpp | 52 +++------- src/core/net/ip6_address.hpp | 11 -- src/core/thread/network_data_tlvs.hpp | 3 +- tests/unit/CMakeLists.txt | 1 + tests/unit/test_bit_utils.cpp | 143 ++++++++++++++++++++++++++ tests/unit/test_serial_number.cpp | 18 ---- 11 files changed, 260 insertions(+), 73 deletions(-) create mode 100644 src/core/common/bit_utils.cpp create mode 100644 tests/unit/test_bit_utils.cpp diff --git a/src/core/BUILD.gn b/src/core/BUILD.gn index 771391dc7..a539fda0f 100644 --- a/src/core/BUILD.gn +++ b/src/core/BUILD.gn @@ -394,6 +394,7 @@ openthread_core_files = [ "common/binary_search.cpp", "common/binary_search.hpp", "common/bit_set.hpp", + "common/bit_utils.cpp", "common/bit_utils.hpp", "common/callback.hpp", "common/clearable.hpp", diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 85632a0a7..95487475f 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -103,6 +103,7 @@ set(COMMON_SOURCES coap/coap_secure.cpp common/appender.cpp common/binary_search.cpp + common/bit_utils.cpp common/crc.cpp common/data.cpp common/error.cpp diff --git a/src/core/common/bit_utils.cpp b/src/core/common/bit_utils.cpp new file mode 100644 index 000000000..508d5a8a0 --- /dev/null +++ b/src/core/common/bit_utils.cpp @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2025, 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 implementation of bit manipulation utility functions. + */ + +#include "bit_utils.hpp" + +#include "common/code_utils.hpp" +#include "common/num_utils.hpp" + +namespace ot { + +uint16_t CountMatchingBits(const uint8_t *aFirst, const uint8_t *aSecond, uint16_t aMaxBitLength) +{ + uint16_t remainingLen = aMaxBitLength; + uint16_t matchedLen = 0; + uint8_t diffMask = 0; + + while (remainingLen > 0) + { + uint16_t len = Min(remainingLen, kBitsPerByte); + + diffMask = (aFirst[0] ^ aSecond[0]); + + if (diffMask != 0) + { + break; + } + + matchedLen += len; + remainingLen -= len; + aFirst++; + aSecond++; + } + + VerifyOrExit(diffMask != 0); + + while (remainingLen > 0) + { + if ((diffMask & 0x80) != 0) + { + break; + } + + diffMask <<= 1; + matchedLen++; + remainingLen--; + } + +exit: + return matchedLen; +} + +} // namespace ot diff --git a/src/core/common/bit_utils.hpp b/src/core/common/bit_utils.hpp index 8169b53d9..ef74eaccc 100644 --- a/src/core/common/bit_utils.hpp +++ b/src/core/common/bit_utils.hpp @@ -84,6 +84,24 @@ template uint8_t CountBitsInMask(UintType aMask) return count; } +/** + * Counts the number of consecutive matching bits between two byte arrays. + * + * This function compares two byte arrays bit-by-bit, starting from the most significant bit (MSB) of the first byte + * in each array. The comparison proceeds until a mismatch is found or until a maximum of @p aMaxBitLength bits have + * been successfully compared. + * + * It is the caller's responsibility to ensure that both @p aFirst and @p aSecond point to buffers large enough to + * contain at least @p aMaxBitLength bits. + * + * @param[in] aFirst A pointer to the first byte array to compare. + * @param[in] aSecond A pointer to the second byte array to compare. + * @param[in] aMaxBitLength The maximum number of bits to compare from the start of the arrays. + * + * @return The number of consecutive matching bits. + */ +uint16_t CountMatchingBits(const uint8_t *aFirst, const uint8_t *aSecond, uint16_t aMaxBitLength); + /** * Sets the specified bit in a given integer to 1. * diff --git a/src/core/net/ip4_types.cpp b/src/core/net/ip4_types.cpp index 4755e790a..6d45b5529 100644 --- a/src/core/net/ip4_types.cpp +++ b/src/core/net/ip4_types.cpp @@ -191,8 +191,7 @@ Cidr::InfoString Cidr::ToString(void) const bool Cidr::operator==(const Cidr &aOther) const { - return (mLength == aOther.mLength) && - (Ip6::Prefix::MatchLength(GetBytes(), aOther.GetBytes(), Ip4::Address::kSize) >= mLength); + return (mLength == aOther.mLength) && (CountMatchingBits(GetBytes(), aOther.GetBytes(), mLength) >= mLength); } void Cidr::Set(const uint8_t *aAddress, uint8_t aLength) diff --git a/src/core/net/ip6_address.cpp b/src/core/net/ip6_address.cpp index 3eb185f95..3e1ff3981 100644 --- a/src/core/net/ip6_address.cpp +++ b/src/core/net/ip6_address.cpp @@ -69,19 +69,19 @@ bool Prefix::IsUniqueLocal(void) const { return (mLength >= 7) && ((mPrefix.mFie bool Prefix::IsEqual(const uint8_t *aPrefixBytes, uint8_t aPrefixLength) const { - return (mLength == aPrefixLength) && (MatchLength(GetBytes(), aPrefixBytes, GetBytesSize()) >= mLength); + return (mLength == aPrefixLength) && (CountMatchingBits(GetBytes(), aPrefixBytes, mLength) >= mLength); } bool Prefix::ContainsPrefix(const Prefix &aSubPrefix) const { return (mLength >= aSubPrefix.mLength) && - (MatchLength(GetBytes(), aSubPrefix.GetBytes(), aSubPrefix.GetBytesSize()) >= aSubPrefix.GetLength()); + (CountMatchingBits(GetBytes(), aSubPrefix.GetBytes(), aSubPrefix.GetLength()) >= aSubPrefix.GetLength()); } bool Prefix::ContainsPrefix(const NetworkPrefix &aSubPrefix) const { return (mLength >= NetworkPrefix::kLength) && - (MatchLength(GetBytes(), aSubPrefix.m8, NetworkPrefix::kSize) >= NetworkPrefix::kLength); + (CountMatchingBits(GetBytes(), aSubPrefix.m8, NetworkPrefix::kLength) >= NetworkPrefix::kLength); } void Prefix::Tidy(void) @@ -102,17 +102,18 @@ void Prefix::Tidy(void) bool Prefix::operator==(const Prefix &aOther) const { - return (mLength == aOther.mLength) && (MatchLength(GetBytes(), aOther.GetBytes(), GetBytesSize()) >= GetLength()); + return (mLength == aOther.mLength) && + (CountMatchingBits(GetBytes(), aOther.GetBytes(), GetLength()) >= GetLength()); } bool Prefix::operator<(const Prefix &aOther) const { - bool isSmaller; - uint8_t minLength; - uint8_t matchedLength; + bool isSmaller; + uint8_t minLength; + uint16_t matchedLength; minLength = Min(GetLength(), aOther.GetLength()); - matchedLength = MatchLength(GetBytes(), aOther.GetBytes(), SizeForLength(minLength)); + matchedLength = CountMatchingBits(GetBytes(), aOther.GetBytes(), minLength); if (matchedLength >= minLength) { @@ -126,35 +127,6 @@ exit: return isSmaller; } -uint8_t Prefix::MatchLength(const uint8_t *aPrefixA, const uint8_t *aPrefixB, uint8_t aMaxSize) -{ - uint8_t matchedLength = 0; - - OT_ASSERT(aMaxSize <= Address::kSize); - - for (uint8_t i = 0; i < aMaxSize; i++) - { - uint8_t diff = aPrefixA[i] ^ aPrefixB[i]; - - if (diff == 0) - { - matchedLength += kBitsPerByte; - } - else - { - while ((diff & 0x80) == 0) - { - matchedLength++; - diff <<= 1; - } - - break; - } - } - - return matchedLength; -} - bool Prefix::IsValidNat64PrefixLength(uint8_t aLength) { return (aLength == 32) || (aLength == 40) || (aLength == 48) || (aLength == 56) || (aLength == 64) || @@ -381,12 +353,12 @@ void Address::SetToIp4Mapped(const Ip4::Address &aIp4Address) bool Address::MatchesPrefix(const Prefix &aPrefix) const { - return Prefix::MatchLength(mFields.m8, aPrefix.GetBytes(), aPrefix.GetBytesSize()) >= aPrefix.GetLength(); + return CountMatchingBits(mFields.m8, aPrefix.GetBytes(), aPrefix.GetLength()) >= aPrefix.GetLength(); } bool Address::MatchesPrefix(const uint8_t *aPrefix, uint8_t aPrefixLength) const { - return Prefix::MatchLength(mFields.m8, aPrefix, Prefix::SizeForLength(aPrefixLength)) >= aPrefixLength; + return CountMatchingBits(mFields.m8, aPrefix, aPrefixLength) >= aPrefixLength; } void Address::SetPrefix(const NetworkPrefix &aNetworkPrefix) { mFields.mComponents.mNetworkPrefix = aNetworkPrefix; } @@ -455,7 +427,7 @@ uint8_t Address::GetScope(void) const uint8_t Address::PrefixMatch(const Address &aOther) const { - return Prefix::MatchLength(mFields.m8, aOther.mFields.m8, sizeof(Address)); + return static_cast(CountMatchingBits(mFields.m8, aOther.mFields.m8, BitSizeOf(Address))); } bool Address::MatchesFilter(TypeFilter aFilter) const diff --git a/src/core/net/ip6_address.hpp b/src/core/net/ip6_address.hpp index aeb0e11f0..422182be8 100644 --- a/src/core/net/ip6_address.hpp +++ b/src/core/net/ip6_address.hpp @@ -258,17 +258,6 @@ public: */ static uint8_t SizeForLength(uint8_t aLength) { return BytesForBitSize(aLength); } - /** - * Returns the number of IPv6 prefix bits that match. - * - * @param[in] aPrefixA A pointer to a byte array containing a first prefix. - * @param[in] aPrefixB A pointer to a byte array containing a second prefix. - * @param[in] aMaxSize Number of bytes of the two prefixes. - * - * @returns The number of prefix bits that match. - */ - static uint8_t MatchLength(const uint8_t *aPrefixA, const uint8_t *aPrefixB, uint8_t aMaxSize); - /** * Indicates whether or not a given prefix length is valid for use as a NAT64 prefix. * diff --git a/src/core/thread/network_data_tlvs.hpp b/src/core/thread/network_data_tlvs.hpp index 4dc56f0ba..a871b1478 100644 --- a/src/core/thread/network_data_tlvs.hpp +++ b/src/core/thread/network_data_tlvs.hpp @@ -680,8 +680,7 @@ public: bool IsEqual(const uint8_t *aPrefix, uint8_t aPrefixLength) const { return (aPrefixLength == mPrefixLength) && - (Ip6::Prefix::MatchLength(GetPrefix(), aPrefix, Ip6::Prefix::SizeForLength(aPrefixLength)) >= - mPrefixLength); + (CountMatchingBits(GetPrefix(), aPrefix, aPrefixLength) >= mPrefixLength); } /** diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 71c3ea41d..3f1996c53 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -201,6 +201,7 @@ ot_unit_test(address_sanitizer) ot_unit_test(aes) ot_unit_test(array) ot_unit_test(binary_search) +ot_unit_test(bit_utils) ot_unit_test(checksum) ot_unit_test(child) ot_unit_test(child_table) diff --git a/tests/unit/test_bit_utils.cpp b/tests/unit/test_bit_utils.cpp new file mode 100644 index 000000000..a141a850f --- /dev/null +++ b/tests/unit/test_bit_utils.cpp @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2025, 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 + +#include "test_platform.h" +#include "test_util.hpp" + +#include "common/bit_utils.hpp" + +namespace ot { + +void TestCountBitsInMask(void) +{ + VerifyOrQuit(CountBitsInMask(0) == 0); + VerifyOrQuit(CountBitsInMask(1) == 1); + VerifyOrQuit(CountBitsInMask(2) == 1); + VerifyOrQuit(CountBitsInMask(3) == 2); + VerifyOrQuit(CountBitsInMask(4) == 1); + VerifyOrQuit(CountBitsInMask(7) == 3); + VerifyOrQuit(CountBitsInMask(11) == 3); + VerifyOrQuit(CountBitsInMask(15) == 4); + VerifyOrQuit(CountBitsInMask(0x11) == 2); + VerifyOrQuit(CountBitsInMask(0xef) == 7); + VerifyOrQuit(CountBitsInMask(0xff) == 8); + + VerifyOrQuit(CountBitsInMask(0) == 0); + VerifyOrQuit(CountBitsInMask(0xff00) == 8); + VerifyOrQuit(CountBitsInMask(0xff) == 8); + VerifyOrQuit(CountBitsInMask(0xaa55) == 8); + VerifyOrQuit(CountBitsInMask(0xffff) == 16); + + printf("TestCountBitsInMask() passed\n"); +} + +void TestCountMatchingBitsAllCombinations(void) +{ + // This test iterates through all possible byte combinations and + // bit-lengths (0-8). It calculates the expected matched length + // by exhaustively checking bits one-by-one and then verifies + // that `CountMatchingBits()` returns the correct length. + + uint8_t firstByte = 0; + + do + { + uint8_t secondByte = 0; + + do + { + for (uint8_t blen = 0; blen <= 8; blen++) + { + uint16_t matchedLen = 0; + + for (uint8_t i = 0; i < blen; i++) + { + uint8_t bit = (0x80 >> i); + + if ((firstByte & bit) != (secondByte & bit)) + { + break; + } + + matchedLen++; + } + + VerifyOrQuit(CountMatchingBits(&firstByte, &secondByte, blen) == matchedLen); + } + + secondByte++; + } while (secondByte != 0); + + firstByte++; + + } while (firstByte != 0); + + printf("TestCountMatchingBitsAllCombinations() passed\n"); +} + +void TestCountMatchingBitsExamples(void) +{ + struct TestCase + { + uint8_t mFirst[3]; + uint8_t mSecond[3]; + uint8_t mBitLength; + uint8_t mExpectedMatchedLength; + }; + + static const TestCase kTestCases[] = { + {{0x00, 0x00, 0x00}, {0x00, 0x11, 0x22}, 0, 0}, {{0x6d, 0x13, 0xb0}, {0x6d, 0x13, 0xb0}, 20, 20}, + {{0x6d, 0x13, 0xb0}, {0x6d, 0x13, 0xbf}, 20, 20}, {{0x6d, 0x13, 0xb0}, {0x6d, 0x13, 0xa0}, 20, 19}, + {{0x6d, 0xa3, 0xb0}, {0x6d, 0xa3, 0xa0}, 20, 19}, {{0x77, 0xa3, 0x25}, {0x77, 0xa3, 0xa5}, 20, 16}, + {{0x77, 0xa3, 0x25}, {0x77, 0xa3, 0x65}, 20, 17}, {{0x77, 0xa3, 0x25}, {0x77, 0xa3, 0x05}, 20, 18}, + {{0x77, 0xa3, 0x25}, {0x77, 0xa3, 0x05}, 18, 18}, {{0x77, 0xa3, 0x25}, {0x77, 0xa3, 0x05}, 17, 17}, + }; + + for (const TestCase &testCase : kTestCases) + { + uint16_t matchedLen = CountMatchingBits(testCase.mFirst, testCase.mSecond, testCase.mBitLength); + + VerifyOrQuit(matchedLen == testCase.mExpectedMatchedLength); + } + + printf("TestCountMatchingBitsExamples() passed\n"); +} + +} // namespace ot + +int main(void) +{ + ot::TestCountBitsInMask(); + ot::TestCountMatchingBitsAllCombinations(); + ot::TestCountMatchingBitsExamples(); + + printf("All tests passed\n"); + return 0; +} diff --git a/tests/unit/test_serial_number.cpp b/tests/unit/test_serial_number.cpp index bbd20a212..bba347fdb 100644 --- a/tests/unit/test_serial_number.cpp +++ b/tests/unit/test_serial_number.cpp @@ -178,24 +178,6 @@ void TestNumUtils(void) VerifyOrQuit(DivideAndRoundUp(10, 10) == 1); VerifyOrQuit(DivideAndRoundUp(11, 10) == 2); - VerifyOrQuit(CountBitsInMask(0) == 0); - VerifyOrQuit(CountBitsInMask(1) == 1); - VerifyOrQuit(CountBitsInMask(2) == 1); - VerifyOrQuit(CountBitsInMask(3) == 2); - VerifyOrQuit(CountBitsInMask(4) == 1); - VerifyOrQuit(CountBitsInMask(7) == 3); - VerifyOrQuit(CountBitsInMask(11) == 3); - VerifyOrQuit(CountBitsInMask(15) == 4); - VerifyOrQuit(CountBitsInMask(0x11) == 2); - VerifyOrQuit(CountBitsInMask(0xef) == 7); - VerifyOrQuit(CountBitsInMask(0xff) == 8); - - VerifyOrQuit(CountBitsInMask(0) == 0); - VerifyOrQuit(CountBitsInMask(0xff00) == 8); - VerifyOrQuit(CountBitsInMask(0xff) == 8); - VerifyOrQuit(CountBitsInMask(0xaa55) == 8); - VerifyOrQuit(CountBitsInMask(0xffff) == 16); - printf("TestNumUtils() passed\n"); }