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 alloc::borrow::Cow;
13use core::num::NonZeroU64;
14use alloc::sync::Arc;
15
16use crate::value::{SmallBytes, Value};
17use crate::{Entry, Store, StoreError};
18
19impl Store {
20    /// `GETBIT key offset` — read the bit at `offset` (MSB-first
21    /// within each byte, matching Redis). Returns `0` for missing
22    /// key or offset past the end. Errors on wrong type.
23    pub fn getbit(&mut self, key: &[u8], offset: u64) -> Result<u8, StoreError> {
24        let bytes = match self.get(key)? {
25            Some(cow) => cow,
26            None => return Ok(0),
27        };
28        let byte_idx = (offset / 8) as usize;
29        let bit_idx = 7 - (offset % 8) as u8;
30        if byte_idx >= bytes.len() {
31            return Ok(0);
32        }
33        Ok((bytes[byte_idx] >> bit_idx) & 1)
34    }
35
36    /// `SETBIT key offset value` — set the bit at `offset` to `value`
37    /// (0 or 1). Extends the underlying string with zero-padding if
38    /// `offset / 8 >= current_len`. Returns the PREVIOUS bit value.
39    /// Errors on wrong type or `value > 1`.
40    pub fn setbit(
41        &mut self,
42        key: &[u8],
43        offset: u64,
44        value: u8,
45    ) -> Result<u8, StoreError> {
46        if value > 1 {
47            return Err(StoreError::OutOfRange);
48        }
49        let byte_idx = (offset / 8) as usize;
50        let bit_idx = 7 - (offset % 8) as u8;
51
52        // Read current bytes (Cow); compute previous bit; extend +
53        // write back. We collect into a fresh Vec each time — bitmaps
54        // tend to be hot-write so SmallBytes shrink-fit is moot.
55        let mut owned: Vec<u8> = match self.get(key)? {
56            Some(Cow::Borrowed(b)) => b.to_vec(),
57            Some(Cow::Owned(v)) => v,
58            None => Vec::new(),
59        };
60        if byte_idx >= owned.len() {
61            owned.resize(byte_idx + 1, 0);
62        }
63        let prev = (owned[byte_idx] >> bit_idx) & 1;
64        if value == 1 {
65            owned[byte_idx] |= 1 << bit_idx;
66        } else {
67            owned[byte_idx] &= !(1u8 << bit_idx);
68        }
69        // Store back. Always use the byte-array encoding (never int).
70        let new_val = if owned.is_empty() {
71            Value::Str(SmallBytes::from_slice(&[]))
72        } else {
73            Value::ArcBulk(Arc::new(owned.into_boxed_slice()))
74        };
75        // Take any existing TTL, re-attach to the new entry. Entry
76        // stores `expire_at_ns: Option<NonZeroU64>` (absolute ns).
77        let ttl_ns = self
78            .live_entry(key)
79            .and_then(|e| e.expire_at_ns.map(NonZeroU64::get));
80        self.insert_entry(
81            SmallBytes::from_slice(key),
82            Entry::new(new_val, ttl_ns),
83        );
84        Ok(prev)
85    }
86
87    /// `BITCOUNT key [start end [BYTE|BIT]]` — count set bits.
88    /// `start`/`end` are byte offsets (inclusive, negative-from-tail
89    /// like Redis). `None` for both = whole string.
90    pub fn bitcount(
91        &mut self,
92        key: &[u8],
93        range: Option<(i64, i64)>,
94    ) -> Result<u64, StoreError> {
95        let bytes = match self.get(key)? {
96            Some(cow) => cow,
97            None => return Ok(0),
98        };
99        if bytes.is_empty() {
100            return Ok(0);
101        }
102        let len = bytes.len() as i64;
103        let (s, e) = match range {
104            None => (0, (len - 1) as usize),
105            Some((start, end)) => {
106                let norm = |x: i64| -> i64 {
107                    if x < 0 { (len + x).max(0) } else { x.min(len - 1) }
108                };
109                let s = norm(start);
110                let e = norm(end);
111                if s > e {
112                    return Ok(0);
113                }
114                (s as usize, e as usize)
115            }
116        };
117        Ok(bytes[s..=e]
118            .iter()
119            .map(|b| u64::from(b.count_ones()))
120            .sum())
121    }
122
123    /// `BITPOS key bit [start [end]]` — return the position (bit
124    /// index, MSB-first) of the first bit equal to `bit` (0 or 1)
125    /// in the byte range `[start, end]` (inclusive, Redis-style
126    /// negative indexing). Returns `None` (Redis `-1`) when not
127    /// found. Errors with `OutOfRange` if `bit` > 1.
128    pub fn bitpos(
129        &mut self,
130        key: &[u8],
131        bit: u8,
132        range: Option<(i64, i64)>,
133    ) -> Result<Option<u64>, StoreError> {
134        if bit > 1 {
135            return Err(StoreError::OutOfRange);
136        }
137        let bytes = match self.get(key)? {
138            Some(cow) => cow,
139            None => return Ok(if bit == 0 { Some(0) } else { None }),
140        };
141        if bytes.is_empty() {
142            return Ok(if bit == 0 { Some(0) } else { None });
143        }
144        let len = bytes.len() as i64;
145        let (s, e) = match range {
146            None => (0usize, (len - 1) as usize),
147            Some((start, end)) => {
148                let norm = |x: i64| -> i64 {
149                    if x < 0 { (len + x).max(0) } else { x.min(len - 1) }
150                };
151                let s = norm(start);
152                let e = norm(end);
153                if s > e {
154                    return Ok(None);
155                }
156                (s as usize, e as usize)
157            }
158        };
159        for (i, &b) in bytes[s..=e].iter().enumerate() {
160            let target_mask = if bit == 1 { b } else { !b };
161            if target_mask != 0 {
162                let bit_in_byte = target_mask.leading_zeros() as u64;
163                let byte_idx = (s + i) as u64;
164                return Ok(Some(byte_idx * 8 + bit_in_byte));
165            }
166        }
167        Ok(None)
168    }
169
170    /// `GETRANGE key start end` — substring with Redis-style
171    /// negative indexing; `[start, end]` inclusive. Returns empty
172    /// `Vec` when key absent or range out of bounds.
173    pub fn getrange(
174        &mut self,
175        key: &[u8],
176        start: i64,
177        end: i64,
178    ) -> Result<Vec<u8>, StoreError> {
179        let bytes = match self.get(key)? {
180            Some(cow) => cow,
181            None => return Ok(Vec::new()),
182        };
183        if bytes.is_empty() {
184            return Ok(Vec::new());
185        }
186        let len = bytes.len() as i64;
187        let norm = |x: i64| -> i64 {
188            if x < 0 { (len + x).max(0) } else { x.min(len - 1) }
189        };
190        let s = norm(start) as usize;
191        let e = norm(end) as usize;
192        if s > e {
193            return Ok(Vec::new());
194        }
195        Ok(bytes[s..=e].to_vec())
196    }
197
198    /// `SETRANGE key offset value` — overwrite bytes at `offset`
199    /// with `value`. Extends the string with zero padding if
200    /// `offset > len`. Returns the new total length. Preserves
201    /// any existing TTL.
202    pub fn setrange(
203        &mut self,
204        key: &[u8],
205        offset: u64,
206        value: &[u8],
207    ) -> Result<usize, StoreError> {
208        let offset = offset as usize;
209        let mut owned: Vec<u8> = match self.get(key)? {
210            Some(Cow::Borrowed(b)) => b.to_vec(),
211            Some(Cow::Owned(v)) => v,
212            None => Vec::new(),
213        };
214        let needed = offset + value.len();
215        if needed > owned.len() {
216            owned.resize(needed, 0);
217        }
218        owned[offset..offset + value.len()].copy_from_slice(value);
219        let new_len = owned.len();
220        let new_val = if owned.is_empty() {
221            Value::Str(SmallBytes::from_slice(&[]))
222        } else {
223            Value::ArcBulk(Arc::new(owned.into_boxed_slice()))
224        };
225        let ttl_ns = self
226            .live_entry(key)
227            .and_then(|e| e.expire_at_ns.map(NonZeroU64::get));
228        self.insert_entry(
229            SmallBytes::from_slice(key),
230            Entry::new(new_val, ttl_ns),
231        );
232        Ok(new_len)
233    }
234}