kevy_store/bitmap.rs
1//! Bitmap ops on string-typed values — `SETBIT` / `GETBIT` /
2//! `BITCOUNT`. Redis treats strings as byte arrays addressed at the
3//! bit level; this module exposes those reads / writes against the
4//! existing string value encodings (`Value::Str` / `Value::ArcBulk` /
5//! `Value::Int`).
6//!
7//! Split out from `string.rs` to keep that file under the 500-LOC
8//! house rule.
9
10#[cfg(not(feature = "std"))]
11use crate::nostd_prelude::*;
12use crate::util::range_bounds;
13use alloc::borrow::Cow;
14use alloc::sync::Arc;
15use alloc::vec;
16use core::num::NonZeroU64;
17
18use crate::value::{SmallBytes, Value};
19use crate::{Entry, Store, StoreError};
20
21impl Store {
22 /// `GETBIT key offset` — read the bit at `offset` (MSB-first
23 /// within each byte, matching Redis). Returns `0` for missing
24 /// key or offset past the end. Errors on wrong type.
25 pub fn getbit(&mut self, key: &[u8], offset: u64) -> Result<u8, StoreError> {
26 let bytes = match self.get(key)? {
27 Some(cow) => cow,
28 None => return Ok(0),
29 };
30 let byte_idx = (offset / 8) as usize;
31 let bit_idx = 7 - (offset % 8) as u8;
32 if byte_idx >= bytes.len() {
33 return Ok(0);
34 }
35 Ok((bytes[byte_idx] >> bit_idx) & 1)
36 }
37
38 /// `SETBIT key offset value` — set the bit at `offset` to `value`
39 /// (0 or 1). Extends the underlying string with zero-padding if
40 /// `offset / 8 >= current_len`. Returns the PREVIOUS bit value.
41 /// Errors on wrong type or `value > 1`.
42 pub fn setbit(&mut self, key: &[u8], offset: u64, value: u8) -> Result<u8, StoreError> {
43 if value > 1 {
44 return Err(StoreError::OutOfRange);
45 }
46 let byte_idx = (offset / 8) as usize;
47 let bit_idx = 7 - (offset % 8) as u8;
48
49 // Read current bytes (Cow); compute previous bit; extend +
50 // write back. We collect into a fresh Vec each time — bitmaps
51 // tend to be hot-write so SmallBytes shrink-fit is moot.
52 let mut owned: Vec<u8> = match self.get(key)? {
53 Some(Cow::Borrowed(b)) => b.to_vec(),
54 Some(Cow::Owned(v)) => v,
55 None => Vec::new(),
56 };
57 if byte_idx >= owned.len() {
58 owned.resize(byte_idx + 1, 0);
59 }
60 let prev = (owned[byte_idx] >> bit_idx) & 1;
61 if value == 1 {
62 owned[byte_idx] |= 1 << bit_idx;
63 } else {
64 owned[byte_idx] &= !(1u8 << bit_idx);
65 }
66 // Store back. Always use the byte-array encoding (never int).
67 let new_val = if owned.is_empty() {
68 Value::Str(SmallBytes::from_slice(&[]))
69 } else {
70 Value::ArcBulk(Arc::new(owned.into_boxed_slice()))
71 };
72 // Take any existing TTL, re-attach to the new entry. Entry
73 // stores `expire_at_ns: Option<NonZeroU64>` (absolute ns).
74 let ttl_ns = self.live_entry(key).and_then(|e| e.expire_at_ns.map(NonZeroU64::get));
75 self.insert_entry(SmallBytes::from_slice(key), Entry::new(new_val, ttl_ns));
76 Ok(prev)
77 }
78
79 /// `BITCOUNT key [start end [BYTE|BIT]]` — count set bits.
80 /// `start`/`end` are byte offsets (inclusive, negative-from-tail
81 /// like Redis). `None` for both = whole string.
82 pub fn bitcount(&mut self, key: &[u8], range: Option<(i64, i64)>) -> Result<u64, StoreError> {
83 let bytes = match self.get(key)? {
84 Some(cow) => cow,
85 None => return Ok(0),
86 };
87 if bytes.is_empty() {
88 return Ok(0);
89 }
90 let len = bytes.len() as i64;
91 let (s, e) = match range {
92 None => (0, (len - 1) as usize),
93 Some((start, end)) => {
94 let norm =
95 |x: i64| -> i64 { if x < 0 { (len + x).max(0) } else { x.min(len - 1) } };
96 let s = norm(start);
97 let e = norm(end);
98 if s > e {
99 return Ok(0);
100 }
101 (s as usize, e as usize)
102 }
103 };
104 Ok(bytes[s..=e].iter().map(|b| u64::from(b.count_ones())).sum())
105 }
106
107 /// `BITPOS key bit [start [end]]` — return the position (bit
108 /// index, MSB-first) of the first bit equal to `bit` (0 or 1)
109 /// in the byte range `[start, end]` (inclusive, Redis-style
110 /// negative indexing). Returns `None` (Redis `-1`) when not
111 /// found. Errors with `OutOfRange` if `bit` > 1.
112 pub fn bitpos(
113 &mut self,
114 key: &[u8],
115 bit: u8,
116 range: Option<(i64, i64)>,
117 ) -> Result<Option<u64>, StoreError> {
118 if bit > 1 {
119 return Err(StoreError::OutOfRange);
120 }
121 let bytes = match self.get(key)? {
122 Some(cow) => cow,
123 None => return Ok(if bit == 0 { Some(0) } else { None }),
124 };
125 if bytes.is_empty() {
126 return Ok(if bit == 0 { Some(0) } else { None });
127 }
128 let len = bytes.len() as i64;
129 let (s, e) = match range {
130 None => (0usize, (len - 1) as usize),
131 Some((start, end)) => {
132 let norm =
133 |x: i64| -> i64 { if x < 0 { (len + x).max(0) } else { x.min(len - 1) } };
134 let s = norm(start);
135 let e = norm(end);
136 if s > e {
137 return Ok(None);
138 }
139 (s as usize, e as usize)
140 }
141 };
142 for (i, &b) in bytes[s..=e].iter().enumerate() {
143 let target_mask = if bit == 1 { b } else { !b };
144 if target_mask != 0 {
145 let bit_in_byte = target_mask.leading_zeros() as u64;
146 let byte_idx = (s + i) as u64;
147 return Ok(Some(byte_idx * 8 + bit_in_byte));
148 }
149 }
150 Ok(None)
151 }
152
153 /// `GETRANGE key start end` — substring with Redis-style
154 /// negative indexing; `[start, end]` inclusive. Returns empty
155 /// `Vec` when key absent or range out of bounds.
156 pub fn getrange(&mut self, key: &[u8], start: i64, end: i64) -> Result<Vec<u8>, StoreError> {
157 let bytes = match self.get(key)? {
158 Some(cow) => cow,
159 None => return Ok(Vec::new()),
160 };
161 if bytes.is_empty() {
162 return Ok(Vec::new());
163 }
164 // `range_bounds`, not a clamp of its own. This function had
165 // one, and it capped START at len-1 as well as END — so
166 // `GETRANGE k 99 200` on a 24-byte value answered the last byte
167 // where Redis answers nothing. Redis floors a negative start at
168 // zero and caps only the end; a start past the last index makes
169 // the range empty. The three-way differential against a real
170 // valkey is what found it, after the wire-vs-facade one had
171 // agreed — both surfaces shared the mistake, so comparing them
172 // proved nothing about Redis.
173 Ok(match range_bounds(start, end, bytes.len()) {
174 None => Vec::new(),
175 Some((s, e)) => bytes[s..=e].to_vec(),
176 })
177 }
178
179 /// `SETRANGE key offset value` — overwrite bytes at `offset`
180 /// with `value`. Extends the string with zero padding if
181 /// `offset > len`. Returns the new total length. Preserves
182 /// any existing TTL.
183 pub fn setrange(&mut self, key: &[u8], offset: u64, value: &[u8]) -> Result<usize, StoreError> {
184 let offset = offset as usize;
185 let mut owned: Vec<u8> = match self.get(key)? {
186 Some(Cow::Borrowed(b)) => b.to_vec(),
187 Some(Cow::Owned(v)) => v,
188 None => Vec::new(),
189 };
190 let needed = offset + value.len();
191 if needed > owned.len() {
192 owned.resize(needed, 0);
193 }
194 owned[offset..offset + value.len()].copy_from_slice(value);
195 let new_len = owned.len();
196 let new_val = if owned.is_empty() {
197 Value::Str(SmallBytes::from_slice(&[]))
198 } else {
199 Value::ArcBulk(Arc::new(owned.into_boxed_slice()))
200 };
201 let ttl_ns = self.live_entry(key).and_then(|e| e.expire_at_ns.map(NonZeroU64::get));
202 self.insert_entry(SmallBytes::from_slice(key), Entry::new(new_val, ttl_ns));
203 Ok(new_len)
204 }
205}
206
207// ── BITOP: the operator, and the byte arithmetic ───────────────────
208//
209// Both live here rather than in a facade because neither knows what a
210// key is. `kevy-embedded` computed them for its own BITOP and
211// `kevy-rt` could not reach that code at all — sibling crates — so
212// wiring BITOP to the server wire would have meant a second copy of
213// the padding rules, the 0xff tail of NOT among them. Two
214// implementations of one operator are how two surfaces drift.
215
216/// Combine the source strings under `op` into the `max_len`-byte
217/// destination value (shorter sources zero-padded).
218///
219/// Two rules are easy to get wrong and both are here. A source shorter
220/// than the result reads as zero past its end — so an AND with a short
221/// source clears the tail, and an OR leaves it alone. And NOT does not
222/// stop at its source: Redis inverts the implicit zeros too, so every
223/// byte past the source is `0xff`.
224///
225/// ```
226/// use kevy_store::{BitOp, bitop_combine};
227///
228/// let long = b"\xff\xff".to_vec();
229/// let short = b"\x0f".to_vec();
230/// // AND: the second byte meets an implicit zero.
231/// assert_eq!(bitop_combine(BitOp::And, &[long.clone(), short.clone()], 2), vec![0x0f, 0x00]);
232/// // OR: the implicit zero changes nothing.
233/// assert_eq!(bitop_combine(BitOp::Or, &[long.clone(), short], 2), vec![0xff, 0xff]);
234/// // NOT over a two-byte result from a one-byte source: the tail is 0xff.
235/// assert_eq!(bitop_combine(BitOp::Not, &[vec![0x00]], 2), vec![0xff, 0xff]);
236/// ```
237pub fn bitop_combine(op: BitOp, srcs_bytes: &[Vec<u8>], max_len: usize) -> Vec<u8> {
238 let mut out = vec![0u8; max_len];
239 match op {
240 BitOp::Not => {
241 let s = &srcs_bytes[0];
242 for (i, b) in s.iter().enumerate() {
243 out[i] = !b;
244 }
245 // bytes past s.len() stay 0 — Redis sets them to 0xff
246 // (NOT of implicit zero). Match Redis:
247 for byte in out.iter_mut().skip(s.len()) {
248 *byte = 0xff;
249 }
250 }
251 // AND, OR, XOR. NOT returned above, so the catch-alls below are
252 // XOR — written as `_` rather than `Not => unreachable!()`,
253 // which was four arms that can never run and four regions that
254 // can never be covered.
255 _ => {
256 let init = if op == BitOp::And { 0xff } else { 0x00 };
257 for byte in out.iter_mut() {
258 *byte = init;
259 }
260 for s in srcs_bytes {
261 for (i, b) in out.iter_mut().enumerate() {
262 let sb = s.get(i).copied().unwrap_or(0);
263 *b = match op {
264 BitOp::And => *b & sb,
265 BitOp::Or => *b | sb,
266 _ => *b ^ sb,
267 };
268 }
269 }
270 }
271 }
272 out
273}
274
275/// Operator for the BITOP family.
276///
277/// ```
278/// use kevy_store::{BitOp, bitop_combine};
279/// // NOT takes exactly one source; the callers enforce that, and this
280/// // is what it computes.
281/// assert_eq!(bitop_combine(BitOp::Not, &[vec![0b1010_1010]], 1), vec![0b0101_0101]);
282/// ```
283#[derive(Clone, Copy, Debug, PartialEq, Eq)]
284pub enum BitOp {
285 /// Bitwise AND across source keys.
286 And,
287 /// Bitwise OR across source keys.
288 Or,
289 /// Bitwise XOR across source keys.
290 Xor,
291 /// Bitwise NOT — exactly one source key.
292 Not,
293}