Skip to main content

kevy_embedded/
ops_bitmap.rs

1//! Bitmap reads, writes, and aggregates: `GETBIT` / `SETBIT` /
2//! `BITCOUNT` / `BITPOS` / `BITOP` / `GETRANGE` / `SETRANGE`.
3//!
4//! Strings act as bit arrays addressed MSB-first within each byte,
5//! matching Redis semantics.
6
7use crate::{KevyError, KevyResult};
8
9use crate::store::ensure_writable;
10use crate::store::{Store, commit_write, store_err};
11
12impl Store {
13    /// `GETBIT key offset` — return the bit at `offset` (MSB-first).
14    /// `0` for missing key or past-end.
15    pub fn getbit(&self, key: &[u8], offset: u64) -> KevyResult<u8> {
16        self.wshard(key).store.getbit(key, offset).map_err(store_err)
17    }
18
19    /// `SETBIT key offset value` — set the bit at `offset` to
20    /// `value` (0 or 1). Extends the underlying string with zero
21    /// padding as needed. Returns the PREVIOUS bit value.
22    pub fn setbit(&self, key: &[u8], offset: u64, value: u8) -> KevyResult<u8> {
23        ensure_writable(self)?;
24        let mut g = self.wshard(key);
25        let prev = g.store.setbit(key, offset, value).map_err(store_err)?;
26        let off_str = format!("{offset}");
27        let val_str = format!("{value}");
28        commit_write(&mut g, &[b"SETBIT", key, off_str.as_bytes(), val_str.as_bytes()])?;
29        Ok(prev)
30    }
31
32    /// `BITCOUNT key [start end]` — count set bits over the
33    /// optional byte-offset range (inclusive, negatives-from-tail
34    /// like Redis). `None` for `range` = whole string.
35    pub fn bitcount(&self, key: &[u8], range: Option<(i64, i64)>) -> KevyResult<u64> {
36        self.wshard(key).store.bitcount(key, range).map_err(store_err)
37    }
38
39    /// `BITPOS key bit [start [end]]` — find first bit equal to
40    /// `bit` (0 or 1) in the optional byte range. Returns `None`
41    /// when not found (Redis would reply `:-1`).
42    pub fn bitpos(
43        &self,
44        key: &[u8],
45        bit: u8,
46        range: Option<(i64, i64)>,
47    ) -> KevyResult<Option<u64>> {
48        self.wshard(key)
49            .store
50            .bitpos(key, bit, range)
51            .map_err(store_err)
52    }
53
54    /// `GETRANGE key start end` — substring with Redis negative
55    /// indexing; `[start, end]` inclusive.
56    pub fn getrange(&self, key: &[u8], start: i64, end: i64) -> KevyResult<Vec<u8>> {
57        self.wshard(key)
58            .store
59            .getrange(key, start, end)
60            .map_err(store_err)
61    }
62
63    /// `SETRANGE key offset value` — overwrite bytes at `offset`;
64    /// extends with zero padding if past current length. Returns
65    /// the new total length.
66    pub fn setrange(
67        &self,
68        key: &[u8],
69        offset: u64,
70        value: &[u8],
71    ) -> KevyResult<usize> {
72        ensure_writable(self)?;
73        let mut g = self.wshard(key);
74        let new_len = g
75            .store
76            .setrange(key, offset, value)
77            .map_err(store_err)?;
78        let off_str = format!("{offset}");
79        commit_write(&mut g, &[b"SETRANGE", key, off_str.as_bytes(), value])?;
80        Ok(new_len)
81    }
82
83    /// `BITOP AND|OR|XOR|NOT destkey srckey [srckey ...]` — bitwise
84    /// op across N source keys, stored at `destkey`. Returns the
85    /// destination string length (= longest source length, with
86    /// shorter sources zero-padded). For `Not`, exactly one source
87    /// key (additional ones are rejected).
88    pub fn bitop(
89        &self,
90        op: BitOp,
91        dst: &[u8],
92        srcs: &[&[u8]],
93    ) -> KevyResult<usize> {
94        ensure_writable(self)?;
95        if srcs.is_empty() {
96            return Ok(0);
97        }
98        if matches!(op, BitOp::Not) && srcs.len() != 1 {
99            return Err(KevyError::InvalidInput("BITOP NOT takes exactly one source key".into()));
100        }
101        // Read each source (own each as Vec<u8>) — set-algebra style.
102        let mut srcs_bytes: Vec<Vec<u8>> = Vec::with_capacity(srcs.len());
103        for k in srcs {
104            let v = self.get(k)?.unwrap_or_default();
105            srcs_bytes.push(v);
106        }
107        let max_len = srcs_bytes.iter().map(Vec::len).max().unwrap_or(0);
108        if max_len == 0 {
109            // Empty result — delete dst.
110            self.del(&[dst])?;
111            return Ok(0);
112        }
113        let out = bitop_combine(op, &srcs_bytes, max_len);
114        // Write dst.
115        self.set(dst, &out)?;
116        Ok(max_len)
117    }
118
119    /// `TIME` — `(unix_seconds, microseconds)` tuple. Useful for
120    /// time-based embedded logic + tracing.
121    pub fn time(&self) -> (u64, u32) {
122        let now = std::time::SystemTime::now()
123            .duration_since(std::time::UNIX_EPOCH)
124            .unwrap_or_default();
125        (now.as_secs(), now.subsec_micros())
126    }
127}
128
129/// Combine the source strings under `op` into the `max_len`-byte
130/// destination value (shorter sources zero-padded).
131fn bitop_combine(op: BitOp, srcs_bytes: &[Vec<u8>], max_len: usize) -> Vec<u8> {
132    let mut out = vec![0u8; max_len];
133    match op {
134        BitOp::Not => {
135            let s = &srcs_bytes[0];
136            for (i, b) in s.iter().enumerate() {
137                out[i] = !b;
138            }
139            // bytes past s.len() stay 0 — Redis sets them to 0xff
140            // (NOT of implicit zero). Match Redis:
141            for byte in out.iter_mut().skip(s.len()) {
142                *byte = 0xff;
143            }
144        }
145        _ => {
146            let init = match op {
147                BitOp::And => 0xff,
148                BitOp::Or | BitOp::Xor => 0x00,
149                BitOp::Not => unreachable!(),
150            };
151            for byte in out.iter_mut() {
152                *byte = init;
153            }
154            for s in srcs_bytes {
155                for (i, b) in out.iter_mut().enumerate() {
156                    let sb = s.get(i).copied().unwrap_or(0);
157                    *b = match op {
158                        BitOp::And => *b & sb,
159                        BitOp::Or => *b | sb,
160                        BitOp::Xor => *b ^ sb,
161                        BitOp::Not => unreachable!(),
162                    };
163                }
164            }
165        }
166    }
167    out
168}
169
170/// Operator for [`Store::bitop`].
171#[derive(Clone, Copy, Debug, PartialEq, Eq)]
172pub enum BitOp {
173    /// Bitwise AND across source keys.
174    And,
175    /// Bitwise OR across source keys.
176    Or,
177    /// Bitwise XOR across source keys.
178    Xor,
179    /// Bitwise NOT — exactly one source key.
180    Not,
181}