From 8670523ddf0327bea287e78ec105ed0b8cf09dff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 28 Aug 2026 04:47:03 +0000 Subject: [PATCH 1/2] chore(deps): bump sha1 in the cargo-dependencies group Bumps the cargo-dependencies group with 1 update: [sha1](https://github.com/RustCrypto/hashes). Updates `sha1` from 0.10.7 to 0.11.0 - [Commits](https://github.com/RustCrypto/hashes/compare/sha1-v0.10.7...sha1-v0.11.0) --- updated-dependencies: - dependency-name: sha1 dependency-version: 0.11.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: cargo-dependencies ... Signed-off-by: dependabot[bot] --- Cargo.lock | 65 +++++++++++++++++++++++++++++++++++++++++++++++++----- Cargo.toml | 2 +- 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 604f61b..4de3023 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -227,6 +227,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + [[package]] name = "block2" version = "0.6.2" @@ -357,6 +366,12 @@ version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "core-foundation" version = "0.10.1" @@ -416,6 +431,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + [[package]] name = "ctor" version = "1.0.13" @@ -498,8 +522,19 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid", + "crypto-common 0.2.2", ] [[package]] @@ -805,7 +840,7 @@ dependencies = [ "http", "httpdate", "mime", - "sha1", + "sha1 0.10.7", ] [[package]] @@ -896,6 +931,15 @@ dependencies = [ "url", ] +[[package]] +name = "hybrid-array" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "1.11.0" @@ -1590,7 +1634,7 @@ dependencies = [ "semver", "serde", "serde_json", - "sha1", + "sha1 0.11.0", "tokio", "tracing", "tracing-subscriber", @@ -2063,7 +2107,18 @@ checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "digest 0.10.7", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 89a6c7e..e5701a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ uuid = { version = "1.13.2", features = ["serde", "v7"] } backtrace = { version = "0.3.74", optional = true } findshlibs = { version = "0.10", optional = true } os_info = "3.14" -sha1 = "0.10" +sha1 = "0.11" regex = "1.10" tracing = "0.1" tokio = { version = "1", features = [ From b171343f8b9f2320dece087b9e2595846ae3eb65 Mon Sep 17 00:00:00 2001 From: Anna Garcia Date: Fri, 28 Aug 2026 11:41:36 -0400 Subject: [PATCH 2/2] fix(flags): compute bucketing hash without hex formatting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sha1 0.11 moves off `generic-array` onto `hybrid-array`, and the `Array` that `finalize()` now returns does not implement `LowerHex`, so `format!("{result:x}")` no longer compiles. Read the digest's top 60 bits directly instead. `hexdigest()[:15]` is 15 nibbles, so `from_be_bytes(first 8 bytes) >> 4` is the same integer, and bucketing stays bit-identical to the other SDKs. Pin that with vectors generated from the Python SDK's algorithm — the existing test only asserted self-consistency, so it would have passed even if every user had been rebucketed. Co-Authored-By: Claude Opus 5 (1M context) --- src/feature_flags.rs | 42 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/src/feature_flags.rs b/src/feature_flags.rs index 466c652..f993e7b 100644 --- a/src/feature_flags.rs +++ b/src/feature_flags.rs @@ -470,8 +470,11 @@ pub fn hash_key(key: &str, distinct_id: &str, salt: &str) -> f64 { let mut hasher = Sha1::new(); hasher.update(hash_key.as_bytes()); let result = hasher.finalize(); - let hex_str = format!("{result:x}"); - let hash_val = u64::from_str_radix(&hex_str[..15], 16).unwrap_or(0); + // The top 60 bits of the digest, i.e. the integer value of its first 15 hex + // digits. Matches the Python SDK's `int(sha1(...).hexdigest()[:15], 16)`. + let hash_val = result + .first_chunk::<8>() + .map_or(0, |head| u64::from_be_bytes(*head) >> 4); hash_val as f64 / LONG_SCALE } @@ -1656,6 +1659,41 @@ mod tests { assert_ne!(hash, hash3); } + /// Bucketing must stay bit-identical across SDKs, so pin known values. + /// Generated with the Python SDK's algorithm: + /// `int(hashlib.sha1(f"{key}.{distinct_id}{salt}".encode()).hexdigest()[:15], 16) / 0xfffffffffffffff` + #[test] + fn test_hash_key_matches_known_vectors() { + for (key, distinct_id, salt, expected) in [ + ("test-flag", "user-123", TEST_SALT, 0.982_062_667_408_254_5), + ("test-flag", "user-456", TEST_SALT, 0.695_145_973_300_181_1), + ( + "beta-feature", + "distinct_id", + ROLLOUT_HASH_SALT, + 0.875_596_347_947_407_8, + ), + ( + "beta-feature", + "distinct_id", + VARIANT_HASH_SALT, + 0.228_302_715_824_090_7, + ), + ( + "multivariate-flag", + "user_1", + ROLLOUT_HASH_SALT, + 0.223_607_742_058_685_7, + ), + ] { + assert_eq!( + hash_key(key, distinct_id, salt), + expected, + "hash_key({key:?}, {distinct_id:?}, {salt:?}) drifted from the other SDKs" + ); + } + } + #[test] fn test_simple_flag_match() { let flag = FeatureFlag {