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/// Operator for the BITOP family.
217///
218/// ```
219/// use kevy_store::BitOp;
220/// // NOT takes exactly one source; the callers enforce that, and this
221/// // is what it computes.
222/// assert_eq!(BitOp::Not.combine(&[vec![0b1010_1010]], 1), vec![0b0101_0101]);
223/// ```
224#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
225#[non_exhaustive]
226pub enum BitOp {
227 /// Bitwise AND across source keys.
228 ///
229 /// ```
230 /// use kevy_store::BitOp;
231 /// assert_eq!(BitOp::And.combine(&[vec![0b1100], vec![0b1010]], 1), vec![0b1000]);
232 /// ```
233 And,
234 /// Bitwise OR across source keys.
235 ///
236 /// ```
237 /// use kevy_store::BitOp;
238 /// assert_eq!(BitOp::Or.combine(&[vec![0b1100], vec![0b1010]], 1), vec![0b1110]);
239 /// ```
240 Or,
241 /// Bitwise XOR across source keys.
242 ///
243 /// ```
244 /// use kevy_store::BitOp;
245 /// assert_eq!(BitOp::Xor.combine(&[vec![0b1100], vec![0b1010]], 1), vec![0b0110]);
246 /// ```
247 Xor,
248 /// Bitwise NOT — exactly one source key.
249 ///
250 /// ```
251 /// use kevy_store::BitOp;
252 /// assert_eq!(BitOp::Not.combine(&[vec![0x0f]], 1), vec![0xf0]);
253 /// ```
254 Not,
255}
256
257impl BitOp {
258 /// Combine the source strings under this operator into the `max_len`-byte
259 /// destination value (shorter sources zero-padded).
260 ///
261 /// Two rules are easy to get wrong and both are here. A source shorter
262 /// than the result reads as zero past its end — so an AND with a short
263 /// source clears the tail, and an OR leaves it alone. And NOT does not
264 /// stop at its source: Redis inverts the implicit zeros too, so every
265 /// byte past the source is `0xff`.
266 ///
267 /// ```
268 /// use kevy_store::BitOp;
269 ///
270 /// let long = b"\xff\xff".to_vec();
271 /// let short = b"\x0f".to_vec();
272 /// // AND: the second byte meets an implicit zero.
273 /// assert_eq!(BitOp::And.combine(&[long.clone(), short.clone()], 2), vec![0x0f, 0x00]);
274 /// // OR: the implicit zero changes nothing.
275 /// assert_eq!(BitOp::Or.combine(&[long.clone(), short], 2), vec![0xff, 0xff]);
276 /// // NOT over a two-byte result from a one-byte source: the tail is 0xff.
277 /// assert_eq!(BitOp::Not.combine(&[vec![0x00]], 2), vec![0xff, 0xff]);
278 /// ```
279 #[must_use]
280 pub fn combine(self, srcs_bytes: &[Vec<u8>], max_len: usize) -> Vec<u8> {
281 let mut out = vec![0u8; max_len];
282 match self {
283 BitOp::Not => {
284 let s = &srcs_bytes[0];
285 for (i, b) in s.iter().enumerate() {
286 out[i] = !b;
287 }
288 // bytes past s.len() stay 0 — Redis sets them to 0xff
289 // (NOT of implicit zero). Match Redis:
290 for byte in out.iter_mut().skip(s.len()) {
291 *byte = 0xff;
292 }
293 }
294 // AND, OR, XOR. NOT returned above, so the catch-alls below are
295 // XOR — written as `_` rather than `Not => unreachable!()`,
296 // which was four arms that can never run and four regions that
297 // can never be covered.
298 _ => {
299 let init = if self == BitOp::And { 0xff } else { 0x00 };
300 for byte in out.iter_mut() {
301 *byte = init;
302 }
303 for s in srcs_bytes {
304 for (i, b) in out.iter_mut().enumerate() {
305 let sb = s.get(i).copied().unwrap_or(0);
306 *b = match self {
307 BitOp::And => *b & sb,
308 BitOp::Or => *b | sb,
309 _ => *b ^ sb,
310 };
311 }
312 }
313 }
314 }
315 out
316 }
317}