@@ -142,7 +142,9 @@ class SemanticVersion {
142142 return absl::InvalidArgumentError (" Empty pre-release identifier" );
143143 }
144144
145- bool is_numeric = std::all_of (ident.begin (), ident.end (), ::isdigit);
145+ bool is_numeric =
146+ std::all_of (ident.begin (), ident.end (),
147+ [](unsigned char chr) { return std::isdigit (chr); });
146148 if (is_numeric && HasLeadingZero (ident)) {
147149 return absl::InvalidArgumentError (
148150 " Numeric pre-release identifiers MUST NOT contain leading zeros" );
@@ -174,22 +176,19 @@ class SemanticVersion {
174176
175177 // Numeric identifiers have lower precedence than non-numeric identifiers.
176178 bool lhs_is_num =
177- std::all_of (lhs_part.begin (), lhs_part.end (), ::isdigit);
179+ std::all_of (lhs_part.begin (), lhs_part.end (),
180+ [](unsigned char c) { return std::isdigit (c); });
178181 bool rhs_is_num =
179- std::all_of (rhs_part.begin (), rhs_part.end (), ::isdigit);
182+ std::all_of (rhs_part.begin (), rhs_part.end (),
183+ [](unsigned char c) { return std::isdigit (c); });
180184
181185 if (lhs_is_num && rhs_is_num) {
182- uint64_t lhs_num;
183- uint64_t rhs_num;
184- // Numeric identifiers are compared numerically.
185- if (absl::SimpleAtoi (lhs_part, &lhs_num) &&
186- absl::SimpleAtoi (rhs_part, &rhs_num)) {
187- if (lhs_num != rhs_num) return lhs_num < rhs_num ? -1 : 1 ;
188- } else {
189- // Fallback to lexicographical comparison if parsing fails (e.g.,
190- // overflow)
191- if (lhs_part != rhs_part) return lhs_part < rhs_part ? -1 : 1 ;
186+ // Compare numerically by length first, then lexicographically.
187+ // This supports arbitrary precision integers without overflow.
188+ if (lhs_part.length () != rhs_part.length ()) {
189+ return lhs_part.length () < rhs_part.length () ? -1 : 1 ;
192190 }
191+ if (lhs_part != rhs_part) return lhs_part < rhs_part ? -1 : 1 ;
193192 } else if (lhs_is_num && !rhs_is_num) {
194193 return -1 ;
195194 } else if (!lhs_is_num && rhs_is_num) {
@@ -216,6 +215,121 @@ struct Distribution {
216215 int32_t weight;
217216};
218217
218+ struct FractionalContext {
219+ std::vector<Distribution> distributions;
220+ uint64_t sum_of_weights;
221+ };
222+
223+ // Resolves the bucketing value based on the rule and data.
224+ // If the first argument evaluates to a string, it's used as the bucketing
225+ // property. Otherwise, it falls back to flagKey + targetingKey.
226+ absl::StatusOr<std::string> ResolveBucketingValue (
227+ const json_logic::JsonLogic& eval, const nlohmann::json& values,
228+ const nlohmann::json& data, bool & first_value_used) {
229+ absl::StatusOr<nlohmann::json> bucketing_property_eval =
230+ eval.Apply (values[0 ], data);
231+ if (!bucketing_property_eval.ok ()) return bucketing_property_eval.status ();
232+
233+ if (bucketing_property_eval.value ().is_string ()) {
234+ first_value_used = true ;
235+ return bucketing_property_eval.value ().get <std::string>();
236+ }
237+
238+ first_value_used = false ;
239+ // Fallback logic from spec: Concatenate flagKey and targetingKey if property
240+ // is missing
241+ std::string flag_key;
242+ if (data.contains (" $flagd" ) && data[" $flagd" ].is_object () &&
243+ data[" $flagd" ].contains (" flagKey" ) &&
244+ data[" $flagd" ][" flagKey" ].is_string ()) {
245+ flag_key = data[" $flagd" ][" flagKey" ].get <std::string>();
246+ }
247+
248+ std::string targeting_key;
249+ if (data.contains (" targetingKey" ) && data[" targetingKey" ].is_string ()) {
250+ targeting_key = data[" targetingKey" ].get <std::string>();
251+ }
252+ return absl::StrCat (flag_key, targeting_key);
253+ }
254+
255+ // Parses the distributions from the values array.
256+ absl::StatusOr<FractionalContext> ParseDistributions (
257+ const json_logic::JsonLogic& eval, const nlohmann::json& values,
258+ const nlohmann::json& data, bool first_value_used) {
259+ std::vector<Distribution> distributions;
260+ uint64_t sum_of_weights = 0 ;
261+
262+ for (size_t i = first_value_used ? 1 : 0 ; i < values.size (); i++) {
263+ absl::StatusOr<nlohmann::json> item = eval.Apply (values[i], data);
264+ if (!item.ok ()) return item.status ();
265+ if (!item.value ().is_array () || item.value ().empty ()) {
266+ return absl::InvalidArgumentError (" Invalid distribution element" );
267+ }
268+
269+ if (!item.value ()[0 ].is_string ()) {
270+ return absl::InvalidArgumentError (" Variant name must be a string" );
271+ }
272+
273+ int32_t weight = 1 ;
274+ if (item.value ().size () >= 2 && item.value ()[1 ].is_number ()) {
275+ weight = item.value ()[1 ].get <int32_t >();
276+ if (weight < 0 ) {
277+ return absl::InvalidArgumentError (" Weight must be non-negative." );
278+ }
279+ }
280+
281+ distributions.push_back ({item.value ()[0 ].get <std::string>(), weight});
282+ sum_of_weights += weight;
283+ }
284+
285+ if (distributions.empty ()) {
286+ return absl::InvalidArgumentError (" No distributions found" );
287+ }
288+
289+ if (sum_of_weights == 0 ) {
290+ return absl::InvalidArgumentError (" Sum of weights must be positive" );
291+ }
292+
293+ if (sum_of_weights >=
294+ static_cast <uint64_t >(std::numeric_limits<int32_t >::max ())) {
295+ return absl::InvalidArgumentError (" Sum of weights exceeds maximum limit" );
296+ }
297+
298+ return FractionalContext{std::move (distributions), sum_of_weights};
299+ }
300+
301+ // Calculates the hash value for the given input using MurmurHash3.
302+ absl::StatusOr<uint32_t > CalculateHash (const std::string& input) {
303+ if (input.length () > static_cast <size_t >(std::numeric_limits<int >::max ())) {
304+ return absl::InvalidArgumentError (
305+ " Input string is too long for MurmurHash3" );
306+ }
307+ uint32_t hash_value;
308+ MurmurHash3_x86_32 (input.data (), static_cast <int >(input.length ()), 0 ,
309+ &hash_value);
310+ return hash_value;
311+ }
312+
313+ // Calculates the bucket and selects the variant based on the hash value.
314+ absl::StatusOr<std::string> SelectVariant (
315+ const std::vector<Distribution>& distributions, uint64_t sum_of_weights,
316+ uint32_t hash_value) {
317+ // High-precision bucketing using 64-bit math to distribute hash over
318+ // sum_of_weights
319+ uint64_t bucket = (static_cast <uint64_t >(hash_value) * sum_of_weights) >>
320+ std::numeric_limits<uint32_t >::digits;
321+
322+ uint64_t range_end = 0 ;
323+ for (const Distribution& dist : distributions) {
324+ range_end += dist.weight ;
325+ if (bucket < range_end) {
326+ return dist.variant ;
327+ }
328+ }
329+
330+ return absl::InternalError (" Fractional bucketing failed to find a variant" );
331+ }
332+
219333} // namespace
220334
221335absl::StatusOr<nlohmann::json> StartsWith (const json_logic::JsonLogic& eval,
@@ -271,8 +385,19 @@ absl::StatusOr<nlohmann::json> SemVer(const json_logic::JsonLogic& eval,
271385 if (operation == " <" ) return cmp < 0 ;
272386 if (operation == " >=" ) return cmp >= 0 ;
273387 if (operation == " <=" ) return cmp <= 0 ;
274- if (operation == " ^" ) return ver1.GetMajor () == ver2.GetMajor ();
388+ if (operation == " ^" ) {
389+ if (ver1.Compare (ver2) < 0 ) return false ;
390+ if (ver2.GetMajor () > 0 ) {
391+ return ver1.GetMajor () == ver2.GetMajor ();
392+ }
393+ if (ver2.GetMinor () > 0 ) {
394+ return ver1.GetMajor () == 0 && ver1.GetMinor () == ver2.GetMinor ();
395+ }
396+ return ver1.GetMajor () == 0 && ver1.GetMinor () == 0 &&
397+ ver1.GetPatch () == ver2.GetPatch ();
398+ }
275399 if (operation == " ~" ) {
400+ if (ver1.Compare (ver2) < 0 ) return false ;
276401 return ver1.GetMajor () == ver2.GetMajor () &&
277402 ver1.GetMinor () == ver2.GetMinor ();
278403 }
@@ -294,95 +419,24 @@ absl::StatusOr<nlohmann::json> Fractional(const json_logic::JsonLogic& eval,
294419 " fractional evaluation data has length under 2" );
295420 }
296421
297- // 1. Get the target property value used for bucketing the values
298- absl::StatusOr<nlohmann::json> bucketing_property_eval =
299- eval.Apply (values[0 ], data);
300- if (!bucketing_property_eval.ok ()) return bucketing_property_eval.status ();
301-
302- std::string bucketing_property_value;
303422 bool first_value_used = false ;
304- if (bucketing_property_eval.value ().is_string ()) {
305- bucketing_property_value =
306- bucketing_property_eval.value ().get <std::string>();
307- first_value_used = true ;
308- } else {
309- // Fallback logic from spec: Concatenate flagKey and targetingKey if
310- // property is missing
311- std::string flag_key;
312- if (data.contains (" $flagd" ) && data[" $flagd" ].is_object () &&
313- data[" $flagd" ].contains (" flagKey" ) &&
314- data[" $flagd" ][" flagKey" ].is_string ()) {
315- flag_key = data[" $flagd" ][" flagKey" ].get <std::string>();
316- }
317-
318- std::string targeting_key;
319- if (data.contains (" targetingKey" ) && data[" targetingKey" ].is_string ()) {
320- targeting_key = data[" targetingKey" ].get <std::string>();
321- }
322- bucketing_property_value = absl::StrCat (flag_key, targeting_key);
323- }
324-
325- // 2. Parse the fractional distribution
326- std::vector<Distribution> distributions;
327- uint64_t sum_of_weights = 0 ;
328-
329- for (size_t i = first_value_used ? 1 : 0 ; i < values.size (); i++) {
330- absl::StatusOr<nlohmann::json> item = eval.Apply (values[i], data);
331- if (!item.ok ()) return item.status ();
332- if (!item.value ().is_array () || item.value ().empty ()) {
333- return absl::InvalidArgumentError (" Invalid distribution element" );
334- }
335-
336- if (!item.value ()[0 ].is_string ()) {
337- return absl::InvalidArgumentError (" Variant name must be a string" );
338- }
339-
340- int32_t weight = 1 ;
341- if (item.value ().size () >= 2 && item.value ()[1 ].is_number ()) {
342- weight = item.value ()[1 ].get <int32_t >();
343- if (weight < 0 ) {
344- return absl::InvalidArgumentError (" Weight must be non-negative." );
345- }
346- }
423+ absl::StatusOr<std::string> bucketing_property_res =
424+ ResolveBucketingValue (eval, values, data, first_value_used);
425+ if (!bucketing_property_res.ok ()) return bucketing_property_res.status ();
347426
348- distributions. push_back ({item. value ()[ 0 ]. get <std::string>(), weight});
349- sum_of_weights += weight ;
350- }
427+ absl::StatusOr<FractionalContext> context_res =
428+ ParseDistributions (eval, values, data, first_value_used) ;
429+ if (!context_res. ok ()) return context_res. status ();
351430
352- if (distributions. empty ()) {
353- return absl::InvalidArgumentError ( " No distributions found " );
354- }
431+ absl::StatusOr< uint32_t > hash_res =
432+ CalculateHash (bucketing_property_res. value () );
433+ if (!hash_res. ok ()) return hash_res. status ();
355434
356- if (sum_of_weights == 0 ) {
357- return absl::InvalidArgumentError ( " Sum of weights must be positive " );
358- }
435+ absl::StatusOr<std::string> variant_res = SelectVariant (
436+ context_res-> distributions , context_res-> sum_of_weights , hash_res. value () );
437+ if (!variant_res. ok ()) return variant_res. status ();
359438
360- if (sum_of_weights >=
361- static_cast <uint64_t >(std::numeric_limits<int32_t >::max ())) {
362- return absl::InvalidArgumentError (" Sum of weights exceeds maximum limit" );
363- }
364-
365- // 3. Calculate hash and determine bucket
366- uint32_t hash_value;
367- MurmurHash3_x86_32 (bucketing_property_value.data (),
368- static_cast <int >(bucketing_property_value.length ()), 0 ,
369- &hash_value);
370-
371- // High-precision bucketing using 64-bit math to distribute hash over
372- // sum_of_weights
373- uint64_t bucket = (static_cast <uint64_t >(hash_value) * sum_of_weights) >>
374- std::numeric_limits<uint32_t >::digits;
375-
376- // 4. Return the variant corresponding to the bucket
377- uint64_t range_end = 0 ;
378- for (const Distribution& dist : distributions) {
379- range_end += dist.weight ;
380- if (bucket < range_end) {
381- return dist.variant ;
382- }
383- }
384-
385- return absl::InternalError (" Fractional bucketing failed to find a variant" );
439+ return variant_res.value ();
386440}
387441
388442} // namespace flagd
0 commit comments