kevy-store 4.0.0

kevy keyspace + value types + expiry — pure Rust, zero deps.
Documentation
//! Small pure helpers shared across the store modules.

#[cfg(not(feature = "std"))]
use crate::nostd_prelude::*;
pub(crate) fn norm_index(idx: i64, len: usize) -> Option<usize> {
    let len = len as i64;
    let i = if idx < 0 { idx + len } else { idx };
    if i < 0 || i >= len {
        None
    } else {
        Some(i as usize)
    }
}

/// Clamp a possibly-negative `[start, stop]` range to valid bounds (inclusive),
/// or `None` if the range is empty.
pub(crate) fn range_bounds(start: i64, stop: i64, len: usize) -> Option<(usize, usize)> {
    if len == 0 {
        return None;
    }
    let len = len as i64;
    let s = (if start < 0 { start + len } else { start }).max(0);
    let e = (if stop < 0 { stop + len } else { stop }).min(len - 1);
    if s > e || s >= len {
        None
    } else {
        Some((s as usize, e as usize))
    }
}

/// Strict base-10 `i64` parse over raw bytes (allows a leading `+`/`-`).
pub(crate) fn parse_i64(b: &[u8]) -> Option<i64> {
    core::str::from_utf8(b).ok()?.parse::<i64>().ok()
}

/// L2: try to parse `b` as a CANONICAL `i64` ASCII representation — the same
/// bytes that `i64::to_string` would produce. Returns the i64 only when the
/// formatted i64 round-trips byte-for-byte to `b`. Rejects leading `+`,
/// leading zeros (except `"0"`), and any non-canonical form so SET → GET
/// stays a perfect echo. Used to decide whether to store a string value as
/// `Value::Int(n)` (lessons from valkey OBJ_ENCODING_INT).
pub(crate) fn parse_canonical_i64(b: &[u8]) -> Option<i64> {
    // Reject extremely long inputs cheaply (i64 max is 20 chars incl sign).
    if b.is_empty() || b.len() > 20 {
        return None;
    }
    // Single-byte guard — canonical i64 starts with '-' or an
    // ASCII digit. `redis-benchmark` defaults to 3-byte random alphanumeric
    // values ("xxx"), which paid the full UTF-8 → parse → itoa round-trip
    // only to reject at the round-trip compare. Reject up-front in ~1 ns.
    let first = b[0];
    if !first.is_ascii_digit() && first != b'-' {
        return None;
    }
    let n = core::str::from_utf8(b).ok()?.parse::<i64>().ok()?;
    let mut buf = itoa_i64_stack();
    let s = format_i64_into(n, &mut buf);
    if s == b { Some(n) } else { None }
}

/// L2: format `n` as ASCII bytes into `buf` and return the written slice.
/// Uses `i64::MIN` (20 chars including sign) as the worst-case length.
pub(crate) fn format_i64_into(n: i64, buf: &mut [u8; 20]) -> &[u8] {
    // Standard digit-by-digit unroll: faster + no alloc vs `n.to_string()`.
    // For negatives, format the absolute value as u64 (handles i64::MIN
    // without overflow) then prepend '-'.
    let (mut n_abs, neg) = if n < 0 {
        ((n as i128).unsigned_abs() as u64, true)
    } else {
        (n as u64, false)
    };
    let mut i = buf.len();
    if n_abs == 0 {
        i -= 1;
        buf[i] = b'0';
    } else {
        while n_abs > 0 {
            i -= 1;
            buf[i] = b'0' + (n_abs % 10) as u8;
            n_abs /= 10;
        }
    }
    if neg {
        i -= 1;
        buf[i] = b'-';
    }
    &buf[i..]
}

/// L2: stack scratch for [`format_i64_into`]. 20 bytes = i64::MIN's digit
/// count incl sign.
#[inline]
pub(crate) fn itoa_i64_stack() -> [u8; 20] {
    [0u8; 20]
}

/// Emit `$<len>\r\n` into `out` for a RESP bulk header. Inlined
/// at the GET fast path's `get_into_output` callsite to skip the GetReply
/// enum tag round-trip + caller match arm. Mirror of kevy-rt's local helper.
#[inline]
pub(crate) fn bulk_header_into(out: &mut Vec<u8>, len: usize) {
    out.push(b'$');
    // usize fits in 20 ASCII digits (u64::MAX is 20 digits).
    let mut buf = [0u8; 20];
    let mut n = len;
    let mut i = buf.len();
    if n == 0 {
        i -= 1;
        buf[i] = b'0';
    } else {
        while n > 0 {
            i -= 1;
            buf[i] = b'0' + (n % 10) as u8;
            n /= 10;
        }
    }
    out.extend_from_slice(&buf[i..]);
    out.extend_from_slice(b"\r\n");
}

/// Parse a finite f64 from raw bytes (rejects NaN/inf for value storage).
pub(crate) fn parse_f64(b: &[u8]) -> Option<f64> {
    let f: f64 = core::str::from_utf8(b).ok()?.trim().parse().ok()?;
    f.is_finite().then_some(f)
}

/// Redis-style glob match (`*`, `?`, `[...]` classes with ranges/`^`, `\` escape).
pub fn glob_match(pat: &[u8], s: &[u8]) -> bool {
    glob(pat, s)
}

/// Iterative two-pointer glob with a single backtrack anchor for `*`.
/// O(n·m) worst case — the earlier recursive `(0..=s.len()).any(glob(tail,
/// …))` form was exponential for patterns with several literal-separated
/// `*`s (e.g. `*a*a*a…*!`), a remote ReDoS on any `MATCH`-taking verb
/// (`SCAN`/`KEYS`/`PSUBSCRIBE`/`CONFIG GET`). This algorithm records the
/// position just past the last `*` plus the string index to resume from,
/// and on a mismatch lets that `*` swallow one more character.
fn glob(p: &[u8], s: &[u8]) -> bool {
    let (mut pi, mut si) = (0usize, 0usize);
    let mut star_pi: Option<usize> = None;
    let mut star_si = 0usize;
    while si < s.len() {
        if pi < p.len() && p[pi] == b'*' {
            // Collapse a run of `*`, anchor here, consume no string yet.
            while pi < p.len() && p[pi] == b'*' {
                pi += 1;
            }
            star_pi = Some(pi);
            star_si = si;
            continue;
        }
        if pi < p.len()
            && let Some(width) = match_token(&p[pi..], s[si])
        {
            pi += width;
            si += 1;
            continue;
        }
        // Mismatch: backtrack to the last `*`, letting it eat one more
        // char; with no `*` seen, the strings can't match.
        let Some(anchor) = star_pi else {
            return false;
        };
        pi = anchor;
        star_si += 1;
        si = star_si;
    }
    // The remaining pattern must be all `*` to match the empty suffix.
    while pi < p.len() && p[pi] == b'*' {
        pi += 1;
    }
    pi == p.len()
}

/// Match one non-`*` pattern token at `p[0..]` against `ch`; returns the
/// token's byte width on a match. Mirrors the per-arm semantics of the
/// old recursive matcher (`?` any / `[...]` class / `\x` escaped literal /
/// plain literal).
fn match_token(p: &[u8], ch: u8) -> Option<usize> {
    match p[0] {
        b'?' => Some(1),
        b'[' => {
            let (matched, rest) = match_class(&p[1..], ch);
            matched.then(|| p.len() - rest.len())
        }
        b'\\' if p.len() >= 2 => (p[1] == ch).then_some(2),
        c => (c == ch).then_some(1),
    }
}

/// Match one char against a `[...]` class; return `(matched, pattern_after_class)`.
fn match_class(p: &[u8], ch: u8) -> (bool, &[u8]) {
    let mut i = 0;
    let negate = p.first() == Some(&b'^');
    if negate {
        i += 1;
    }
    let mut matched = false;
    while i < p.len() && p[i] != b']' {
        if p[i] == b'\\' && i + 1 < p.len() {
            matched |= p[i + 1] == ch;
            i += 2;
        } else if i + 2 < p.len() && p[i + 1] == b'-' && p[i + 2] != b']' {
            let (lo, hi) = if p[i] <= p[i + 2] {
                (p[i], p[i + 2])
            } else {
                (p[i + 2], p[i])
            };
            matched |= (lo..=hi).contains(&ch);
            i += 3;
        } else {
            matched |= p[i] == ch;
            i += 1;
        }
    }
    if i < p.len() {
        i += 1; // skip ']'
    }
    (matched ^ negate, &p[i..])
}

/// Format a number the way Redis does: integral values without a decimal point.
pub(crate) fn fmt_num(v: f64) -> Vec<u8> {
    // Bit-exact compare is the contract: "the f64 carries no fractional bits".
    // An epsilon would mis-classify 1.0 + 1e-18 as integer-valued.
    #[cfg(feature = "std")]
    #[allow(clippy::float_cmp)]
    let is_integer_valued = v == v.trunc();
    // core has no float trunc; the i64 round-trip is exact for every |v|
    // < 1e17 the integer arm accepts, and values beyond that range (where
    // the round-trip saturates) route to the same `format!` arm anyway.
    #[cfg(not(feature = "std"))]
    #[allow(clippy::float_cmp)]
    let is_integer_valued = v == ((v as i64) as f64);
    if is_integer_valued && v.abs() < 1e17 {
        (v as i64).to_string().into_bytes()
    } else {
        format!("{v}").into_bytes()
    }
}

/// Apply a signed delta to a `u64` (saturating both directions). Used by
/// `Store::account_delta` / `reweigh_entry` so the in-place mutators don't
/// have to repeat the same overflow-guarded match.
#[inline]
pub(crate) fn apply_delta(v: &mut u64, delta: i64) {
    if delta >= 0 {
        *v = v.saturating_add(delta as u64);
    } else {
        *v = v.saturating_sub((-delta) as u64);
    }
}

/// Heap bytes a `SmallBytes`-encoded key would own (`&[u8]` mirror of
/// `SmallBytes::heap_bytes`; 22-byte inline boundary per `kevy-bytes`).
#[inline]
pub(crate) fn key_heap_bytes_for(key: &[u8]) -> u64 {
    if key.len() <= 22 { 0 } else { key.len() as u64 }
}