use bumpalo::Bump;
use datavalue::DataValue;
use crate::Result;
use crate::arena::ContextStack;
use crate::engine::Engine;
use crate::node::CompiledNode;
fn murmurhash3_x86_32(bytes: &[u8], seed: u32) -> u32 {
const C1: u32 = 0xcc9e_2d51;
const C2: u32 = 0x1b87_3593;
let mut h1 = seed;
let block_count = bytes.len() / 4;
for i in 0..block_count {
let start = i * 4;
let mut k1 = u32::from_le_bytes([
bytes[start],
bytes[start + 1],
bytes[start + 2],
bytes[start + 3],
]);
k1 = k1.wrapping_mul(C1);
k1 = k1.rotate_left(15);
k1 = k1.wrapping_mul(C2);
h1 ^= k1;
h1 = h1.rotate_left(13);
h1 = h1.wrapping_mul(5).wrapping_add(0xe654_6b64);
}
let tail_start = block_count * 4;
let mut k1: u32 = 0;
let tail_len = bytes.len() - tail_start;
if tail_len >= 3 {
k1 ^= (bytes[tail_start + 2] as u32) << 16;
}
if tail_len >= 2 {
k1 ^= (bytes[tail_start + 1] as u32) << 8;
}
if tail_len >= 1 {
k1 ^= bytes[tail_start] as u32;
k1 = k1.wrapping_mul(C1);
k1 = k1.rotate_left(15);
k1 = k1.wrapping_mul(C2);
h1 ^= k1;
}
h1 ^= bytes.len() as u32;
h1 ^= h1 >> 16;
h1 = h1.wrapping_mul(0x85eb_ca6b);
h1 ^= h1 >> 13;
h1 = h1.wrapping_mul(0xc2b2_ae35);
h1 ^= h1 >> 16;
h1
}
#[inline]
pub(crate) fn evaluate_fractional<'a>(
args: &'a [CompiledNode],
ctx: &mut ContextStack<'a>,
engine: &Engine,
arena: &'a Bump,
) -> Result<&'a DataValue<'a>> {
if args.is_empty() {
return Ok(arena.alloc(DataValue::Null));
}
let first = engine.dispatch_node(&args[0], ctx, arena)?;
let (bucket_key, distribution_args, skipped_first): (&str, &[CompiledNode], bool) =
if let Some(s) = first.as_str() {
(s, &args[1..], true)
} else {
let implicit = match implicit_bucket_key(ctx.root_input(), arena) {
Some(k) => k,
None => return Ok(arena.alloc(DataValue::Null)),
};
if matches!(first, DataValue::Null) {
(implicit, &args[1..], true)
} else {
(implicit, args, false)
}
};
if distribution_args.is_empty() {
return Ok(arena.alloc(DataValue::Null));
}
let mut buckets: bumpalo::collections::Vec<(&str, i64)> =
bumpalo::collections::Vec::with_capacity_in(distribution_args.len(), arena);
let mut total_weight: i64 = 0;
for (i, node) in distribution_args.iter().enumerate() {
let v = if !skipped_first && i == 0 {
first
} else {
engine.dispatch_node(node, ctx, arena)?
};
let arr = match v.as_array() {
Some(a) => a,
None => return Ok(arena.alloc(DataValue::Null)),
};
if arr.is_empty() {
return Ok(arena.alloc(DataValue::Null));
}
let variant = match arr[0].as_str() {
Some(s) => s,
None => return Ok(arena.alloc(DataValue::Null)),
};
let weight = if arr.len() >= 2 {
arr[1].as_i64().unwrap_or(1).max(0)
} else {
1
};
total_weight += weight;
buckets.push((variant, weight));
}
if buckets.is_empty() || total_weight <= 0 {
return Ok(arena.alloc(DataValue::Null));
}
let hash = murmurhash3_x86_32(bucket_key.as_bytes(), 0);
let bucket = ((hash as u64) * (total_weight as u64)) >> 32;
let mut range_end: u64 = 0;
for (variant, weight) in &buckets {
range_end += *weight as u64;
if bucket < range_end {
let s_in_arena = arena.alloc_str(variant);
return Ok(arena.alloc(DataValue::String(s_in_arena)));
}
}
Ok(arena.alloc(DataValue::Null))
}
fn implicit_bucket_key<'a>(root: &'a DataValue<'a>, arena: &'a Bump) -> Option<&'a str> {
let targeting_key = lookup_string(root, "targetingKey")?;
if targeting_key.is_empty() {
return None;
}
let flag_key = root
.as_object()
.and_then(|obj| object_get(obj, "$flagd"))
.and_then(|flagd| lookup_string(flagd, "flagKey"))
.unwrap_or("");
if flag_key.is_empty() {
return Some(targeting_key);
}
let mut buf =
bumpalo::collections::String::with_capacity_in(flag_key.len() + targeting_key.len(), arena);
buf.push_str(flag_key);
buf.push_str(targeting_key);
Some(buf.into_bump_str())
}
fn lookup_string<'a>(value: &'a DataValue<'a>, key: &str) -> Option<&'a str> {
let pairs = value.as_object()?;
object_get(pairs, key)?.as_str()
}
fn object_get<'a>(pairs: &'a [(&'a str, DataValue<'a>)], key: &str) -> Option<&'a DataValue<'a>> {
pairs.iter().find(|(k, _)| *k == key).map(|(_, v)| v)
}
#[inline]
pub(crate) fn evaluate_sem_ver<'a>(
args: &'a [CompiledNode],
ctx: &mut ContextStack<'a>,
engine: &Engine,
arena: &'a Bump,
) -> Result<&'a DataValue<'a>> {
if args.len() != 3 {
return Ok(arena.alloc(DataValue::Null));
}
let v1_av = engine.dispatch_node(&args[0], ctx, arena)?;
let op_av = engine.dispatch_node(&args[1], ctx, arena)?;
let v2_av = engine.dispatch_node(&args[2], ctx, arena)?;
let v1 = match parse_version(v1_av, arena) {
Some(v) => v,
None => return Ok(arena.alloc(DataValue::Null)),
};
let v2 = match parse_version(v2_av, arena) {
Some(v) => v,
None => return Ok(arena.alloc(DataValue::Null)),
};
let op = match op_av.as_str() {
Some(s) => s,
None => return Ok(arena.alloc(DataValue::Null)),
};
let result = match op {
"=" => v1 == v2,
"!=" => v1 != v2,
"<" => v1 < v2,
"<=" => v1 <= v2,
">" => v1 > v2,
">=" => v1 >= v2,
"^" => v1.major == v2.major,
"~" => v1.major == v2.major && v1.minor == v2.minor,
_ => return Ok(arena.alloc(DataValue::Null)),
};
Ok(crate::arena::singletons::singleton_bool(result))
}
fn parse_version<'a>(value: &'a DataValue<'a>, arena: &'a Bump) -> Option<semver::Version> {
let raw: &str = if let Some(s) = value.as_str() {
s
} else if let Some(n) = value.as_i64() {
let s = bumpalo::format!(in arena, "{}", n);
s.into_bump_str()
} else if let Some(f) = value.as_f64() {
let s = bumpalo::format!(in arena, "{}", f);
s.into_bump_str()
} else {
return None;
};
let stripped = raw.strip_prefix(['v', 'V']).unwrap_or(raw);
let without_build = match stripped.find('+') {
Some(i) => &stripped[..i],
None => stripped,
};
let (core, pre_suffix) = match without_build.find('-') {
Some(i) => (&without_build[..i], &without_build[i..]),
None => (without_build, ""),
};
let dot_count = core.bytes().filter(|b| *b == b'.').count();
let padded_owned;
let padded: &str = match dot_count {
0 => {
padded_owned = bumpalo::format!(in arena, "{}.0.0{}", core, pre_suffix);
padded_owned.into_bump_str()
}
1 => {
padded_owned = bumpalo::format!(in arena, "{}.0{}", core, pre_suffix);
padded_owned.into_bump_str()
}
_ => without_build,
};
semver::Version::parse(padded).ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn murmurhash3_x86_32_empty_string() {
assert_eq!(murmurhash3_x86_32(b"", 0), 0);
}
#[test]
fn murmurhash3_x86_32_known_vectors() {
assert_eq!(murmurhash3_x86_32(b"hello", 0), 0x248bfa47);
assert_eq!(murmurhash3_x86_32(b"foo", 0), 0xf6a5c420);
assert_eq!(
murmurhash3_x86_32(b"The quick brown fox jumps over the lazy dog", 0),
0x2e4ff723
);
}
#[test]
fn murmurhash3_x86_32_with_seed_1() {
assert_eq!(murmurhash3_x86_32(b"hello", 1), 0xbb4abcad);
}
#[test]
fn murmurhash3_x86_32_tail_lengths_round_trip() {
assert_eq!(murmurhash3_x86_32(b"a", 0), 0x3c2569b2);
assert_eq!(murmurhash3_x86_32(b"ab", 0), 0x9bbfd75f);
assert_eq!(murmurhash3_x86_32(b"abc", 0), 0xb3dd93fa);
assert_eq!(murmurhash3_x86_32(b"abcd", 0), 0x43ed676a);
}
}