From 0693bceb75136b089a72279f75ce758b9afc9f9b Mon Sep 17 00:00:00 2001 From: Abtin Keshavarzian Date: Wed, 27 May 2026 10:21:44 -0700 Subject: [PATCH] [bit-set] enhance `BitSet` class (#13156) This commit extends the `BitSet` class with several new methods: - `CountElements()` - `IsSubsetOf()` and `IsSupersetOf()` - `Complement()` - `UnionWith()`, `IntersectWith()`, and `SubtractWith()` - `SetMask()`, `AppendTo()`, and `ReadFrom()` message. This commit also introduces a new `BitSetUtils` non-template base class for the `BitSet` template class. This change helps optimize code by moving the common implementation logic for various bit manipulation operations out of the template, reducing template instantiation overhead. --- src/core/BUILD.gn | 1 + src/core/CMakeLists.txt | 1 + src/core/common/bit_set.cpp | 155 ++++++++++++++++++++++++++ src/core/common/bit_set.hpp | 153 +++++++++++++++++++++++--- tests/unit/CMakeLists.txt | 1 + tests/unit/test_bit_set.cpp | 212 ++++++++++++++++++++++++++++++++++++ 6 files changed, 507 insertions(+), 16 deletions(-) create mode 100644 src/core/common/bit_set.cpp create mode 100644 tests/unit/test_bit_set.cpp diff --git a/src/core/BUILD.gn b/src/core/BUILD.gn index d16c51141..79c96c21e 100644 --- a/src/core/BUILD.gn +++ b/src/core/BUILD.gn @@ -424,6 +424,7 @@ openthread_core_files = [ "common/as_core_type.hpp", "common/binary_search.cpp", "common/binary_search.hpp", + "common/bit_set.cpp", "common/bit_set.hpp", "common/bit_utils.cpp", "common/bit_utils.hpp", diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index eb5fe3a72..d61f3cd7a 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -114,6 +114,7 @@ set(COMMON_SOURCES coap/coap_secure.cpp common/appender.cpp common/binary_search.cpp + common/bit_set.cpp common/bit_utils.cpp common/crc.cpp common/data.cpp diff --git a/src/core/common/bit_set.cpp b/src/core/common/bit_set.cpp new file mode 100644 index 000000000..681a11a1a --- /dev/null +++ b/src/core/common/bit_set.cpp @@ -0,0 +1,155 @@ +/* + * 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 utility function for a bit-set. + */ + +#include "bit_set.hpp" + +#include "common/code_utils.hpp" +#include "common/message.hpp" + +namespace ot { + +bool BitSetUtils::IsAllZero(const uint8_t *aMask, uint16_t aSize) +{ + bool allZero = true; + + for (uint16_t i = 0; i < aSize; i++) + { + VerifyOrExit(aMask[i] == 0, allZero = false); + } + +exit: + return allZero; +} + +uint16_t BitSetUtils::CountBits(const uint8_t *aMask, uint16_t aSize) +{ + uint16_t count = 0; + + for (uint16_t i = 0; i < aSize; i++) + { + count += CountBitsInMask(aMask[i]); + } + + return count; +} + +void BitSetUtils::FlipBits(uint8_t *aTargetMask, uint16_t aSize, uint16_t aNumBits) +{ + for (uint16_t i = 0; i < aSize; i++) + { + aTargetMask[i] = ~aTargetMask[i]; + } + + Tidy(aTargetMask, aSize, aNumBits); +} + +bool BitSetUtils::IsSubset(const uint8_t *aMask, const uint8_t *aSuperMask, uint16_t aSize) +{ + bool isSubset = true; + + for (uint16_t i = 0; i < aSize; i++) + { + VerifyOrExit((aMask[i] & aSuperMask[i]) == aMask[i], isSubset = false); + } + +exit: + return isSubset; +} + +void BitSetUtils::Union(uint8_t *aTargetMask, const uint8_t *aMask, uint16_t aSize) +{ + for (uint16_t i = 0; i < aSize; i++) + { + aTargetMask[i] |= aMask[i]; + } +} + +void BitSetUtils::Intersect(uint8_t *aTargetMask, const uint8_t *aMask, uint16_t aSize) +{ + for (uint16_t i = 0; i < aSize; i++) + { + aTargetMask[i] &= aMask[i]; + } +} + +void BitSetUtils::Subtract(uint8_t *aTargetMask, const uint8_t *aMask, uint16_t aSize) +{ + for (uint16_t i = 0; i < aSize; i++) + { + aTargetMask[i] &= (~aMask[i]); + } +} + +void BitSetUtils::Copy(uint8_t *aTargetMask, const uint8_t *aMask, uint16_t aSize, uint16_t aNumBits) +{ + for (uint16_t i = 0; i < aSize; i++) + { + aTargetMask[i] = aMask[i]; + } + + Tidy(aTargetMask, aSize, aNumBits); +} + +void BitSetUtils::Tidy(uint8_t *aTargetMask, uint16_t aSize, uint16_t aNumBits) +{ + // `Tidy` clears any unused bits in the last byte of the mask. + // + // Since bits are allocated starting from the MSB of the first byte + // (byte 0), the unused bits will be the lowest bits of the last + // byte (byte `aSize - 1`). The number of unused bits is calculated + // as `(aSize * kBitsPerByte) - aNumBits`. + + aTargetMask[aSize - 1] &= static_cast(~(static_cast(1 << (aSize * kBitsPerByte - aNumBits)) - 1)); +} + +Error BitSetUtils::AppendMask(Message &aMessage, const uint8_t *aMask, uint16_t aSize) +{ + return aMessage.AppendBytes(aMask, aSize); +} + +Error BitSetUtils::ReadMask(const Message &aMessage, + const OffsetRange &aOffsetRange, + uint8_t *aTargetMask, + uint16_t aSize, + uint16_t aNumBits) +{ + Error error; + + SuccessOrExit(error = aMessage.Read(aOffsetRange, aTargetMask, aSize)); + Tidy(aTargetMask, aSize, aNumBits); + +exit: + return error; +} + +} // namespace ot diff --git a/src/core/common/bit_set.hpp b/src/core/common/bit_set.hpp index 05472da59..baf907e94 100644 --- a/src/core/common/bit_set.hpp +++ b/src/core/common/bit_set.hpp @@ -39,6 +39,7 @@ #include "common/bit_utils.hpp" #include "common/clearable.hpp" #include "common/equatable.hpp" +#include "common/error.hpp" namespace ot { @@ -51,13 +52,42 @@ namespace ot { * @{ */ +class Message; +class OffsetRange; + +class BitSetUtils +{ +protected: + // Common helpers used by `BitSet<>` templates + + static uint8_t BitMaskFor(uint16_t aIndex) { return (0x80 >> (aIndex & 7)); } + static bool IsAllZero(const uint8_t *aMask, uint16_t aSize); + static uint16_t CountBits(const uint8_t *aMask, uint16_t aSize); + static void FlipBits(uint8_t *aTargetMask, uint16_t aSize, uint16_t aNumBits); + static bool IsSubset(const uint8_t *aMask, const uint8_t *aSuperMask, uint16_t aSize); + static void Union(uint8_t *aTargetMask, const uint8_t *aMask, uint16_t aSize); + static void Subtract(uint8_t *aTargetMask, const uint8_t *aMask, uint16_t aSize); + static void Intersect(uint8_t *aTargetMask, const uint8_t *aMask, uint16_t aSize); + static void Copy(uint8_t *aTargetMask, const uint8_t *aMask, uint16_t aSize, uint16_t aNumBits); + static void Tidy(uint8_t *aTargetMask, uint16_t aSize, uint16_t aNumBits); + static Error AppendMask(Message &aMessage, const uint8_t *aMask, uint16_t aSize); + static Error ReadMask(const Message &aMessage, + const OffsetRange &aOffsetRange, + uint8_t *aTargetMask, + uint16_t aSize, + uint16_t aNumBits); +}; + /** * Represents a bit-set. * * @tparam kNumBits Specifies the number of bits. */ -template class BitSet : public Equatable>, public Clearable> +template +class BitSet : public Equatable>, public Clearable>, private BitSetUtils { + static_assert(kNumBits > 0, "kNumBits cannot be zero"); + public: /** * Indicates whether a given bit index is contained in the set. @@ -109,26 +139,117 @@ public: * @retval TRUE If the set is empty. * @retval FALSE If the set is not empty. */ - bool IsEmpty(void) const + bool IsEmpty(void) const { return IsAllZero(mMask, kMaskSize); } + + /** + * Returns the number of elements (bits set to TRUE) in the set. + * + * @returns The number of elements in the set. + */ + uint16_t CountElements(void) const { return CountBits(mMask, kMaskSize); } + + /** + * Indicates whether the set is a subset of another set. + * + * @param[in] aOther The other set to check against. + * + * @retval TRUE If the set is a subset of @p aOther. + * @retval FALSE If the set is not a subset of @p aOther. + */ + bool IsSubsetOf(const BitSet &aOther) const { return IsSubset(mMask, aOther.mMask, kMaskSize); } + + /** + * Indicates whether the set is a superset of another set. + * + * @param[in] aOther The other set to check against. + * + * @retval TRUE If the set is a superset of @p aOther. + * @retval FALSE If the set is not a superset of @p aOther. + */ + bool IsSupersetOf(const BitSet &aOther) const { return aOther.IsSubsetOf(*this); } + + /** + * Complements the set (flips all bits in the mask). + */ + void Complement(void) { FlipBits(mMask, kMaskSize, kNumBits); } + + /** + * Performs a union operation with another set. + * + * @param[in] aOther The other set. + */ + void UnionWith(const BitSet &aOther) { Union(mMask, aOther.mMask, kMaskSize); } + + /** + * Performs an intersection operation with another set. + * + * @param[in] aOther The other set. + */ + void IntersectWith(const BitSet &aOther) { Intersect(mMask, aOther.mMask, kMaskSize); } + + /** + * Performs a subtraction operation with another set. + * + * @param[in] aOther The other set. + */ + void SubtractWith(const BitSet &aOther) { Subtract(mMask, aOther.mMask, kMaskSize); } + + /** + * Gets a pointer to the byte array representing the bit mask. + * + * @returns A pointer to the byte array representing the bit mask. + */ + const uint8_t *GetMaskBytes(void) const { return mMask; } + + /** + * Gets the size of the bit mask in bytes. + * + * @returns The size of the bit mask in bytes. + */ + uint16_t GetMaskSize(void) const { return kMaskSize; } + + /** + * Gets the maximum number of bits in the set (`kNumBits` template parameter). + * + * @returns The maximum number of bits in the set. + */ + uint16_t GetNumBits(void) const { return kNumBits; } + + /** + * Sets the bit mask from a given byte array. + * + * @param[in] aMask A pointer to the byte array to copy from. MUST contain proper size + */ + void SetMask(const uint8_t *aMask) { Copy(mMask, aMask, kMaskSize, kNumBits); } + + /** + * Appends the bit mask bytes to a message. + * + * @param[in] aMessage The message to append to. + * + * @retval kErrorNone Successfully appended the bit mask. + * @retval kErrorNoBufs Insufficient available buffers to grow the message. + */ + Error AppendTo(Message &aMessage) const { return AppendMask(aMessage, mMask, kMaskSize); } + + /** + * Reads the bit mask bytes from a message. + * + * @param[in] aMessage The message to read from. + * @param[in] aOffsetRange The offset range in the message to read from. + * + * @retval kErrorNone Successfully read the bit mask. + * @retval kErrorParse Failed to read the bit mask. + */ + Error ReadFrom(const Message &aMessage, const OffsetRange &aOffsetRange) { - bool isEmpty = true; - - for (uint8_t byte : mMask) - { - if (byte != 0) - { - isEmpty = false; - break; - } - } - - return isEmpty; + return ReadMask(aMessage, aOffsetRange, mMask, kMaskSize, kNumBits); } private: - static uint8_t BitMaskFor(uint16_t aIndex) { return (0x80 >> (aIndex & 7)); } + static constexpr uint16_t kMaskSize = BytesForBitSize(kNumBits); - uint8_t mMask[BytesForBitSize(kNumBits)]; + uint8_t mMask[kMaskSize]; }; /** diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index dbb48f487..70f49b1f1 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -202,6 +202,7 @@ ot_unit_test(aes) ot_unit_test(array) ot_unit_test(binary_search) ot_unit_test(bit_utils) +ot_unit_test(bit_set) ot_unit_test(checksum) ot_unit_test(child) ot_unit_test(child_table) diff --git a/tests/unit/test_bit_set.cpp b/tests/unit/test_bit_set.cpp new file mode 100644 index 000000000..2dcee7a1f --- /dev/null +++ b/tests/unit/test_bit_set.cpp @@ -0,0 +1,212 @@ +/* + * 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 + +#include "test_platform.h" +#include "test_util.hpp" + +#include "common/bit_set.hpp" + +namespace ot { + +template void TestBitSetProperties(void) +{ + BitSet bitSet; + uint16_t i; + + printf("TestBitSetProperties<%u>\r\n", kNumBits); + + bitSet.Clear(); + VerifyOrQuit(bitSet.IsEmpty()); + VerifyOrQuit(bitSet.CountElements() == 0); + VerifyOrQuit(bitSet.GetNumBits() == kNumBits); + VerifyOrQuit(bitSet.GetMaskSize() == BytesForBitSize(kNumBits)); + + for (i = 0; i < kNumBits; i++) + { + VerifyOrQuit(!bitSet.Has(i)); + } + + bitSet.Add(0); + VerifyOrQuit(!bitSet.IsEmpty()); + VerifyOrQuit(bitSet.CountElements() == 1); + VerifyOrQuit(bitSet.Has(0)); + + bitSet.Remove(0); + VerifyOrQuit(bitSet.IsEmpty()); + VerifyOrQuit(!bitSet.Has(0)); + + for (i = 0; i < kNumBits; i++) + { + bitSet.Add(i); + VerifyOrQuit(bitSet.Has(i)); + VerifyOrQuit(bitSet.CountElements() == i + 1); + } + + bitSet.Clear(); + VerifyOrQuit(bitSet.IsEmpty()); + + for (i = 0; i < kNumBits; i++) + { + VerifyOrQuit(!bitSet.Has(i)); + } + + bitSet.Update(0, true); + VerifyOrQuit(bitSet.Has(0)); + bitSet.Update(0, false); + VerifyOrQuit(!bitSet.Has(0)); +} + +void TestBitSetOperations(void) +{ + BitSet<10> set1; + BitSet<10> set2; + BitSet<10> unionSet; + BitSet<10> intersectSet; + BitSet<10> subtractSet; + + printf("TestBitSetOperations\r\n"); + + set1.Clear(); + set2.Clear(); + + VerifyOrQuit(set1.IsSubsetOf(set2)); + VerifyOrQuit(set1.IsSupersetOf(set2)); + VerifyOrQuit(set1 == set2); + + set1.Add(1); + set1.Add(3); + set1.Add(8); + + VerifyOrQuit(!set1.IsEmpty()); + VerifyOrQuit(set1.CountElements() == 3); + + VerifyOrQuit(!set1.IsSubsetOf(set2)); + VerifyOrQuit(set2.IsSubsetOf(set1)); + VerifyOrQuit(set1.IsSupersetOf(set2)); + VerifyOrQuit(set1 != set2); + + VerifyOrQuit(set1.IsSubsetOf(set1)); + VerifyOrQuit(set1.IsSupersetOf(set1)); + + set2.Add(1); + set2.Add(8); + + VerifyOrQuit(set2.IsSubsetOf(set1)); + VerifyOrQuit(!set1.IsSubsetOf(set2)); + VerifyOrQuit(set1.IsSupersetOf(set2)); + + unionSet = set1; + unionSet.UnionWith(set2); + VerifyOrQuit(unionSet == set1); + + set2.Add(5); + unionSet = set1; + unionSet.UnionWith(set2); + VerifyOrQuit(unionSet.CountElements() == 4); + VerifyOrQuit(unionSet.Has(1) && unionSet.Has(3) && unionSet.Has(5) && unionSet.Has(8)); + + intersectSet = set1; + intersectSet.IntersectWith(set2); + VerifyOrQuit(intersectSet.CountElements() == 2); + VerifyOrQuit(intersectSet.Has(1) && intersectSet.Has(8)); + VerifyOrQuit(!intersectSet.Has(3) && !intersectSet.Has(5)); + + subtractSet = set1; + subtractSet.SubtractWith(set2); + VerifyOrQuit(subtractSet.CountElements() == 1); + VerifyOrQuit(subtractSet.Has(3)); +} + +void TestBitSetComplementAndTidy(void) +{ + BitSet<10> bitSet; + const uint8_t *bytes; + uint16_t i; + uint8_t dirtyMask[2]; + + printf("TestBitSetComplementAndTidy\r\n"); + + bitSet.Clear(); + bitSet.Complement(); + + for (i = 0; i < 10; i++) + { + VerifyOrQuit(bitSet.Has(i)); + } + + bytes = bitSet.GetMaskBytes(); + VerifyOrQuit(bitSet.GetMaskSize() == 2); + VerifyOrQuit(bytes[0] == 0xff); + VerifyOrQuit(bytes[1] == 0xc0); + + bitSet.Complement(); + VerifyOrQuit(bitSet.IsEmpty()); + VerifyOrQuit(bytes[0] == 0x00); + VerifyOrQuit(bytes[1] == 0x00); + + dirtyMask[0] = 0xff; + dirtyMask[1] = 0xff; + bitSet.SetMask(dirtyMask); + + for (i = 0; i < 10; i++) + { + VerifyOrQuit(bitSet.Has(i)); + } + + VerifyOrQuit(bitSet.CountElements() == 10); + + bytes = bitSet.GetMaskBytes(); + VerifyOrQuit(bytes[0] == 0xff); + VerifyOrQuit(bytes[1] == 0xc0); +} + +void TestBitSet(void) +{ + TestBitSetProperties<1>(); + TestBitSetProperties<7>(); + TestBitSetProperties<8>(); + TestBitSetProperties<9>(); + TestBitSetProperties<16>(); + TestBitSetProperties<17>(); + TestBitSetProperties<100>(); + + TestBitSetOperations(); + TestBitSetComplementAndTidy(); +} + +} // namespace ot + +int main(void) +{ + ot::TestBitSet(); + + printf("All tests passed\n"); + return 0; +}