From 6388218b0ee848cacf5ab07ca2a45f11539df53e Mon Sep 17 00:00:00 2001 From: Marcin Olko Date: Wed, 25 Mar 2026 16:03:47 +0000 Subject: [PATCH 1/5] Implemented flagd-specific JSONLogic operators Signed-off-by: Marcin Olko --- providers/flagd/src/evaluator/BUILD | 17 + providers/flagd/src/evaluator/evaluator.cpp | 8 +- providers/flagd/src/evaluator/flagd_ops.cpp | 358 +++++++++++++++ providers/flagd/src/evaluator/flagd_ops.h | 26 ++ .../src/evaluator/murmur_hash/.clang-tidy | 2 + .../flagd/src/evaluator/murmur_hash/BUILD | 10 + .../src/evaluator/murmur_hash/MurmurHash3.cpp | 410 ++++++++++++++++++ .../src/evaluator/murmur_hash/MurmurHash3.h | 37 ++ providers/flagd/tests/evaluator/BUILD | 22 + .../evaluator/flagd_fractional_op_test.cpp | 246 +++++++++++ .../flagd/tests/evaluator/flagd_ops_test.cpp | 176 ++++++++ 11 files changed, 1311 insertions(+), 1 deletion(-) create mode 100644 providers/flagd/src/evaluator/flagd_ops.cpp create mode 100644 providers/flagd/src/evaluator/flagd_ops.h create mode 100644 providers/flagd/src/evaluator/murmur_hash/.clang-tidy create mode 100644 providers/flagd/src/evaluator/murmur_hash/BUILD create mode 100644 providers/flagd/src/evaluator/murmur_hash/MurmurHash3.cpp create mode 100644 providers/flagd/src/evaluator/murmur_hash/MurmurHash3.h create mode 100644 providers/flagd/tests/evaluator/flagd_fractional_op_test.cpp create mode 100644 providers/flagd/tests/evaluator/flagd_ops_test.cpp diff --git a/providers/flagd/src/evaluator/BUILD b/providers/flagd/src/evaluator/BUILD index 7176704..263391a 100644 --- a/providers/flagd/src/evaluator/BUILD +++ b/providers/flagd/src/evaluator/BUILD @@ -1,5 +1,21 @@ load("@rules_cc//cc:defs.bzl", "cc_library") +cc_library( + name = "flagd_ops", + srcs = ["flagd_ops.cpp"], + hdrs = ["flagd_ops.h"], + include_prefix = "flagd/evaluator", + strip_include_prefix = "", + visibility = ["//visibility:public"], + deps = [ + "//providers/flagd/src/evaluator/json_logic", + "//providers/flagd/src/evaluator/murmur_hash", + "@abseil-cpp//absl/log", + "@abseil-cpp//absl/strings", + "@nlohmann_json//:json", + ], +) + cc_library( name = "evaluator", srcs = ["evaluator.cpp"], @@ -8,6 +24,7 @@ cc_library( strip_include_prefix = "", visibility = ["//visibility:public"], deps = [ + ":flagd_ops", "//providers/flagd/src/evaluator/json_logic", "//providers/flagd/src/sync", "@abseil-cpp//absl/log", diff --git a/providers/flagd/src/evaluator/evaluator.cpp b/providers/flagd/src/evaluator/evaluator.cpp index 6a978ba..5ba18ff 100644 --- a/providers/flagd/src/evaluator/evaluator.cpp +++ b/providers/flagd/src/evaluator/evaluator.cpp @@ -8,6 +8,7 @@ #include "absl/log/log.h" #include "absl/strings/str_cat.h" #include "flagd/sync/sync.h" +#include "flagd_ops.h" namespace flagd { @@ -107,7 +108,12 @@ nlohmann::json ContextToJson(const openfeature::EvaluationContext& ctx) { } // namespace JsonLogicEvaluator::JsonLogicEvaluator(std::shared_ptr sync) - : sync_(std::move(sync)) {} + : sync_(std::move(sync)) { + json_logic_.RegisterOperation("starts_with", StartsWith); + json_logic_.RegisterOperation("ends_with", EndsWith); + json_logic_.RegisterOperation("sem_ver", SemVer); + json_logic_.RegisterOperation("fractional", Fractional); +} template std::unique_ptr> diff --git a/providers/flagd/src/evaluator/flagd_ops.cpp b/providers/flagd/src/evaluator/flagd_ops.cpp new file mode 100644 index 0000000..59e9ee9 --- /dev/null +++ b/providers/flagd/src/evaluator/flagd_ops.cpp @@ -0,0 +1,358 @@ +#include "flagd_ops.h" + +#include +#include +#include +#include +#include + +#include "absl/status/status.h" +#include "absl/strings/numbers.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/str_split.h" +#include "flagd/evaluator/murmur_hash/MurmurHash3.h" + +namespace flagd { + +namespace { + +// Evaluates and retrieves a fixed number of string arguments from JsonLogic. +absl::StatusOr> GetStrings( + const json_logic::JsonLogic& eval, const nlohmann::json& values, + const nlohmann::json& data, size_t expected_size) { + if (!values.is_array()) { + return absl::InvalidArgumentError("Arguments must be an array"); + } + + if (values.size() != expected_size) { + return absl::InvalidArgumentError(absl::StrCat( + "Operator requires exactly ", expected_size, " arguments")); + } + + std::vector result; + result.reserve(expected_size); + for (const nlohmann::json& item : values) { + absl::StatusOr applied = eval.Apply(item, data); + if (!applied.ok()) return applied.status(); + if (!applied.value().is_string()) { + return absl::InvalidArgumentError( + "All arguments must evaluate to strings"); + } + result.push_back(applied.value().get()); + } + + return result; +} + +// Represents a Semantic Version (SemVer 2.0.0) for comparison. +class SemanticVersion { + private: + uint64_t major_; + uint64_t minor_; + uint64_t patch_; + std::vector pre_release_; + + public: + explicit SemanticVersion(uint64_t major = 0, uint64_t minor = 0, + uint64_t patch = 0, + std::vector pre_release = {}) + : major_(major), + minor_(minor), + patch_(patch), + pre_release_(std::move(pre_release)) {} + + uint64_t GetMajor() const { return major_; } + uint64_t GetMinor() const { return minor_; } + uint64_t GetPatch() const { return patch_; } + const std::vector& GetPreRelease() const { return pre_release_; } + + // Parses a string into a SemanticVersion object. + // Supports partial versions (e.g., "1.2") by defaulting missing parts to 0. + static absl::StatusOr Parse(std::string_view text) { + if (!text.empty() && (text[0] == 'v' || text[0] == 'V')) { + text.remove_prefix(1); + } + + // 1. Remove build metadata (ignored for precedence comparison) + std::vector build_parts = absl::StrSplit(text, '+'); + std::string_view core_and_pre = build_parts[0]; + + // 2. Separate core and pre-release + std::vector pre_parts = + absl::StrSplit(core_and_pre, absl::MaxSplits('-', 1)); + std::string_view core = pre_parts[0]; + + // 3. Parse core components (major.minor.patch) + std::vector core_parts = absl::StrSplit(core, '.'); + if (core_parts.empty() || core_parts.size() > 3) { + return absl::InvalidArgumentError( + absl::StrCat("Invalid SemVer core: ", core)); + } + + uint64_t major = 0; + uint64_t minor = 0; + uint64_t patch = 0; + if (!absl::SimpleAtoi(core_parts[0], &major)) { + return absl::InvalidArgumentError( + absl::StrCat("Invalid SemVer major digits: ", core)); + } + + if (core_parts.size() >= 2) { + if (!absl::SimpleAtoi(core_parts[1], &minor)) { + return absl::InvalidArgumentError( + absl::StrCat("Invalid SemVer minor digits: ", core)); + } + } + + if (core_parts.size() == 3) { + if (!absl::SimpleAtoi(core_parts[2], &patch)) { + return absl::InvalidArgumentError( + absl::StrCat("Invalid SemVer patch digits: ", core)); + } + } + + // 4. Parse pre-release identifiers + std::vector pre_release; + if (pre_parts.size() > 1) { + std::vector identifiers = + absl::StrSplit(pre_parts[1], '.'); + for (std::string_view ident : identifiers) { + if (ident.empty()) { + return absl::InvalidArgumentError("Empty pre-release identifier"); + } + pre_release.emplace_back(ident); + } + } + + return SemanticVersion(major, minor, patch, std::move(pre_release)); + } + + // Compares two SemanticVersion objects based on SemVer 2.0.0 precedence + // rules. Returns: -1 if this < other, 0 if equal, 1 if this > other + int Compare(const SemanticVersion& other) const { + if (major_ != other.major_) return major_ < other.major_ ? -1 : 1; + if (minor_ != other.minor_) return minor_ < other.minor_ ? -1 : 1; + if (patch_ != other.patch_) return patch_ < other.patch_ ? -1 : 1; + + // A normal version has higher precedence than a pre-release version + if (pre_release_.empty() && !other.pre_release_.empty()) return 1; + if (!pre_release_.empty() && other.pre_release_.empty()) return -1; + if (pre_release_.empty() && other.pre_release_.empty()) return 0; + + size_t len = std::min(pre_release_.size(), other.pre_release_.size()); + for (size_t i = 0; i < len; ++i) { + const std::string& lhs_part = pre_release_[i]; + const std::string& rhs_part = other.pre_release_[i]; + + // Numeric identifiers have lower precedence than non-numeric identifiers. + bool lhs_is_num = + std::all_of(lhs_part.begin(), lhs_part.end(), ::isdigit); + bool rhs_is_num = + std::all_of(rhs_part.begin(), rhs_part.end(), ::isdigit); + + if (lhs_is_num && rhs_is_num) { + uint64_t lhs_num; + uint64_t rhs_num; + // Numeric identifiers are compared numerically. + if (absl::SimpleAtoi(lhs_part, &lhs_num) && + absl::SimpleAtoi(rhs_part, &rhs_num)) { + if (lhs_num != rhs_num) return lhs_num < rhs_num ? -1 : 1; + } else { + // Fallback to lexicographical comparison if parsing fails (e.g., + // overflow) + if (lhs_part != rhs_part) return lhs_part < rhs_part ? -1 : 1; + } + } else if (lhs_is_num && !rhs_is_num) { + return -1; + } else if (!lhs_is_num && rhs_is_num) { + return 1; + } else { + // Non-numeric identifiers are compared lexicographically in ASCII sort + // order. + if (lhs_part != rhs_part) return lhs_part < rhs_part ? -1 : 1; + } + } + + // A larger set of pre-release fields has a higher precedence than a smaller + // set. + if (pre_release_.size() != other.pre_release_.size()) { + return pre_release_.size() < other.pre_release_.size() ? -1 : 1; + } + + return 0; + } +}; + +} // namespace + +absl::StatusOr StartsWith(const json_logic::JsonLogic& eval, + const nlohmann::json& values, + const nlohmann::json& data) { + absl::StatusOr> strings_res = + GetStrings(eval, values, data, 2); + if (!strings_res.ok()) return strings_res.status(); + + const std::string& source_str = strings_res.value()[0]; + const std::string& prefix = strings_res.value()[1]; + + if (prefix.length() > source_str.length()) return false; + return source_str.compare(0, prefix.length(), prefix) == 0; +} + +absl::StatusOr EndsWith(const json_logic::JsonLogic& eval, + const nlohmann::json& values, + const nlohmann::json& data) { + absl::StatusOr> strings_res = + GetStrings(eval, values, data, 2); + if (!strings_res.ok()) return strings_res.status(); + + const std::string& source_str = strings_res.value()[0]; + const std::string& suffix = strings_res.value()[1]; + + if (suffix.length() > source_str.length()) return false; + return source_str.compare(source_str.length() - suffix.length(), + suffix.length(), suffix) == 0; +} + +absl::StatusOr SemVer(const json_logic::JsonLogic& eval, + const nlohmann::json& values, + const nlohmann::json& data) { + absl::StatusOr> strings_res = + GetStrings(eval, values, data, 3); + if (!strings_res.ok()) return strings_res.status(); + + absl::StatusOr v1_res = + SemanticVersion::Parse(strings_res.value()[0]); + if (!v1_res.ok()) return v1_res.status(); + const SemanticVersion& ver1 = v1_res.value(); + + const std::string& operation = strings_res.value()[1]; + + absl::StatusOr v2_res = + SemanticVersion::Parse(strings_res.value()[2]); + if (!v2_res.ok()) return v2_res.status(); + const SemanticVersion& ver2 = v2_res.value(); + + const int cmp = ver1.Compare(ver2); + + if (operation == "=" || operation == "==") return cmp == 0; + if (operation == "!=") return cmp != 0; + if (operation == ">") return cmp > 0; + if (operation == "<") return cmp < 0; + if (operation == ">=") return cmp >= 0; + if (operation == "<=") return cmp <= 0; + if (operation == "^") return ver1.GetMajor() == ver2.GetMajor(); + if (operation == "~") { + return ver1.GetMajor() == ver2.GetMajor() && + ver1.GetMinor() == ver2.GetMinor(); + } + + return absl::InvalidArgumentError( + absl::StrCat("Unknown SemVer operator: ", operation)); +} + +absl::StatusOr Fractional(const json_logic::JsonLogic& eval, + const nlohmann::json& values, + const nlohmann::json& data) { + if (!values.is_array()) { + return absl::InvalidArgumentError( + "fractional evaluation data is not an array"); + } + + if (values.size() < 2) { + return absl::InvalidArgumentError( + "fractional evaluation data has length under 2"); + } + + // 1. Get the target property value used for bucketing the values + absl::StatusOr bucketing_property_eval = + eval.Apply(values[0], data); + if (!bucketing_property_eval.ok()) return bucketing_property_eval.status(); + std::string bucketing_property_value; + bool first_value_used = false; + if (bucketing_property_eval.value().is_string()) { + bucketing_property_value = + bucketing_property_eval.value().get(); + first_value_used = true; + } else { + // Fallback logic from spec: Concatenate flagKey and targetingKey if + // property is missing + std::string flag_key; + if (data.contains("$flagd") && data["$flagd"].is_object() && + data["$flagd"].contains("flagKey") && + data["$flagd"]["flagKey"].is_string()) { + flag_key = data["$flagd"]["flagKey"].get(); + } + + std::string targeting_key; + if (data.contains("targetingKey") && data["targetingKey"].is_string()) { + targeting_key = data["targetingKey"].get(); + } + bucketing_property_value = absl::StrCat(flag_key, targeting_key); + } + + // 2. Parse the fractional distribution + struct Distribution { + std::string variant; + int weight; + }; + std::vector distributions; + uint64_t sum_of_weights = 0; + + for (size_t i = first_value_used ? 1 : 0; i < values.size(); i++) { + absl::StatusOr item = eval.Apply(values[i], data); + if (!item.ok()) return item.status(); + if (!item.value().is_array() || item.value().empty()) { + return absl::InvalidArgumentError("Invalid distribution element"); + } + + if (!item.value()[0].is_string()) { + return absl::InvalidArgumentError("Variant name must be a string"); + } + + int weight = 1; + if (item.value().size() >= 2 && item.value()[1].is_number()) { + weight = item.value()[1].get(); + } + + distributions.push_back({item.value()[0].get(), weight}); + sum_of_weights += weight; + } + + if (distributions.empty()) { + return absl::InvalidArgumentError("No distributions found"); + } + + if (sum_of_weights == 0) { + return absl::InvalidArgumentError("Sum of weights must be positive"); + } + + if (sum_of_weights >= + static_cast(std::numeric_limits::max())) { + return absl::InvalidArgumentError("Sum of weights exceeds maximum limit"); + } + + // 3. Calculate hash and determine bucket + uint32_t hash_value; + MurmurHash3_x86_32(bucketing_property_value.data(), + static_cast(bucketing_property_value.length()), 0, + &hash_value); + + // High-precision bucketing using 64-bit math to distribute hash over + // sum_of_weights + uint64_t bucket = (static_cast(hash_value) * sum_of_weights) >> + std::numeric_limits::digits; + + // 4. Return the variant corresponding to the bucket + uint64_t range_end = 0; + for (const Distribution& dist : distributions) { + range_end += dist.weight; + if (bucket < range_end) { + return dist.variant; + } + } + + return ""; +} + +} // namespace flagd diff --git a/providers/flagd/src/evaluator/flagd_ops.h b/providers/flagd/src/evaluator/flagd_ops.h new file mode 100644 index 0000000..95bd846 --- /dev/null +++ b/providers/flagd/src/evaluator/flagd_ops.h @@ -0,0 +1,26 @@ +#ifndef PROVIDERS_FLAGD_SRC_EVALUATOR_FLAGD_OPS_H_ +#define PROVIDERS_FLAGD_SRC_EVALUATOR_FLAGD_OPS_H_ + +#include + +#include "absl/status/statusor.h" +#include "flagd/evaluator/json_logic/json_logic.h" + +namespace flagd { + +absl::StatusOr StartsWith(const json_logic::JsonLogic& eval, + const nlohmann::json& values, + const nlohmann::json& data); +absl::StatusOr EndsWith(const json_logic::JsonLogic& eval, + const nlohmann::json& values, + const nlohmann::json& data); +absl::StatusOr SemVer(const json_logic::JsonLogic& eval, + const nlohmann::json& values, + const nlohmann::json& data); +absl::StatusOr Fractional(const json_logic::JsonLogic& eval, + const nlohmann::json& values, + const nlohmann::json& data); + +} // namespace flagd + +#endif // PROVIDERS_FLAGD_SRC_EVALUATOR_FLAGD_OPS_H_ diff --git a/providers/flagd/src/evaluator/murmur_hash/.clang-tidy b/providers/flagd/src/evaluator/murmur_hash/.clang-tidy new file mode 100644 index 0000000..c5d83c4 --- /dev/null +++ b/providers/flagd/src/evaluator/murmur_hash/.clang-tidy @@ -0,0 +1,2 @@ +# Disable all checks for third-party code but keep a dummy check to avoid errors +Checks: "-*,bugprone-argument-comment" diff --git a/providers/flagd/src/evaluator/murmur_hash/BUILD b/providers/flagd/src/evaluator/murmur_hash/BUILD new file mode 100644 index 0000000..b8bdbd3 --- /dev/null +++ b/providers/flagd/src/evaluator/murmur_hash/BUILD @@ -0,0 +1,10 @@ +load("@rules_cc//cc:defs.bzl", "cc_library") + +cc_library( + name = "murmur_hash", + srcs = ["MurmurHash3.cpp"], + hdrs = ["MurmurHash3.h"], + include_prefix = "flagd/evaluator/murmur_hash", + strip_include_prefix = "", + visibility = ["//visibility:public"], +) diff --git a/providers/flagd/src/evaluator/murmur_hash/MurmurHash3.cpp b/providers/flagd/src/evaluator/murmur_hash/MurmurHash3.cpp new file mode 100644 index 0000000..1cc0cfe --- /dev/null +++ b/providers/flagd/src/evaluator/murmur_hash/MurmurHash3.cpp @@ -0,0 +1,410 @@ +//----------------------------------------------------------------------------- +// MurmurHash3 was written by Austin Appleby, and is placed in the public +// domain. The author hereby disclaims copyright to this source code. + +// Note - The x86 and x64 versions do _not_ produce the same results, as the +// algorithms are optimized for their respective platforms. You can still +// compile and run any of them on any platform, but your performance with the +// non-native version will be less than optimal. + +#include "MurmurHash3.h" + +//----------------------------------------------------------------------------- +// Platform-specific functions and macros + +// Microsoft Visual Studio + +#if defined(_MSC_VER) + +#define FORCE_INLINE __forceinline + +#include + +#define ROTL32(x, y) _rotl(x, y) +#define ROTL64(x, y) _rotl64(x, y) + +#define BIG_CONSTANT(x) (x) + +// Other compilers + +#else // defined(_MSC_VER) + +#define FORCE_INLINE inline __attribute__((always_inline)) + +inline uint32_t rotl32(uint32_t x, int8_t r) { + return (x << r) | (x >> (32 - r)); +} + +inline uint64_t rotl64(uint64_t x, int8_t r) { + return (x << r) | (x >> (64 - r)); +} + +#define ROTL32(x, y) rotl32(x, y) +#define ROTL64(x, y) rotl64(x, y) + +#define BIG_CONSTANT(x) (x##LLU) + +#endif // !defined(_MSC_VER) + +//----------------------------------------------------------------------------- +// Block read - if your platform needs to do endian-swapping or can only +// handle aligned reads, do the conversion here + +FORCE_INLINE uint32_t getblock32(const uint32_t* p, int i) { return p[i]; } + +FORCE_INLINE uint64_t getblock64(const uint64_t* p, int i) { return p[i]; } + +//----------------------------------------------------------------------------- +// Finalization mix - force all bits of a hash block to avalanche + +FORCE_INLINE uint32_t fmix32(uint32_t h) { + h ^= h >> 16; + h *= 0x85ebca6b; + h ^= h >> 13; + h *= 0xc2b2ae35; + h ^= h >> 16; + + return h; +} + +//---------- + +FORCE_INLINE uint64_t fmix64(uint64_t k) { + k ^= k >> 33; + k *= BIG_CONSTANT(0xff51afd7ed558ccd); + k ^= k >> 33; + k *= BIG_CONSTANT(0xc4ceb9fe1a85ec53); + k ^= k >> 33; + + return k; +} + +//----------------------------------------------------------------------------- + +void MurmurHash3_x86_32(const void* key, int len, uint32_t seed, void* out) { + const uint8_t* data = (const uint8_t*)key; + const int nblocks = len / 4; + + uint32_t h1 = seed; + + const uint32_t c1 = 0xcc9e2d51; + const uint32_t c2 = 0x1b873593; + + //---------- + // body + + const uint32_t* blocks = (const uint32_t*)(data + nblocks * 4); + + for (int i = -nblocks; i; i++) { + uint32_t k1 = getblock32(blocks, i); + + k1 *= c1; + k1 = ROTL32(k1, 15); + k1 *= c2; + + h1 ^= k1; + h1 = ROTL32(h1, 13); + h1 = h1 * 5 + 0xe6546b64; + } + + //---------- + // tail + + const uint8_t* tail = (const uint8_t*)(data + nblocks * 4); + + uint32_t k1 = 0; + + switch (len & 3) { + case 3: + k1 ^= tail[2] << 16; + case 2: + k1 ^= tail[1] << 8; + case 1: + k1 ^= tail[0]; + k1 *= c1; + k1 = ROTL32(k1, 15); + k1 *= c2; + h1 ^= k1; + }; + + //---------- + // finalization + + h1 ^= len; + + h1 = fmix32(h1); + + *(uint32_t*)out = h1; +} + +//----------------------------------------------------------------------------- + +void MurmurHash3_x86_128(const void* key, const int len, uint32_t seed, + void* out) { + const uint8_t* data = (const uint8_t*)key; + const int nblocks = len / 16; + + uint32_t h1 = seed; + uint32_t h2 = seed; + uint32_t h3 = seed; + uint32_t h4 = seed; + + const uint32_t c1 = 0x239b961b; + const uint32_t c2 = 0xab0e9789; + const uint32_t c3 = 0x38b34ae5; + const uint32_t c4 = 0xa1e38b93; + + //---------- + // body + + const uint32_t* blocks = (const uint32_t*)(data + nblocks * 16); + + for (int i = -nblocks; i; i++) { + uint32_t k1 = getblock32(blocks, i * 4 + 0); + uint32_t k2 = getblock32(blocks, i * 4 + 1); + uint32_t k3 = getblock32(blocks, i * 4 + 2); + uint32_t k4 = getblock32(blocks, i * 4 + 3); + + k1 *= c1; + k1 = ROTL32(k1, 15); + k1 *= c2; + h1 ^= k1; + + h1 = ROTL32(h1, 19); + h1 += h2; + h1 = h1 * 5 + 0x561ccd1b; + + k2 *= c2; + k2 = ROTL32(k2, 16); + k2 *= c3; + h2 ^= k2; + + h2 = ROTL32(h2, 17); + h2 += h3; + h2 = h2 * 5 + 0x0bcaa747; + + k3 *= c3; + k3 = ROTL32(k3, 17); + k3 *= c4; + h3 ^= k3; + + h3 = ROTL32(h3, 15); + h3 += h4; + h3 = h3 * 5 + 0x96cd1c35; + + k4 *= c4; + k4 = ROTL32(k4, 18); + k4 *= c1; + h4 ^= k4; + + h4 = ROTL32(h4, 13); + h4 += h1; + h4 = h4 * 5 + 0x32ac3b17; + } + + //---------- + // tail + + const uint8_t* tail = (const uint8_t*)(data + nblocks * 16); + + uint32_t k1 = 0; + uint32_t k2 = 0; + uint32_t k3 = 0; + uint32_t k4 = 0; + + switch (len & 15) { + case 15: + k4 ^= tail[14] << 16; + case 14: + k4 ^= tail[13] << 8; + case 13: + k4 ^= tail[12] << 0; + k4 *= c4; + k4 = ROTL32(k4, 18); + k4 *= c1; + h4 ^= k4; + + case 12: + k3 ^= tail[11] << 24; + case 11: + k3 ^= tail[10] << 16; + case 10: + k3 ^= tail[9] << 8; + case 9: + k3 ^= tail[8] << 0; + k3 *= c3; + k3 = ROTL32(k3, 17); + k3 *= c4; + h3 ^= k3; + + case 8: + k2 ^= tail[7] << 24; + case 7: + k2 ^= tail[6] << 16; + case 6: + k2 ^= tail[5] << 8; + case 5: + k2 ^= tail[4] << 0; + k2 *= c2; + k2 = ROTL32(k2, 16); + k2 *= c3; + h2 ^= k2; + + case 4: + k1 ^= tail[3] << 24; + case 3: + k1 ^= tail[2] << 16; + case 2: + k1 ^= tail[1] << 8; + case 1: + k1 ^= tail[0] << 0; + k1 *= c1; + k1 = ROTL32(k1, 15); + k1 *= c2; + h1 ^= k1; + }; + + //---------- + // finalization + + h1 ^= len; + h2 ^= len; + h3 ^= len; + h4 ^= len; + + h1 += h2; + h1 += h3; + h1 += h4; + h2 += h1; + h3 += h1; + h4 += h1; + + h1 = fmix32(h1); + h2 = fmix32(h2); + h3 = fmix32(h3); + h4 = fmix32(h4); + + h1 += h2; + h1 += h3; + h1 += h4; + h2 += h1; + h3 += h1; + h4 += h1; + + ((uint32_t*)out)[0] = h1; + ((uint32_t*)out)[1] = h2; + ((uint32_t*)out)[2] = h3; + ((uint32_t*)out)[3] = h4; +} + +//----------------------------------------------------------------------------- + +void MurmurHash3_x64_128(const void* key, const int len, const uint32_t seed, + void* out) { + const uint8_t* data = (const uint8_t*)key; + const int nblocks = len / 16; + + uint64_t h1 = seed; + uint64_t h2 = seed; + + const uint64_t c1 = BIG_CONSTANT(0x87c37b91114253d5); + const uint64_t c2 = BIG_CONSTANT(0x4cf5ad432745937f); + + //---------- + // body + + const uint64_t* blocks = (const uint64_t*)(data); + + for (int i = 0; i < nblocks; i++) { + uint64_t k1 = getblock64(blocks, i * 2 + 0); + uint64_t k2 = getblock64(blocks, i * 2 + 1); + + k1 *= c1; + k1 = ROTL64(k1, 31); + k1 *= c2; + h1 ^= k1; + + h1 = ROTL64(h1, 27); + h1 += h2; + h1 = h1 * 5 + 0x52dce729; + + k2 *= c2; + k2 = ROTL64(k2, 33); + k2 *= c1; + h2 ^= k2; + + h2 = ROTL64(h2, 31); + h2 += h1; + h2 = h2 * 5 + 0x38495ab5; + } + + //---------- + // tail + + const uint8_t* tail = (const uint8_t*)(data + nblocks * 16); + + uint64_t k1 = 0; + uint64_t k2 = 0; + + switch (len & 15) { + case 15: + k2 ^= ((uint64_t)tail[14]) << 48; + case 14: + k2 ^= ((uint64_t)tail[13]) << 40; + case 13: + k2 ^= ((uint64_t)tail[12]) << 32; + case 12: + k2 ^= ((uint64_t)tail[11]) << 24; + case 11: + k2 ^= ((uint64_t)tail[10]) << 16; + case 10: + k2 ^= ((uint64_t)tail[9]) << 8; + case 9: + k2 ^= ((uint64_t)tail[8]) << 0; + k2 *= c2; + k2 = ROTL64(k2, 33); + k2 *= c1; + h2 ^= k2; + + case 8: + k1 ^= ((uint64_t)tail[7]) << 56; + case 7: + k1 ^= ((uint64_t)tail[6]) << 48; + case 6: + k1 ^= ((uint64_t)tail[5]) << 40; + case 5: + k1 ^= ((uint64_t)tail[4]) << 32; + case 4: + k1 ^= ((uint64_t)tail[3]) << 24; + case 3: + k1 ^= ((uint64_t)tail[2]) << 16; + case 2: + k1 ^= ((uint64_t)tail[1]) << 8; + case 1: + k1 ^= ((uint64_t)tail[0]) << 0; + k1 *= c1; + k1 = ROTL64(k1, 31); + k1 *= c2; + h1 ^= k1; + }; + + //---------- + // finalization + + h1 ^= len; + h2 ^= len; + + h1 += h2; + h2 += h1; + + h1 = fmix64(h1); + h2 = fmix64(h2); + + h1 += h2; + h2 += h1; + + ((uint64_t*)out)[0] = h1; + ((uint64_t*)out)[1] = h2; +} + +//----------------------------------------------------------------------------- \ No newline at end of file diff --git a/providers/flagd/src/evaluator/murmur_hash/MurmurHash3.h b/providers/flagd/src/evaluator/murmur_hash/MurmurHash3.h new file mode 100644 index 0000000..e1c35f2 --- /dev/null +++ b/providers/flagd/src/evaluator/murmur_hash/MurmurHash3.h @@ -0,0 +1,37 @@ +//----------------------------------------------------------------------------- +// MurmurHash3 was written by Austin Appleby, and is placed in the public +// domain. The author hereby disclaims copyright to this source code. + +#ifndef _MURMURHASH3_H_ +#define _MURMURHASH3_H_ + +//----------------------------------------------------------------------------- +// Platform-specific functions and macros + +// Microsoft Visual Studio + +#if defined(_MSC_VER) && (_MSC_VER < 1600) + +typedef unsigned char uint8_t; +typedef unsigned int uint32_t; +typedef unsigned __int64 uint64_t; + +// Other compilers + +#else // defined(_MSC_VER) + +#include + +#endif // !defined(_MSC_VER) + +//----------------------------------------------------------------------------- + +void MurmurHash3_x86_32(const void* key, int len, uint32_t seed, void* out); + +void MurmurHash3_x86_128(const void* key, int len, uint32_t seed, void* out); + +void MurmurHash3_x64_128(const void* key, int len, uint32_t seed, void* out); + +//----------------------------------------------------------------------------- + +#endif // _MURMURHASH3_H_ \ No newline at end of file diff --git a/providers/flagd/tests/evaluator/BUILD b/providers/flagd/tests/evaluator/BUILD index 6f2ae7a..0711852 100644 --- a/providers/flagd/tests/evaluator/BUILD +++ b/providers/flagd/tests/evaluator/BUILD @@ -10,3 +10,25 @@ cc_test( "@nlohmann_json//:json", ], ) + +cc_test( + name = "flagd_ops_test", + srcs = ["flagd_ops_test.cpp"], + deps = [ + "//providers/flagd/src/evaluator:flagd_ops", + "//providers/flagd/src/evaluator/json_logic", + "@googletest//:gtest_main", + "@nlohmann_json//:json", + ], +) + +cc_test( + name = "flagd_fractional_op_test", + srcs = ["flagd_fractional_op_test.cpp"], + deps = [ + "//providers/flagd/src/evaluator:flagd_ops", + "//providers/flagd/src/evaluator/json_logic", + "@googletest//:gtest_main", + "@nlohmann_json//:json", + ], +) diff --git a/providers/flagd/tests/evaluator/flagd_fractional_op_test.cpp b/providers/flagd/tests/evaluator/flagd_fractional_op_test.cpp new file mode 100644 index 0000000..179928f --- /dev/null +++ b/providers/flagd/tests/evaluator/flagd_fractional_op_test.cpp @@ -0,0 +1,246 @@ +// TODO(#91): This file contains rewritten test cases from +// open-feature/flagd-testbed/blob/main/gherkin/targeting.feature. +// They should be removed once automatic gherkin tests are introduced. +#include + +#include + +#include "providers/flagd/src/evaluator/flagd_ops.h" +#include "providers/flagd/src/evaluator/json_logic/json_logic.h" + +using json_logic::JsonLogic; +using nlohmann::json; + +class FlagdOpsTest : public ::testing::Test { + protected: + void SetUp() override { + // Assuming these are registered + json_logic_.RegisterOperation("starts_with", flagd::StartsWith); + json_logic_.RegisterOperation("ends_with", flagd::EndsWith); + json_logic_.RegisterOperation("sem_ver", flagd::SemVer); + json_logic_.RegisterOperation("fractional", flagd::Fractional); + } + + JsonLogic json_logic_; +}; + +TEST_F(FlagdOpsTest, FractionalV2BasicDistribution) { + json data = json::object(); + data["$flagd"] = json::object(); + data["$flagd"]["flagKey"] = "fractional-flag"; + + json logic = json::parse(R"({ + "fractional": [ + {"cat": [ + { "var": "$flagd.flagKey" }, + { "var": "user.name" } + ]}, + [ "clubs", 25 ], + [ "diamonds", 25 ], + [ "hearts", 25 ], + [ "spades", 25 ] + ] + })"); + + // V2 Expected Values + data["user"]["name"] = "jack"; + EXPECT_EQ(json_logic_.Apply(logic, data).value(), "hearts"); + + data["user"]["name"] = "queen"; + EXPECT_EQ(json_logic_.Apply(logic, data).value(), "spades"); + + data["user"]["name"] = "ten"; + EXPECT_EQ(json_logic_.Apply(logic, data).value(), "clubs"); + + data["user"]["name"] = "nine"; + EXPECT_EQ(json_logic_.Apply(logic, data).value(), "diamonds"); + + data["user"]["name"] = "3"; + EXPECT_EQ(json_logic_.Apply(logic, data).value(), "clubs"); +} + +TEST_F(FlagdOpsTest, FractionalV2Shorthand) { + json data = json::object(); + data["$flagd"] = json::object(); + data["$flagd"]["flagKey"] = "fractional-flag-shorthand"; + + json logic = json::parse(R"({ + "fractional": [ + [ "heads" ], + [ "tails", 1 ] + ] + })"); + + // V2 Expected Values + data["targetingKey"] = "jon@company.com"; + EXPECT_EQ(json_logic_.Apply(logic, data).value(), "heads"); + + data["targetingKey"] = "jane@company.com"; + EXPECT_EQ(json_logic_.Apply(logic, data).value(), "tails"); +} + +TEST_F(FlagdOpsTest, FractionalV2SharedSeed) { + json data = json::object(); + + json logic = json::parse(R"({ + "fractional": [ + { "cat": [ + "shared-seed", + { "var": "user.name" } + ]}, + [ "clubs", 25 ], + [ "diamonds", 25 ], + [ "hearts", 25 ], + [ "spades", 25 ] + ] + })"); + + // V2 Expected Values + data["user"]["name"] = "seven"; + EXPECT_EQ(json_logic_.Apply(logic, data).value(), "hearts"); + + data["user"]["name"] = "eight"; + EXPECT_EQ(json_logic_.Apply(logic, data).value(), "diamonds"); + + data["user"]["name"] = "nine"; + EXPECT_EQ(json_logic_.Apply(logic, data).value(), "clubs"); + + data["user"]["name"] = "two"; + EXPECT_EQ(json_logic_.Apply(logic, data).value(), "spades"); +} + +TEST_F(FlagdOpsTest, FractionalV2HashEdgeCases) { + json data = json::object(); + + json logic = json::parse(R"({ + "fractional": [ + { "var": "targetingKey" }, + [ "lower", 50 ], + [ "upper", 50 ] + ] + })"); + + // hash = 0 + data["targetingKey"] = "ejOoVL"; + EXPECT_EQ(json_logic_.Apply(logic, data).value(), "lower"); + + // hash = 1 + data["targetingKey"] = "bY9fO-"; + EXPECT_EQ(json_logic_.Apply(logic, data).value(), "lower"); + + // hash = 2147483647 (INT32_MAX) + data["targetingKey"] = "SI7p-"; + EXPECT_EQ(json_logic_.Apply(logic, data).value(), "lower"); + + // hash = 2147483648 (INT32_MIN when cast to signed 32-bit - critical + // threshold) + data["targetingKey"] = "6LvT0"; + EXPECT_EQ(json_logic_.Apply(logic, data).value(), "upper"); + + // hash = 4294967295 (UINT32_MAX / -1 when signed) + data["targetingKey"] = "ceQdGm"; + EXPECT_EQ(json_logic_.Apply(logic, data).value(), "upper"); +} + +TEST_F(FlagdOpsTest, FractionalV2NestedIfVariantName) { + json data = json::object(); + + json logic = json::parse(R"({ + "fractional": [ + { "var": "targetingKey" }, + [ + { + "if": [ + { "==": [{ "var": "tier" }, "premium"] }, + "premium", + "standard" + ] + }, + 50 + ], + [ "standard", 50 ] + ] + })"); + + data["targetingKey"] = "jon@company.com"; + data["tier"] = "premium"; + EXPECT_EQ(json_logic_.Apply(logic, data).value(), "premium"); + + data["tier"] = "basic"; + EXPECT_EQ(json_logic_.Apply(logic, data).value(), "standard"); + + data["targetingKey"] = "user1"; + data["tier"] = "premium"; + EXPECT_EQ(json_logic_.Apply(logic, data).value(), "standard"); + + data["tier"] = "basic"; + EXPECT_EQ(json_logic_.Apply(logic, data).value(), "standard"); +} + +TEST_F(FlagdOpsTest, FractionalV2NestedVarVariantName) { + json data = json::object(); + + json logic = json::parse(R"({ + "fractional": [ + { "var": "targetingKey" }, + [ { "var": "color" }, 50 ], + [ "blue", 50 ] + ] + })"); + + data["targetingKey"] = "jon@company.com"; + + data["color"] = "red"; + EXPECT_EQ(json_logic_.Apply(logic, data).value(), "red"); + + data["color"] = "green"; + EXPECT_EQ(json_logic_.Apply(logic, data).value(), "green"); + + data["targetingKey"] = "user1"; + data["color"] = "red"; + EXPECT_EQ(json_logic_.Apply(logic, data).value(), "blue"); + + // Fallbacks handled by outer engine, json_logic returns the literal var + // evaluations + data["targetingKey"] = "jon@company.com"; + data["color"] = "yellow"; + EXPECT_EQ(json_logic_.Apply(logic, data).value(), "yellow"); + + data["color"] = ""; + EXPECT_EQ(json_logic_.Apply(logic, data).value(), ""); +} + +TEST_F(FlagdOpsTest, FractionalV2NestedWeightLogic) { + json data = json::object(); + + json logic = json::parse(R"({ + "fractional": [ + { "var": "targetingKey" }, + [ + "red", + { + "if": [ + { "==": [{ "var": "tier" }, "premium"] }, + 100, + 0 + ] + } + ], + [ "blue", 10 ] + ] + })"); + + data["targetingKey"] = "jon@company.com"; + data["tier"] = "premium"; + EXPECT_EQ(json_logic_.Apply(logic, data).value(), "red"); + + data["tier"] = "basic"; + EXPECT_EQ(json_logic_.Apply(logic, data).value(), "blue"); + + data["targetingKey"] = "user1"; + data["tier"] = "premium"; + EXPECT_EQ(json_logic_.Apply(logic, data).value(), "red"); + + data["tier"] = "basic"; + EXPECT_EQ(json_logic_.Apply(logic, data).value(), "blue"); +} \ No newline at end of file diff --git a/providers/flagd/tests/evaluator/flagd_ops_test.cpp b/providers/flagd/tests/evaluator/flagd_ops_test.cpp new file mode 100644 index 0000000..8dbee5d --- /dev/null +++ b/providers/flagd/tests/evaluator/flagd_ops_test.cpp @@ -0,0 +1,176 @@ +#include "providers/flagd/src/evaluator/flagd_ops.h" + +#include + +#include + +#include "providers/flagd/src/evaluator/json_logic/json_logic.h" + +using json_logic::JsonLogic; +using nlohmann::json; + +class FlagdOpsTest : public ::testing::Test { + protected: + void SetUp() override { + json_logic_.RegisterOperation("starts_with", flagd::StartsWith); + json_logic_.RegisterOperation("ends_with", flagd::EndsWith); + json_logic_.RegisterOperation("sem_ver", flagd::SemVer); + json_logic_.RegisterOperation("fractional", flagd::Fractional); + } + + JsonLogic json_logic_; +}; + +TEST_F(FlagdOpsTest, StartsWith) { + json data = json::object(); + + // Basic success + EXPECT_TRUE( + json_logic_ + .Apply(json::parse(R"({"starts_with": ["hello world", "hello"]})"), + data) + .value()); + EXPECT_FALSE( + json_logic_ + .Apply(json::parse(R"({"starts_with": ["hello world", "world"]})"), + data) + .value()); + + // Evaluation of arguments + data["prefix"] = "hello"; + EXPECT_TRUE( + json_logic_ + .Apply(json::parse( + R"({"starts_with": ["hello world", {"var": "prefix"}]})"), + data) + .value()); + + // Error cases + EXPECT_FALSE( + json_logic_.Apply(json::parse(R"({"starts_with": ["a", "abc"]})"), data) + .value()); + EXPECT_FALSE( + json_logic_.Apply(json::parse(R"({"starts_with": ["", "a"]})"), data) + .value()); +} + +TEST_F(FlagdOpsTest, EndsWith) { + json data = json::object(); + + // Basic success + EXPECT_TRUE( + json_logic_ + .Apply(json::parse(R"({"ends_with": ["hello world", "world"]})"), + data) + .value()); + EXPECT_FALSE( + json_logic_ + .Apply(json::parse(R"({"ends_with": ["hello world", "hello"]})"), + data) + .value()); + + // Evaluation of arguments + data["suffix"] = "world"; + EXPECT_TRUE( + json_logic_ + .Apply(json::parse( + R"({"ends_with": ["hello world", {"var": "suffix"}]})"), + data) + .value()); + + // Error cases + EXPECT_FALSE( + json_logic_.Apply(json::parse(R"({"ends_with": ["a", "abc"]})"), data) + .value()); + EXPECT_FALSE( + json_logic_.Apply(json::parse(R"({"ends_with": ["", "a"]})"), data) + .value()); +} + +TEST_F(FlagdOpsTest, SemVer) { + json data = json::object(); + + // Basic comparison + EXPECT_TRUE( + json_logic_ + .Apply(json::parse(R"({"sem_ver": ["1.2.3", "=", "1.2.3"]})"), data) + .value()); + EXPECT_TRUE( + json_logic_ + .Apply(json::parse(R"({"sem_ver": ["1.2.3", "!=", "1.2.4"]})"), data) + .value()); + EXPECT_TRUE( + json_logic_ + .Apply(json::parse(R"({"sem_ver": ["2.0.0", ">", "1.9.9"]})"), data) + .value()); + EXPECT_TRUE( + json_logic_ + .Apply(json::parse(R"({"sem_ver": ["1.0.0", "<", "2.0.0"]})"), data) + .value()); + EXPECT_TRUE( + json_logic_ + .Apply(json::parse(R"({"sem_ver": ["1.2.3", ">=", "1.2.3"]})"), data) + .value()); + EXPECT_TRUE( + json_logic_ + .Apply(json::parse(R"({"sem_ver": ["1.2.3", "<=", "1.2.3"]})"), data) + .value()); + + // Pre-release precedence + EXPECT_TRUE( + json_logic_ + .Apply(json::parse(R"({"sem_ver": ["1.0.0-alpha", "<", "1.0.0"]})"), + data) + .value()); + EXPECT_TRUE( + json_logic_ + .Apply(json::parse( + R"({"sem_ver": ["1.0.0-alpha", "<", "1.0.0-alpha.1"]})"), + data) + .value()); + + // Build metadata ignored + EXPECT_TRUE( + json_logic_ + .Apply(json::parse( + R"({"sem_ver": ["1.0.0+build.1", "=", "1.0.0+build.2"]})"), + data) + .value()); + + // v/V prefix + EXPECT_TRUE( + json_logic_ + .Apply(json::parse(R"({"sem_ver": ["v1.2.3", "=", "1.2.3"]})"), data) + .value()); + EXPECT_TRUE( + json_logic_ + .Apply(json::parse(R"({"sem_ver": ["1.2.3", "=", "V1.2.3"]})"), data) + .value()); + + // Compatible operators (^ and ~) + EXPECT_TRUE( + json_logic_ + .Apply(json::parse(R"({"sem_ver": ["1.2.3", "^", "1.0.0"]})"), data) + .value()); + EXPECT_FALSE( + json_logic_ + .Apply(json::parse(R"({"sem_ver": ["2.0.0", "^", "1.0.0"]})"), data) + .value()); + EXPECT_TRUE( + json_logic_ + .Apply(json::parse(R"({"sem_ver": ["1.2.3", "~", "1.2.0"]})"), data) + .value()); + EXPECT_FALSE( + json_logic_ + .Apply(json::parse(R"({"sem_ver": ["1.3.0", "~", "1.2.0"]})"), data) + .value()); + // Partial version support (v1.2 -> 1.2.0, v1 -> 1.0.0) + EXPECT_TRUE( + json_logic_ + .Apply(json::parse(R"({"sem_ver": ["v1.2", "=", "1.2.0"]})"), data) + .value()); + EXPECT_TRUE( + json_logic_ + .Apply(json::parse(R"({"sem_ver": ["v1", "=", "1.0.0"]})"), data) + .value()); +} From 0d13e88ad42c2400ab874618977874aea4807bde Mon Sep 17 00:00:00 2001 From: Marcin Olko Date: Thu, 26 Mar 2026 08:11:50 +0000 Subject: [PATCH 2/5] Addressed gemini comments Signed-off-by: Marcin Olko --- providers/flagd/src/evaluator/flagd_ops.cpp | 37 +++++++++++++++++---- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/providers/flagd/src/evaluator/flagd_ops.cpp b/providers/flagd/src/evaluator/flagd_ops.cpp index 59e9ee9..d669786 100644 --- a/providers/flagd/src/evaluator/flagd_ops.cpp +++ b/providers/flagd/src/evaluator/flagd_ops.cpp @@ -7,6 +7,7 @@ #include #include "absl/status/status.h" +#include "absl/strings/match.h" #include "absl/strings/numbers.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_split.h" @@ -16,6 +17,11 @@ namespace flagd { namespace { +// Checks if a string has a leading zero (and is not just "0"). +bool HasLeadingZero(std::string_view str) { + return str.size() > 1 && str[0] == '0'; +} + // Evaluates and retrieves a fixed number of string arguments from JsonLogic. absl::StatusOr> GetStrings( const json_logic::JsonLogic& eval, const nlohmann::json& values, @@ -92,12 +98,20 @@ class SemanticVersion { uint64_t major = 0; uint64_t minor = 0; uint64_t patch = 0; + if (HasLeadingZero(core_parts[0])) { + return absl::InvalidArgumentError( + absl::StrCat("Major version MUST NOT contain leading zeros: ", core)); + } if (!absl::SimpleAtoi(core_parts[0], &major)) { return absl::InvalidArgumentError( absl::StrCat("Invalid SemVer major digits: ", core)); } if (core_parts.size() >= 2) { + if (HasLeadingZero(core_parts[1])) { + return absl::InvalidArgumentError(absl::StrCat( + "Minor version MUST NOT contain leading zeros: ", core)); + } if (!absl::SimpleAtoi(core_parts[1], &minor)) { return absl::InvalidArgumentError( absl::StrCat("Invalid SemVer minor digits: ", core)); @@ -105,6 +119,10 @@ class SemanticVersion { } if (core_parts.size() == 3) { + if (HasLeadingZero(core_parts[2])) { + return absl::InvalidArgumentError(absl::StrCat( + "Patch version MUST NOT contain leading zeros: ", core)); + } if (!absl::SimpleAtoi(core_parts[2], &patch)) { return absl::InvalidArgumentError( absl::StrCat("Invalid SemVer patch digits: ", core)); @@ -120,6 +138,13 @@ class SemanticVersion { if (ident.empty()) { return absl::InvalidArgumentError("Empty pre-release identifier"); } + + bool is_numeric = std::all_of(ident.begin(), ident.end(), ::isdigit); + if (is_numeric && HasLeadingZero(ident)) { + return absl::InvalidArgumentError( + "Numeric pre-release identifiers MUST NOT contain leading zeros"); + } + pre_release.emplace_back(ident); } } @@ -195,8 +220,7 @@ absl::StatusOr StartsWith(const json_logic::JsonLogic& eval, const std::string& source_str = strings_res.value()[0]; const std::string& prefix = strings_res.value()[1]; - if (prefix.length() > source_str.length()) return false; - return source_str.compare(0, prefix.length(), prefix) == 0; + return absl::StartsWith(source_str, prefix); } absl::StatusOr EndsWith(const json_logic::JsonLogic& eval, @@ -209,9 +233,7 @@ absl::StatusOr EndsWith(const json_logic::JsonLogic& eval, const std::string& source_str = strings_res.value()[0]; const std::string& suffix = strings_res.value()[1]; - if (suffix.length() > source_str.length()) return false; - return source_str.compare(source_str.length() - suffix.length(), - suffix.length(), suffix) == 0; + return absl::EndsWith(source_str, suffix); } absl::StatusOr SemVer(const json_logic::JsonLogic& eval, @@ -313,6 +335,9 @@ absl::StatusOr Fractional(const json_logic::JsonLogic& eval, int weight = 1; if (item.value().size() >= 2 && item.value()[1].is_number()) { weight = item.value()[1].get(); + if (weight < 0) { + return absl::InvalidArgumentError("Weight must be non-negative."); + } } distributions.push_back({item.value()[0].get(), weight}); @@ -352,7 +377,7 @@ absl::StatusOr Fractional(const json_logic::JsonLogic& eval, } } - return ""; + return absl::InternalError("Fractional bucketing failed to find a variant"); } } // namespace flagd From 05852f966a206fa082b38679d02626d3a8b2cb89 Mon Sep 17 00:00:00 2001 From: Marcin Olko Date: Thu, 26 Mar 2026 10:42:48 +0000 Subject: [PATCH 3/5] Addressed gemini comments Signed-off-by: Marcin Olko --- providers/flagd/src/evaluator/flagd_ops.cpp | 50 ++++++++++--------- .../evaluator/flagd_fractional_op_test.cpp | 2 +- 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/providers/flagd/src/evaluator/flagd_ops.cpp b/providers/flagd/src/evaluator/flagd_ops.cpp index d669786..0baafea 100644 --- a/providers/flagd/src/evaluator/flagd_ops.cpp +++ b/providers/flagd/src/evaluator/flagd_ops.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include "absl/status/status.h" @@ -22,6 +23,19 @@ bool HasLeadingZero(std::string_view str) { return str.size() > 1 && str[0] == '0'; } +absl::Status ParseSemVerNum(std::string_view num_str, std::string_view name, + uint64_t* out) { + if (HasLeadingZero(num_str)) { + return absl::InvalidArgumentError(absl::StrCat( + name, " version MUST NOT contain leading zeros: ", num_str)); + } + if (!absl::SimpleAtoi(num_str, out)) { + return absl::InvalidArgumentError( + absl::StrCat("Invalid SemVer ", name, " digits: ", num_str)); + } + return absl::OkStatus(); +}; + // Evaluates and retrieves a fixed number of string arguments from JsonLogic. absl::StatusOr> GetStrings( const json_logic::JsonLogic& eval, const nlohmann::json& values, @@ -98,34 +112,22 @@ class SemanticVersion { uint64_t major = 0; uint64_t minor = 0; uint64_t patch = 0; - if (HasLeadingZero(core_parts[0])) { - return absl::InvalidArgumentError( - absl::StrCat("Major version MUST NOT contain leading zeros: ", core)); - } - if (!absl::SimpleAtoi(core_parts[0], &major)) { - return absl::InvalidArgumentError( - absl::StrCat("Invalid SemVer major digits: ", core)); + if (absl::Status status = ParseSemVerNum(core_parts[0], "Major", &major); + !status.ok()) { + return status; } if (core_parts.size() >= 2) { - if (HasLeadingZero(core_parts[1])) { - return absl::InvalidArgumentError(absl::StrCat( - "Minor version MUST NOT contain leading zeros: ", core)); - } - if (!absl::SimpleAtoi(core_parts[1], &minor)) { - return absl::InvalidArgumentError( - absl::StrCat("Invalid SemVer minor digits: ", core)); + if (absl::Status status = ParseSemVerNum(core_parts[1], "Minor", &minor); + !status.ok()) { + return status; } } if (core_parts.size() == 3) { - if (HasLeadingZero(core_parts[2])) { - return absl::InvalidArgumentError(absl::StrCat( - "Patch version MUST NOT contain leading zeros: ", core)); - } - if (!absl::SimpleAtoi(core_parts[2], &patch)) { - return absl::InvalidArgumentError( - absl::StrCat("Invalid SemVer patch digits: ", core)); + if (absl::Status status = ParseSemVerNum(core_parts[2], "Patch", &patch); + !status.ok()) { + return status; } } @@ -316,7 +318,7 @@ absl::StatusOr Fractional(const json_logic::JsonLogic& eval, // 2. Parse the fractional distribution struct Distribution { std::string variant; - int weight; + int32_t weight; }; std::vector distributions; uint64_t sum_of_weights = 0; @@ -332,9 +334,9 @@ absl::StatusOr Fractional(const json_logic::JsonLogic& eval, return absl::InvalidArgumentError("Variant name must be a string"); } - int weight = 1; + int32_t weight = 1; if (item.value().size() >= 2 && item.value()[1].is_number()) { - weight = item.value()[1].get(); + weight = item.value()[1].get(); if (weight < 0) { return absl::InvalidArgumentError("Weight must be non-negative."); } diff --git a/providers/flagd/tests/evaluator/flagd_fractional_op_test.cpp b/providers/flagd/tests/evaluator/flagd_fractional_op_test.cpp index 179928f..6958a5e 100644 --- a/providers/flagd/tests/evaluator/flagd_fractional_op_test.cpp +++ b/providers/flagd/tests/evaluator/flagd_fractional_op_test.cpp @@ -243,4 +243,4 @@ TEST_F(FlagdOpsTest, FractionalV2NestedWeightLogic) { data["tier"] = "basic"; EXPECT_EQ(json_logic_.Apply(logic, data).value(), "blue"); -} \ No newline at end of file +} From 1343e9edb2db5b01dd0e2c7173fd06437d7d9b3d Mon Sep 17 00:00:00 2001 From: Marcin Olko Date: Thu, 26 Mar 2026 10:57:01 +0000 Subject: [PATCH 4/5] Improved code a bit Signed-off-by: Marcin Olko --- providers/flagd/src/evaluator/flagd_ops.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/providers/flagd/src/evaluator/flagd_ops.cpp b/providers/flagd/src/evaluator/flagd_ops.cpp index 0baafea..6f7e169 100644 --- a/providers/flagd/src/evaluator/flagd_ops.cpp +++ b/providers/flagd/src/evaluator/flagd_ops.cpp @@ -23,6 +23,7 @@ bool HasLeadingZero(std::string_view str) { return str.size() > 1 && str[0] == '0'; } +// Parses number according to SemVer 2.0.0 specification. absl::Status ParseSemVerNum(std::string_view num_str, std::string_view name, uint64_t* out) { if (HasLeadingZero(num_str)) { @@ -210,6 +211,11 @@ class SemanticVersion { } }; +struct Distribution { + std::string variant; + int32_t weight; +}; + } // namespace absl::StatusOr StartsWith(const json_logic::JsonLogic& eval, @@ -292,6 +298,7 @@ absl::StatusOr Fractional(const json_logic::JsonLogic& eval, absl::StatusOr bucketing_property_eval = eval.Apply(values[0], data); if (!bucketing_property_eval.ok()) return bucketing_property_eval.status(); + std::string bucketing_property_value; bool first_value_used = false; if (bucketing_property_eval.value().is_string()) { @@ -316,10 +323,6 @@ absl::StatusOr Fractional(const json_logic::JsonLogic& eval, } // 2. Parse the fractional distribution - struct Distribution { - std::string variant; - int32_t weight; - }; std::vector distributions; uint64_t sum_of_weights = 0; From f05c4f3338a76279b4c1787e46cb1177dad6a562 Mon Sep 17 00:00:00 2001 From: Marcin Olko Date: Thu, 26 Mar 2026 14:36:26 +0000 Subject: [PATCH 5/5] Adding info and rationale on murmurHash3 library Signed-off-by: Marcin Olko --- .../flagd/src/evaluator/murmur_hash/MurmurHash3.cpp | 1 + providers/flagd/src/evaluator/murmur_hash/MurmurHash3.h | 1 + providers/flagd/src/evaluator/murmur_hash/README.md | 9 +++++++++ 3 files changed, 11 insertions(+) create mode 100644 providers/flagd/src/evaluator/murmur_hash/README.md diff --git a/providers/flagd/src/evaluator/murmur_hash/MurmurHash3.cpp b/providers/flagd/src/evaluator/murmur_hash/MurmurHash3.cpp index 1cc0cfe..bf03ebd 100644 --- a/providers/flagd/src/evaluator/murmur_hash/MurmurHash3.cpp +++ b/providers/flagd/src/evaluator/murmur_hash/MurmurHash3.cpp @@ -1,6 +1,7 @@ //----------------------------------------------------------------------------- // MurmurHash3 was written by Austin Appleby, and is placed in the public // domain. The author hereby disclaims copyright to this source code. +// Source: https://github.com/aappleby/smhasher/blob/master/src/MurmurHash3.cpp // Note - The x86 and x64 versions do _not_ produce the same results, as the // algorithms are optimized for their respective platforms. You can still diff --git a/providers/flagd/src/evaluator/murmur_hash/MurmurHash3.h b/providers/flagd/src/evaluator/murmur_hash/MurmurHash3.h index e1c35f2..594c276 100644 --- a/providers/flagd/src/evaluator/murmur_hash/MurmurHash3.h +++ b/providers/flagd/src/evaluator/murmur_hash/MurmurHash3.h @@ -1,6 +1,7 @@ //----------------------------------------------------------------------------- // MurmurHash3 was written by Austin Appleby, and is placed in the public // domain. The author hereby disclaims copyright to this source code. +// Source: https://github.com/aappleby/smhasher/blob/master/src/MurmurHash3.h #ifndef _MURMURHASH3_H_ #define _MURMURHASH3_H_ diff --git a/providers/flagd/src/evaluator/murmur_hash/README.md b/providers/flagd/src/evaluator/murmur_hash/README.md new file mode 100644 index 0000000..d6c4357 --- /dev/null +++ b/providers/flagd/src/evaluator/murmur_hash/README.md @@ -0,0 +1,9 @@ +# MurmurHash3 + +This directory contains an implementation of MurmurHash3, copied from [aappleby/smhasher](https://github.com/aappleby/smhasher/blob/master/src/MurmurHash3.cpp). + +## Origin and License + +All MurmurHash versions are public domain software, and the author (Austin Appleby) disclaims all copyright to their code. + +The code was imported directly as there is no official Bazel package for MurmurHash3 and the full `smhasher` repository contains unnecessary components for this use case.