mirror of
https://github.com/esphome/esphome.git
synced 2026-01-10 12:10:48 -07:00
Add FNV-1a hash functions (#12502)
This commit is contained in:
@@ -143,17 +143,29 @@ uint16_t crc16be(const uint8_t *data, uint16_t len, uint16_t crc, uint16_t poly,
|
||||
return refout ? (crc ^ 0xffff) : crc;
|
||||
}
|
||||
|
||||
// FNV-1 hash - deprecated, use fnv1a_hash() for new code
|
||||
uint32_t fnv1_hash(const char *str) {
|
||||
uint32_t hash = 2166136261UL;
|
||||
uint32_t hash = FNV1_OFFSET_BASIS;
|
||||
if (str) {
|
||||
while (*str) {
|
||||
hash *= 16777619UL;
|
||||
hash *= FNV1_PRIME;
|
||||
hash ^= *str++;
|
||||
}
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
// FNV-1a hash - preferred for new code
|
||||
uint32_t fnv1a_hash_extend(uint32_t hash, const char *str) {
|
||||
if (str) {
|
||||
while (*str) {
|
||||
hash ^= *str++;
|
||||
hash *= FNV1_PRIME;
|
||||
}
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
float random_float() { return static_cast<float>(random_uint32()) / static_cast<float>(UINT32_MAX); }
|
||||
|
||||
// Strings
|
||||
|
||||
@@ -378,9 +378,24 @@ uint16_t crc16be(const uint8_t *data, uint16_t len, uint16_t crc = 0, uint16_t p
|
||||
bool refout = false);
|
||||
|
||||
/// Calculate a FNV-1 hash of \p str.
|
||||
/// Note: FNV-1a (fnv1a_hash) is preferred for new code due to better avalanche characteristics.
|
||||
uint32_t fnv1_hash(const char *str);
|
||||
inline uint32_t fnv1_hash(const std::string &str) { return fnv1_hash(str.c_str()); }
|
||||
|
||||
/// FNV-1 32-bit offset basis
|
||||
constexpr uint32_t FNV1_OFFSET_BASIS = 2166136261UL;
|
||||
/// FNV-1 32-bit prime
|
||||
constexpr uint32_t FNV1_PRIME = 16777619UL;
|
||||
|
||||
/// Extend a FNV-1a hash with additional string data.
|
||||
uint32_t fnv1a_hash_extend(uint32_t hash, const char *str);
|
||||
inline uint32_t fnv1a_hash_extend(uint32_t hash, const std::string &str) {
|
||||
return fnv1a_hash_extend(hash, str.c_str());
|
||||
}
|
||||
/// Calculate a FNV-1a hash of \p str.
|
||||
inline uint32_t fnv1a_hash(const char *str) { return fnv1a_hash_extend(FNV1_OFFSET_BASIS, str); }
|
||||
inline uint32_t fnv1a_hash(const std::string &str) { return fnv1a_hash(str.c_str()); }
|
||||
|
||||
/// Return a random 32-bit unsigned integer.
|
||||
uint32_t random_uint32();
|
||||
/// Return a random float between 0 and 1.
|
||||
|
||||
60
tests/integration/fixtures/fnv1a_hash.yaml
Normal file
60
tests/integration/fixtures/fnv1a_hash.yaml
Normal file
@@ -0,0 +1,60 @@
|
||||
esphome:
|
||||
name: fnv1a-hash-test
|
||||
platformio_options:
|
||||
build_flags:
|
||||
- "-DDEBUG"
|
||||
on_boot:
|
||||
- lambda: |-
|
||||
using esphome::fnv1a_hash;
|
||||
using esphome::fnv1a_hash_extend;
|
||||
|
||||
// Test empty string (should return offset basis)
|
||||
uint32_t hash_empty = fnv1a_hash("");
|
||||
if (hash_empty == 0x811c9dc5) {
|
||||
ESP_LOGI("FNV1A", "empty PASSED");
|
||||
} else {
|
||||
ESP_LOGE("FNV1A", "empty FAILED: 0x%08x != 0x811c9dc5", hash_empty);
|
||||
}
|
||||
|
||||
// Test known FNV-1a hashes
|
||||
uint32_t hash_hello = fnv1a_hash("hello");
|
||||
if (hash_hello == 0x4f9f2cab) {
|
||||
ESP_LOGI("FNV1A", "known_hello PASSED");
|
||||
} else {
|
||||
ESP_LOGE("FNV1A", "known_hello FAILED: 0x%08x != 0x4f9f2cab", hash_hello);
|
||||
}
|
||||
|
||||
uint32_t hash_helloworld = fnv1a_hash("helloworld");
|
||||
if (hash_helloworld == 0x3b9f5c61) {
|
||||
ESP_LOGI("FNV1A", "known_helloworld PASSED");
|
||||
} else {
|
||||
ESP_LOGE("FNV1A", "known_helloworld FAILED: 0x%08x != 0x3b9f5c61", hash_helloworld);
|
||||
}
|
||||
|
||||
// Test fnv1a_hash_extend consistency
|
||||
uint32_t hash1 = fnv1a_hash("hello");
|
||||
hash1 = fnv1a_hash_extend(hash1, "world");
|
||||
uint32_t hash2 = fnv1a_hash("helloworld");
|
||||
|
||||
if (hash1 == hash2) {
|
||||
ESP_LOGI("FNV1A", "extend PASSED");
|
||||
} else {
|
||||
ESP_LOGE("FNV1A", "extend FAILED: 0x%08x != 0x%08x", hash1, hash2);
|
||||
}
|
||||
|
||||
// Test with std::string
|
||||
std::string str1 = "foo";
|
||||
std::string str2 = "bar";
|
||||
uint32_t hash3 = fnv1a_hash(str1);
|
||||
hash3 = fnv1a_hash_extend(hash3, str2);
|
||||
uint32_t hash4 = fnv1a_hash("foobar");
|
||||
|
||||
if (hash3 == hash4) {
|
||||
ESP_LOGI("FNV1A", "string PASSED");
|
||||
} else {
|
||||
ESP_LOGE("FNV1A", "string FAILED: 0x%08x != 0x%08x", hash3, hash4);
|
||||
}
|
||||
|
||||
host:
|
||||
api:
|
||||
logger:
|
||||
69
tests/integration/test_fnv1a_hash.py
Normal file
69
tests/integration/test_fnv1a_hash.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""Integration test for FNV-1a hash functions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fnv1a_hash(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test that FNV-1a hash functions work correctly."""
|
||||
|
||||
test_results = {}
|
||||
all_tests_complete = asyncio.Event()
|
||||
expected_tests = {"empty", "known_hello", "known_helloworld", "extend", "string"}
|
||||
|
||||
def on_log_line(line: str) -> None:
|
||||
"""Capture log lines with test results."""
|
||||
# Strip ANSI escape codes
|
||||
clean_line = re.sub(r"\x1b\[[0-9;]*m", "", line)
|
||||
# Look for our test result messages
|
||||
# Format: "[timestamp][level][FNV1A:line]: test_name PASSED"
|
||||
match = re.search(r"\[FNV1A:\d+\]:\s+(\w+)\s+(PASSED|FAILED)", clean_line)
|
||||
if match:
|
||||
test_name = match.group(1)
|
||||
result = match.group(2)
|
||||
test_results[test_name] = result
|
||||
if set(test_results.keys()) >= expected_tests:
|
||||
all_tests_complete.set()
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=on_log_line),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
device_info = await client.device_info()
|
||||
assert device_info is not None
|
||||
assert device_info.name == "fnv1a-hash-test"
|
||||
|
||||
# Wait for all tests to complete or timeout
|
||||
try:
|
||||
await asyncio.wait_for(all_tests_complete.wait(), timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(f"Tests timed out. Got results for: {set(test_results.keys())}")
|
||||
|
||||
# Verify all tests passed
|
||||
assert "empty" in test_results, "empty string test not found"
|
||||
assert test_results["empty"] == "PASSED", "empty string test failed"
|
||||
|
||||
assert "known_hello" in test_results, "known_hello test not found"
|
||||
assert test_results["known_hello"] == "PASSED", "known_hello test failed"
|
||||
|
||||
assert "known_helloworld" in test_results, "known_helloworld test not found"
|
||||
assert test_results["known_helloworld"] == "PASSED", (
|
||||
"known_helloworld test failed"
|
||||
)
|
||||
|
||||
assert "extend" in test_results, "fnv1a_hash_extend test not found"
|
||||
assert test_results["extend"] == "PASSED", "fnv1a_hash_extend test failed"
|
||||
|
||||
assert "string" in test_results, "std::string test not found"
|
||||
assert test_results["string"] == "PASSED", "std::string test failed"
|
||||
Reference in New Issue
Block a user