This is an automated email from the ASF dual-hosted git repository.
twice pushed a commit to branch unstable
in repository https://gitbox.apache.org/repos/asf/kvrocks.git
The following commit(s) were added to refs/heads/unstable by this push:
new aa1de3f5 Fix BITCOUNT/BITPOS negative handling (#2069)
aa1de3f5 is described below
commit aa1de3f53252a16e43d381fec83d64ec59228869
Author: mwish <[email protected]>
AuthorDate: Mon Jan 29 20:31:38 2024 +0800
Fix BITCOUNT/BITPOS negative handling (#2069)
Fixing and testing BITCOUNT with negative start/stop.
This patch:
1. Add a `BitmapString::NormalizeRange` for handling the range
2. For BITCOUNT/BITPOS, fixing the syntax of `value.size()`
3. Add the testing
---
src/types/redis_bitmap.cc | 39 ++++++++------
src/types/redis_bitmap_string.cc | 25 ++++-----
src/types/redis_bitmap_string.h | 10 ++++
tests/cppunit/types/bitmap_test.cc | 71 ++++++++++++++++++++++++++
tests/gocase/unit/type/strings/strings_test.go | 6 +++
5 files changed, 122 insertions(+), 29 deletions(-)
diff --git a/src/types/redis_bitmap.cc b/src/types/redis_bitmap.cc
index 31cd55ba..60569a9f 100644
--- a/src/types/redis_bitmap.cc
+++ b/src/types/redis_bitmap.cc
@@ -188,6 +188,7 @@ rocksdb::Status Bitmap::SetBit(const Slice &user_key,
uint32_t offset, bool new_
uint32_t byte_index = (offset / 8) % kBitmapSegmentBytes;
uint64_t used_size = index + byte_index + 1;
uint64_t bitmap_size = std::max(used_size, metadata.size);
+ // NOTE: value.size() might be greater than metadata.size.
ExpandBitmapSegment(&value, byte_index + 1);
uint32_t bit_offset = offset % 8;
*old_bit = (value[byte_index] & (1 << bit_offset)) != 0;
@@ -223,10 +224,10 @@ rocksdb::Status Bitmap::BitCount(const Slice &user_key,
int64_t start, int64_t s
return bitmap_string_db.BitCount(raw_value, start, stop, cnt);
}
- if (start < 0) start += static_cast<int64_t>(metadata.size) + 1;
- if (stop < 0) stop += static_cast<int64_t>(metadata.size) + 1;
- if (stop > static_cast<int64_t>(metadata.size)) stop =
static_cast<int64_t>(metadata.size);
- if (start < 0 || stop <= 0 || start >= stop) return rocksdb::Status::OK();
+ // Counting bits in byte [start, stop].
+ std::tie(start, stop) = BitmapString::NormalizeRange(start, stop,
static_cast<int64_t>(metadata.size));
+ // Always return 0 if start is greater than stop after normalization.
+ if (start > stop) return rocksdb::Status::OK();
auto u_start = static_cast<uint32_t>(start);
auto u_stop = static_cast<uint32_t>(stop);
@@ -244,12 +245,17 @@ rocksdb::Status Bitmap::BitCount(const Slice &user_key,
int64_t start, int64_t s
.Encode();
s = storage_->Get(read_options, sub_key, &pin_value);
if (!s.ok() && !s.IsNotFound()) return s;
+ // NotFound means all bits in this segment are 0.
if (s.IsNotFound()) continue;
- size_t j = 0;
- if (i == start_index) j = u_start % kBitmapSegmentBytes;
- auto k = static_cast<int64_t>(pin_value.size());
- if (i == stop_index) k = u_stop % kBitmapSegmentBytes + 1;
- *cnt += BitmapString::RawPopcount(reinterpret_cast<const uint8_t
*>(pin_value.data()) + j, k);
+ // Counting bits in [start_in_segment, start_in_segment +
length_in_segment)
+ size_t start_in_segment = 0;
+ if (i == start_index) start_in_segment = u_start % kBitmapSegmentBytes;
+ // Though `ExpandBitmapSegment` might generate a segment with logical size
less than pin_value.size(),
+ // the `RawPopcount` will always return 0 on these padding bytes, so we
don't need to worry about it.
+ auto length_in_segment = static_cast<int64_t>(pin_value.size());
+ if (i == stop_index) length_in_segment = u_stop % kBitmapSegmentBytes + 1;
+ *cnt += BitmapString::RawPopcount(reinterpret_cast<const uint8_t
*>(pin_value.data()) + start_in_segment,
+ length_in_segment);
}
return rocksdb::Status::OK();
}
@@ -271,13 +277,7 @@ rocksdb::Status Bitmap::BitPos(const Slice &user_key, bool
bit, int64_t start, i
redis::BitmapString bitmap_string_db(storage_, namespace_);
return bitmap_string_db.BitPos(raw_value, bit, start, stop, stop_given,
pos);
}
-
- if (start < 0) start += static_cast<int64_t>(metadata.size) + 1;
- if (stop < 0) stop += static_cast<int64_t>(metadata.size) + 1;
- if (start < 0 || stop < 0 || start > stop) {
- *pos = -1;
- return rocksdb::Status::OK();
- }
+ std::tie(start, stop) = BitmapString::NormalizeRange(start, stop,
static_cast<int64_t>(metadata.size));
auto u_start = static_cast<uint32_t>(start);
auto u_stop = static_cast<uint32_t>(stop);
@@ -319,7 +319,12 @@ rocksdb::Status Bitmap::BitPos(const Slice &user_key, bool
bit, int64_t start, i
}
}
if (!bit && pin_value.size() < kBitmapSegmentBytes) {
- *pos = static_cast<int64_t>(i * kBitmapSegmentBits + pin_value.size() *
8);
+ // ExpandBitmapSegment might generate a segment with size less than
kBitmapSegmentBytes,
+ // but larger than logical size, so we need to align the last segment
with `metadata.size`
+ // rather than `pin_value.size()`.
+ auto last_segment_bytes = metadata.size % kBitmapSegmentBytes;
+ DCHECK_LE(last_segment_bytes, pin_value.size());
+ *pos = static_cast<int64_t>(i * kBitmapSegmentBits + last_segment_bytes
* 8);
return rocksdb::Status::OK();
}
pin_value.Reset();
diff --git a/src/types/redis_bitmap_string.cc b/src/types/redis_bitmap_string.cc
index 9b179630..228ce7e9 100644
--- a/src/types/redis_bitmap_string.cc
+++ b/src/types/redis_bitmap_string.cc
@@ -70,17 +70,13 @@ rocksdb::Status BitmapString::SetBit(const Slice &ns_key,
std::string *raw_value
rocksdb::Status BitmapString::BitCount(const std::string &raw_value, int64_t
start, int64_t stop, uint32_t *cnt) {
*cnt = 0;
- auto string_value =
raw_value.substr(Metadata::GetOffsetAfterExpire(raw_value[0]));
+ std::string_view string_value =
std::string_view{raw_value}.substr(Metadata::GetOffsetAfterExpire(raw_value[0]));
/* Convert negative indexes */
if (start < 0 && stop < 0 && start > stop) {
return rocksdb::Status::OK();
}
auto strlen = static_cast<int64_t>(string_value.size());
- if (start < 0) start = strlen + start;
- if (stop < 0) stop = strlen + stop;
- if (start < 0) start = 0;
- if (stop < 0) stop = 0;
- if (stop >= strlen) stop = strlen - 1;
+ std::tie(start, stop) = NormalizeRange(start, stop, strlen);
/* Precondition: end >= 0 && end < strlen, so the only condition where
* zero can be returned is: start > stop. */
@@ -95,12 +91,8 @@ rocksdb::Status BitmapString::BitPos(const std::string
&raw_value, bool bit, int
bool stop_given, int64_t *pos) {
auto string_value =
raw_value.substr(Metadata::GetOffsetAfterExpire(raw_value[0]));
auto strlen = static_cast<int64_t>(string_value.size());
- /* Convert negative indexes */
- if (start < 0) start = strlen + start;
- if (stop < 0) stop = strlen + stop;
- if (start < 0) start = 0;
- if (stop < 0) stop = 0;
- if (stop >= strlen) stop = strlen - 1;
+ /* Convert negative and out-of-bound indexes */
+ std::tie(start, stop) = NormalizeRange(start, stop, strlen);
if (start > stop) {
*pos = -1;
@@ -205,6 +197,15 @@ int64_t BitmapString::RawBitpos(const uint8_t *c, int64_t
count, bool bit) {
return res;
}
+std::pair<int64_t, int64_t> BitmapString::NormalizeRange(int64_t origin_start,
int64_t origin_end, int64_t length) {
+ if (origin_start < 0) origin_start = length + origin_start;
+ if (origin_end < 0) origin_end = length + origin_end;
+ if (origin_start < 0) origin_start = 0;
+ if (origin_end < 0) origin_end = 0;
+ if (origin_end >= length) origin_end = length - 1;
+ return {origin_start, origin_end};
+}
+
rocksdb::Status BitmapString::Bitfield(const Slice &ns_key, std::string
*raw_value,
const std::vector<BitfieldOperation>
&ops,
std::vector<std::optional<BitfieldValue>> *rets) {
diff --git a/src/types/redis_bitmap_string.h b/src/types/redis_bitmap_string.h
index ab61c211..637a476d 100644
--- a/src/types/redis_bitmap_string.h
+++ b/src/types/redis_bitmap_string.h
@@ -46,6 +46,16 @@ class BitmapString : public Database {
static size_t RawPopcount(const uint8_t *p, int64_t count);
static int64_t RawBitpos(const uint8_t *c, int64_t count, bool bit);
+
+ // NormalizeRange converts a range to a normalized range, which is a range
with start and stop in [0, length).
+ //
+ // If start/end is negative, it will be converted to positive by adding
length to it, and if the result is still
+ // negative, it will be converted to 0.
+ // If start/end is larger than length, it will be converted to length - 1.
+ //
+ // Return:
+ // The normalized [start, end] range.
+ static std::pair<int64_t, int64_t> NormalizeRange(int64_t origin_start,
int64_t origin_end, int64_t length);
};
} // namespace redis
diff --git a/tests/cppunit/types/bitmap_test.cc
b/tests/cppunit/types/bitmap_test.cc
index 4742f011..9bb97381 100644
--- a/tests/cppunit/types/bitmap_test.cc
+++ b/tests/cppunit/types/bitmap_test.cc
@@ -68,6 +68,51 @@ TEST_F(RedisBitmapTest, BitCount) {
auto s = bitmap_->Del(key_);
}
+TEST_F(RedisBitmapTest, BitCountNegative) {
+ {
+ bool bit = false;
+ bitmap_->SetBit(key_, 0, true, &bit);
+ EXPECT_FALSE(bit);
+ }
+ uint32_t cnt = 0;
+ bitmap_->BitCount(key_, 0, 4 * 1024, &cnt);
+ EXPECT_EQ(cnt, 1);
+ bitmap_->BitCount(key_, 0, 0, &cnt);
+ EXPECT_EQ(cnt, 1);
+ bitmap_->BitCount(key_, 0, -1, &cnt);
+ EXPECT_EQ(cnt, 1);
+ bitmap_->BitCount(key_, -1, -1, &cnt);
+ EXPECT_EQ(cnt, 1);
+ bitmap_->BitCount(key_, 1, 1, &cnt);
+ EXPECT_EQ(cnt, 0);
+ bitmap_->BitCount(key_, -10000, -10000, &cnt);
+ EXPECT_EQ(cnt, 1);
+
+ {
+ bool bit = false;
+ bitmap_->SetBit(key_, 5, true, &bit);
+ EXPECT_FALSE(bit);
+ }
+ bitmap_->BitCount(key_, -10000, -10000, &cnt);
+ EXPECT_EQ(cnt, 2);
+
+ {
+ bool bit = false;
+ bitmap_->SetBit(key_, 8 * 1024 - 1, true, &bit);
+ EXPECT_FALSE(bit);
+ bitmap_->SetBit(key_, 8 * 1024, true, &bit);
+ EXPECT_FALSE(bit);
+ }
+
+ bitmap_->BitCount(key_, 0, 1024, &cnt);
+ EXPECT_EQ(cnt, 4);
+
+ bitmap_->BitCount(key_, 0, 1023, &cnt);
+ EXPECT_EQ(cnt, 3);
+
+ auto s = bitmap_->Del(key_);
+}
+
TEST_F(RedisBitmapTest, BitPosClearBit) {
int64_t pos = 0;
bool old_bit = false;
@@ -95,6 +140,32 @@ TEST_F(RedisBitmapTest, BitPosSetBit) {
auto s = bitmap_->Del(key_);
}
+TEST_F(RedisBitmapTest, BitPosNegative) {
+ {
+ bool bit = false;
+ bitmap_->SetBit(key_, 8 * 1024 - 1, true, &bit);
+ EXPECT_FALSE(bit);
+ }
+ int64_t pos = 0;
+ // First bit is negative
+ bitmap_->BitPos(key_, false, 0, -1, true, &pos);
+ EXPECT_EQ(0, pos);
+ // 8 * 1024 - 1 bit is positive
+ bitmap_->BitPos(key_, true, 0, -1, true, &pos);
+ EXPECT_EQ(8 * 1024 - 1, pos);
+ // First bit in 1023 byte is negative
+ bitmap_->BitPos(key_, false, -1, -1, true, &pos);
+ EXPECT_EQ(8 * 1023, pos);
+ // Last Bit in 1023 byte is positive
+ bitmap_->BitPos(key_, true, -1, -1, true, &pos);
+ EXPECT_EQ(8 * 1024 - 1, pos);
+ // Large negative number will be normalized.
+ bitmap_->BitPos(key_, false, -10000, -10000, true, &pos);
+ EXPECT_EQ(0, pos);
+
+ auto s = bitmap_->Del(key_);
+}
+
TEST_F(RedisBitmapTest, BitfieldGetSetTest) {
constexpr uint32_t magic = 0xdeadbeef;
diff --git a/tests/gocase/unit/type/strings/strings_test.go
b/tests/gocase/unit/type/strings/strings_test.go
index fc799fc5..e3474b05 100644
--- a/tests/gocase/unit/type/strings/strings_test.go
+++ b/tests/gocase/unit/type/strings/strings_test.go
@@ -388,6 +388,12 @@ func TestString(t *testing.T) {
require.NoError(t, rdb.SetBit(ctx, "mykey", maxOffset, 1).Err())
require.EqualValues(t, 1, rdb.GetBit(ctx, "mykey",
maxOffset).Val())
require.EqualValues(t, 1, rdb.BitCount(ctx, "mykey",
&redis.BitCount{Start: 0, End: maxOffset / 8}).Val())
+ // Last byte should contain 1 bit.
+ require.EqualValues(t, 1, rdb.BitCount(ctx, "mykey",
&redis.BitCount{Start: -1, End: -1}).Val())
+ // 0 - Last byte should contain 1 bit.
+ require.EqualValues(t, 1, rdb.BitCount(ctx, "mykey",
&redis.BitCount{Start: -100, End: -1}).Val())
+ // The first byte shouldn't contain any bits
+ require.EqualValues(t, 0, rdb.BitCount(ctx, "mykey",
&redis.BitCount{Start: -100, End: -100}).Val())
require.EqualValues(t, maxOffset, rdb.BitPos(ctx, "mykey",
1).Val())
})