Skip to main content

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}