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