From 441feadb8b781d41284810bb6d49ace29a9a1da8 Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Tue, 24 Jun 2025 14:36:29 +0300 Subject: [PATCH 1/8] feat(yahash): implemeted `Hash64` and `Hash64WithTime` --- yahash/yahash.go | 34 ++++++++++++++++++ yahash/yahash_test.go | 81 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 yahash/yahash.go create mode 100644 yahash/yahash_test.go diff --git a/yahash/yahash.go b/yahash/yahash.go new file mode 100644 index 0000000..7ff78d3 --- /dev/null +++ b/yahash/yahash.go @@ -0,0 +1,34 @@ +package yahash + +import ( + "hash/fnv" + "time" +) + +func Hash64WithTime(date time.Time, args ...string) int64 { + return Hash64(date.Format(time.DateOnly), args...) +} + +func Hash64(data string, args ...string) int64 { + hasher := fnv.New64a() + hasher.Write([]byte(data)) + + for _, arg := range args { + hasher.Write([]byte(arg)) + } + + return int64(hasher.Sum64()) +} + +func ValidateHash64ByDays(expectedHash int64, daysBack int, args ...string) bool { + for i := 0; i <= daysBack; i++ { + date := time.Now().AddDate(0, 0, -i) + generatedHash := Hash64WithTime(date, args...) + + if generatedHash == expectedHash { + return true + } + } + + return false +} diff --git a/yahash/yahash_test.go b/yahash/yahash_test.go new file mode 100644 index 0000000..898bdb3 --- /dev/null +++ b/yahash/yahash_test.go @@ -0,0 +1,81 @@ +package yahash_test + +import ( + "fmt" + "testing" + "time" + + "github.com/YaCodeDev/GoYaCodeDevUtils/yahash" + "github.com/stretchr/testify/assert" +) + +var testDataForHash = []string{"yadatetestlolkek", "polliizz", "yanevlad_"} + +func TestHash64_Deterministic(t *testing.T) { + yadata := "yadata" + + hash1 := yahash.Hash64(yadata, testDataForHash...) + hash2 := yahash.Hash64(yadata, testDataForHash...) + + assert.Equal(t, hash1, hash2, fmt.Sprintf("Hash64 not deterministic: got %d and %d", hash1, hash2)) +} + +func TestHash64WithTime_Deterministic(t *testing.T) { + hash1 := yahash.Hash64WithTime(time.Now(), testDataForHash...) + hash2 := yahash.Hash64WithTime(time.Now(), testDataForHash...) + + assert.Equal(t, hash1, hash2, fmt.Sprintf("Hash64WithTime not deterministic: got %d and %d", hash1, hash2)) +} + +func TestHash64_Matches_Hash64WithTime(t *testing.T) { + hash64 := yahash.Hash64(time.Now().Format(time.DateOnly), testDataForHash...) + hash64WithTime := yahash.Hash64WithTime(time.Now(), testDataForHash...) + + assert.Equal(t, hash64, hash64WithTime, + fmt.Sprintf("Hash64 doesn't match to Hash64WithTime. hash64: %d, hash64WithTime: %d", hash64, hash64WithTime)) +} + +func TestValidateHash64ByDays_Today(t *testing.T) { + t.Parallel() + + t.Run("Today", func(t *testing.T) { + todayHash := yahash.Hash64(time.Now().Format(time.DateOnly), testDataForHash...) + daysBack := 1 + + assert.True(t, yahash.ValidateHash64ByDays(todayHash, daysBack, testDataForHash...), + "Failed to validate correct hash") + }) + + t.Run("Yesterday", func(t *testing.T) { + hashYesterday := yahash.Hash64WithTime(time.Now().AddDate(0, 0, -1), testDataForHash...) + + t.Run("True", func(t *testing.T) { + daysBack := 1 + + assert.True(t, yahash.ValidateHash64ByDays(hashYesterday, daysBack, testDataForHash...), + "Got `False` by valid hash64") + }) + + t.Run("False", func(t *testing.T) { + daysBack := 0 + + assert.False(t, yahash.ValidateHash64ByDays(hashYesterday, daysBack, testDataForHash...), + "Got `True` by invalid hash64") + }) + }) + + t.Run("Tomorrow Day", func(t *testing.T) { + hash := yahash.Hash64WithTime(time.Now().AddDate(0, 0, 1), testDataForHash...) + daysBack := 1 + + assert.False(t, yahash.ValidateHash64ByDays(hash, daysBack, testDataForHash...), + "Got `True` by invalid hash64 because tommorow day") + }) + + t.Run("Invalid Date", func(t *testing.T) { + hash := yahash.Hash64WithTime(time.Now().AddDate(0, 0, -3), testDataForHash...) + + assert.False(t, yahash.ValidateHash64ByDays(hash, 1, testDataForHash...), + "Got `True` by invalid hash64 because old date") + }) +} From 1c50a37289a014a7b93e22ccfc14e01e1f28fead Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Tue, 24 Jun 2025 14:44:19 +0300 Subject: [PATCH 2/8] chore(yahash): correct log failed test --- yahash/yahash_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yahash/yahash_test.go b/yahash/yahash_test.go index 898bdb3..015b40b 100644 --- a/yahash/yahash_test.go +++ b/yahash/yahash_test.go @@ -69,7 +69,7 @@ func TestValidateHash64ByDays_Today(t *testing.T) { daysBack := 1 assert.False(t, yahash.ValidateHash64ByDays(hash, daysBack, testDataForHash...), - "Got `True` by invalid hash64 because tommorow day") + "Got `True` by invalid hash64 because tomorrow day") }) t.Run("Invalid Date", func(t *testing.T) { From 02dbea757d57116f5f0d2ccc3316dcef9837278a Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Wed, 25 Jun 2025 02:50:37 +0300 Subject: [PATCH 3/8] feat(yahash): implemented hash with time checking --- yahash/yahash.go | 83 +++++++++++++++++++++++++++++++------ yahash/yahash_test.go | 96 +++++++++++++++++++++++++------------------ 2 files changed, 126 insertions(+), 53 deletions(-) diff --git a/yahash/yahash.go b/yahash/yahash.go index 7ff78d3..875c706 100644 --- a/yahash/yahash.go +++ b/yahash/yahash.go @@ -2,33 +2,90 @@ package yahash import ( "hash/fnv" + "strconv" "time" + + "github.com/YaCodeDev/GoYaCodeDevUtils/valueparser" ) -func Hash64WithTime(date time.Time, args ...string) int64 { - return Hash64(date.Format(time.DateOnly), args...) +type HashableType valueparser.ParsableType + +type HashFunc[I HashableType, O comparable] func(data I, args ...I) O + +type Hash[I HashableType, O comparable] struct { + hash HashFunc[I, O] + interval time.Duration + secret I + back int } -func Hash64(data string, args ...string) int64 { - hasher := fnv.New64a() - hasher.Write([]byte(data)) +func NewHash[I HashableType, O comparable]( + hash HashFunc[I, O], + secret I, + interval time.Duration, + back int, +) Hash[I, O] { + if interval < time.Second { + interval = time.Second + } - for _, arg := range args { - hasher.Write([]byte(arg)) + return Hash[I, O]{ + hash: hash, + secret: secret, + interval: interval, + back: back, } +} - return int64(hasher.Sum64()) +func (h *Hash[I, O]) Hash(data I, args ...I) O { + return h.hash(data, append(args, h.secret)...) +} + +func (h *Hash[I, O]) HashWithTime(inputTime time.Time, args ...I) O { + parsedTime, _ := valueparser. + ParseValue[I]( + strconv.FormatInt(inputTime.Unix()/int64(h.interval/time.Second), 10)) // SAFETY: This cannot return error + + return h.hash(parsedTime, append(args, h.secret)...) } -func ValidateHash64ByDays(expectedHash int64, daysBack int, args ...string) bool { - for i := 0; i <= daysBack; i++ { - date := time.Now().AddDate(0, 0, -i) - generatedHash := Hash64WithTime(date, args...) +func (h *Hash[I, O]) ValidateWithoutTime(expected O, data I, args ...I) bool { + return h.Hash(data, args...) == expected +} + +func (h *Hash[I, O]) Validate(expected O, args ...I) bool { + for i := 0; i <= h.back; i++ { + date := time.Now().Add(h.interval * -time.Duration(i)) + generated := h.HashWithTime(date, args...) - if generatedHash == expectedHash { + if generated == expected { return true } } return false } + +func (h *Hash[I, O]) ValidateCustomBack(expected O, back int, args ...I) bool { + for i := 0; i <= back; i++ { + date := time.Now().Add(h.interval * -time.Duration(i)) + generated := h.HashWithTime(date, args...) + + if generated == expected { + return true + } + } + + return false +} + +func FNVStringToInt64(data string, args ...string) int64 { + hasher := fnv.New64() + hasher.Write([]byte(data)) + + for _, arg := range args { + hasher.Write([]byte(arg)) + } + + return int64(hasher.Sum64()) +} diff --git a/yahash/yahash_test.go b/yahash/yahash_test.go index 015b40b..bd4a6c3 100644 --- a/yahash/yahash_test.go +++ b/yahash/yahash_test.go @@ -2,6 +2,7 @@ package yahash_test import ( "fmt" + "strconv" "testing" "time" @@ -9,73 +10,88 @@ import ( "github.com/stretchr/testify/assert" ) -var testDataForHash = []string{"yadatetestlolkek", "polliizz", "yanevlad_"} +var ( + testDataForHash = []string{"yadatetestlolkek", "polliizz", "yanevlad_"} + secret = "yanesupertestsecret" + testHash = yahash.NewHash(yahash.FNVStringToInt64, secret, time.Hour, 5) +) -func TestHash64_Deterministic(t *testing.T) { - yadata := "yadata" +func TestHash64_DeterministicWorks(t *testing.T) { + data := "yadata" - hash1 := yahash.Hash64(yadata, testDataForHash...) - hash2 := yahash.Hash64(yadata, testDataForHash...) + hash1 := testHash.Hash(data, testDataForHash...) + hash2 := testHash.Hash(data, testDataForHash...) - assert.Equal(t, hash1, hash2, fmt.Sprintf("Hash64 not deterministic: got %d and %d", hash1, hash2)) + assert.Equal(t, hash1, hash2, fmt.Sprintf("Hash not deterministic: got %d and %d", hash1, hash2)) } -func TestHash64WithTime_Deterministic(t *testing.T) { - hash1 := yahash.Hash64WithTime(time.Now(), testDataForHash...) - hash2 := yahash.Hash64WithTime(time.Now(), testDataForHash...) +func TestHash64WithTime_DeterministicWorks(t *testing.T) { + hash1 := testHash.HashWithTime(time.Now(), testDataForHash...) + hash2 := testHash.HashWithTime(time.Now(), testDataForHash...) assert.Equal(t, hash1, hash2, fmt.Sprintf("Hash64WithTime not deterministic: got %d and %d", hash1, hash2)) } -func TestHash64_Matches_Hash64WithTime(t *testing.T) { - hash64 := yahash.Hash64(time.Now().Format(time.DateOnly), testDataForHash...) - hash64WithTime := yahash.Hash64WithTime(time.Now(), testDataForHash...) +func TestHash_Matches_HashWithTime(t *testing.T) { + hash := testHash.Hash( + strconv.FormatInt(time.Now().Unix()/int64(time.Hour/time.Second), 10), testDataForHash..., + ) + hashWithTime := testHash.HashWithTime(time.Now(), testDataForHash...) - assert.Equal(t, hash64, hash64WithTime, - fmt.Sprintf("Hash64 doesn't match to Hash64WithTime. hash64: %d, hash64WithTime: %d", hash64, hash64WithTime)) + assert.Equal(t, hash, hashWithTime, + fmt.Sprintf("Hash64 doesn't match to Hash64WithTime. hash64: %d, hash64WithTime: %d", hash, hashWithTime)) } -func TestValidateHash64ByDays_Today(t *testing.T) { +func TestValidateHash_Works(t *testing.T) { t.Parallel() - t.Run("Today", func(t *testing.T) { - todayHash := yahash.Hash64(time.Now().Format(time.DateOnly), testDataForHash...) - daysBack := 1 - - assert.True(t, yahash.ValidateHash64ByDays(todayHash, daysBack, testDataForHash...), - "Failed to validate correct hash") - }) - - t.Run("Yesterday", func(t *testing.T) { - hashYesterday := yahash.Hash64WithTime(time.Now().AddDate(0, 0, -1), testDataForHash...) + t.Run("[Validate] Works", func(t *testing.T) { + hash := yahash.NewHash(yahash.FNVStringToInt64, secret, time.Second, 5) t.Run("True", func(t *testing.T) { - daysBack := 1 + expected := hash.HashWithTime(time.Now().Add(-time.Second*4), testDataForHash...) - assert.True(t, yahash.ValidateHash64ByDays(hashYesterday, daysBack, testDataForHash...), - "Got `False` by valid hash64") + assert.True(t, hash.Validate(expected, testDataForHash...), + "Got `True` by valid hash with correct date") }) t.Run("False", func(t *testing.T) { - daysBack := 0 + expected := hash.HashWithTime(time.Now().Add(-time.Second*7), testDataForHash...) - assert.False(t, yahash.ValidateHash64ByDays(hashYesterday, daysBack, testDataForHash...), - "Got `True` by invalid hash64") + assert.False(t, hash.Validate(expected, testDataForHash...), + "Got `True` by invalid hash with non correct date") }) }) - t.Run("Tomorrow Day", func(t *testing.T) { - hash := yahash.Hash64WithTime(time.Now().AddDate(0, 0, 1), testDataForHash...) - daysBack := 1 + t.Run("[ValidateWithoutTime] Works", func(t *testing.T) { + data := "brizzinck" + + expected := testHash.Hash(data, testDataForHash...) + + t.Run("True", func(t *testing.T) { + assert.True(t, testHash.ValidateWithoutTime(expected, data, testDataForHash...), + "Got `False` by valid hash without time") + }) - assert.False(t, yahash.ValidateHash64ByDays(hash, daysBack, testDataForHash...), - "Got `True` by invalid hash64 because tomorrow day") + t.Run("False", func(t *testing.T) { + assert.False(t, testHash.ValidateWithoutTime(expected, data+"s", testDataForHash...), + "Got `True` by invalid hash without time") + }) }) - t.Run("Invalid Date", func(t *testing.T) { - hash := yahash.Hash64WithTime(time.Now().AddDate(0, 0, -3), testDataForHash...) + t.Run("[ValidateCustomBack]", func(t *testing.T) { + t.Run("True", func(t *testing.T) { + expected := testHash.HashWithTime(time.Now().Add(-time.Hour*6), testDataForHash...) + + assert.True(t, testHash.ValidateCustomBack(expected, 7, testDataForHash...), + "Got `False` by valid hash with correct date") + }) + + t.Run("False", func(t *testing.T) { + expected := testHash.HashWithTime(time.Now().Add(-time.Hour*16), testDataForHash...) - assert.False(t, yahash.ValidateHash64ByDays(hash, 1, testDataForHash...), - "Got `True` by invalid hash64 because old date") + assert.False(t, testHash.ValidateCustomBack(expected, 10, testDataForHash...), + "Got `True` by invalid hash with non correct date") + }) }) } From a5df3058dcef3047b3f5ab58705443a80e2073f0 Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Wed, 25 Jun 2025 03:03:09 +0300 Subject: [PATCH 4/8] feat(yahash): add `docs` for hasher --- yahash/yahash.go | 177 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 176 insertions(+), 1 deletion(-) diff --git a/yahash/yahash.go b/yahash/yahash.go index 875c706..a3c6fbb 100644 --- a/yahash/yahash.go +++ b/yahash/yahash.go @@ -1,3 +1,65 @@ +// Package yahash provides a small, generic helper around hashing functions that +// makes it trivial to combine arbitrary user‑supplied data with a secret (salt) +// **and** a rolling time component. +// +// ## Typical use‑case +// +// - Build short‑lived tokens or "request signatures" that must be recomputed on +// the server side and validated within an allowed time window. +// +// - Generate cache‑keys that expire automatically when the configured +// `interval` elapses. +// +// - Quickly protect a webhook or URL with a deterministic but time‑bounded +// hash without the overhead of full‑blown JWT or HMAC libraries. +// +// The API is intentionally minimal: you bring **any** hashing algorithm (as a +// `HashFunc`) and the helper takes care of salting it with the secret and with +// a truncated Unix‑timestamp. +// +// # Example (basic, secret‑only) +// +// The simplest scenario hashes an arbitrary string together with a secret: +// +// package main +// +// import ( +// "fmt" +// "time" +// +// "github.com/YaCodeDev/GoYaCodeDevUtils/yahash" +// ) +// +// func main() { +// hasher := yahash.NewHash[yahash.HashableType, int64]( +// yahash.FNVStringToInt64, +// "my‑super‑secret", // salt +// time.Minute, // irrelevant here, no time component +// 0, // no backwards validation window +// ) +// +// h := hasher.Hash("payload") +// fmt.Println(h) +// } +// +// # Example (time‑bound validation) +// +// secret := "yanesupertestsecret" +// data := []string{"yadatetestlolkek", "polliizz", "yanevlad_"} +// +// // Create a token valid for five one‑hour periods back. +// hasher := yahash.NewHash(yahash.FNVStringToInt64, secret, time.Hour, 5) +// +// // The client computes a hash for "now". +// expected := hasher.HashWithTime(time.Now(), data...) +// +// // The server receives `expected` and validates it – it will compare against +// // the current period and the previous five. +// if ok := hasher.Validate(expected, data...); !ok { +// // reject request +// } +// +// ---------------------------------------------------------------------------------- package yahash import ( @@ -8,17 +70,76 @@ import ( "github.com/YaCodeDev/GoYaCodeDevUtils/valueparser" ) +// HashableType describes the set of types that can be parsed from / converted to +// a string by the *valueparser* package **and** that you intend to feed into the +// supplied hashing function. In practice it will usually resolve to `string`, +// `int64`, or another scalar type supported by your parser. type HashableType valueparser.ParsableType +// HashFunc is the signature every hashing algorithm must satisfy in order to be +// used with `Hash`. +// +// - *I* – the input type (usually `string`). +// - *O* – the output type **must** be `comparable` so that we can check equality +// when validating. +// +// A hash function receives the main *data* plus zero or more *args* that are +// already salted with the secret (see implementation) – so it can simply write +// them into its internal state. +// +// For example, `FNVStringToInt64` below fulfils this contract. +// +// # Example +// +// custom := func(s string, args ...string) uint32 { +// h := fnv.New32() +// h.Write([]byte(s)) +// for _, a := range args { h.Write([]byte(a)) } +// return h.Sum32() +// } +// +// _ = yahash.NewHash(custom, "secret", time.Second, 3) +// +// (note that `uint32` is *comparable*, so it is allowed). type HashFunc[I HashableType, O comparable] func(data I, args ...I) O +// Hash bundles a hashing function together with: +// +// - *secret* – an extra argument automatically appended to every call so the +// output cannot be reproduced without knowing it (simple salting). +// +// - *interval* – the size of a time‑window; +// +// - *back* – how many *previous* windows are accepted during validation +// +// In other words, the triple *interval / back / secret* defines the security +// model while `HashFunc` defines the actual mathematical transform. +// +// The zero value is **not** usable; always construct via `NewHash`. type Hash[I HashableType, O comparable] struct { hash HashFunc[I, O] - interval time.Duration + interval time.Duration // ≥ 1s after constructor check secret I back int } +// NewHash returns an initialised Hash helper. +// +// • `interval` shorter than one second is automatically promoted to exactly one +// second – sub-second windows rarely make sense and can break when system +// clocks are not precise. +// +// • `back` accepts *N* previous windows for validation +// +// # Panics +// +// The function does **not** panic; all parameters are sanitised. +// +// # Example +// +// hasher := yahash.NewHash(yahash.FNVStringToInt64, "secret", time.Minute, 5) +// hash := hasher.HashWithTime(time.Now()) +// if !hasher.Validate(hash) { /* reject */ } func NewHash[I HashableType, O comparable]( hash HashFunc[I, O], secret I, @@ -37,10 +158,27 @@ func NewHash[I HashableType, O comparable]( } } +// Hash hashes *data* together with optional extra arguments **and** the secret. +// +// The secret is always appended as the last argument so that callers do not have +// to remember to pass it explicitly – a common pitfall when the hashing happens +// in several places. +// +// # Example +// +// hash := hasher.Hash("yadata", "ya_args1", "ya_args2") func (h *Hash[I, O]) Hash(data I, args ...I) O { return h.hash(data, append(args, h.secret)...) } +// HashWithTime is identical to `Hash` but replaces *data* with a +// Unix‑timestamp. This effectively rolls the secret every +// *interval* and makes tokens time‑bound. +// +// # Example +// +// // within request handler: +// hash := hasher.HashWithTime(time.Now(), userID) func (h *Hash[I, O]) HashWithTime(inputTime time.Time, args ...I) O { parsedTime, _ := valueparser. ParseValue[I]( @@ -49,10 +187,31 @@ func (h *Hash[I, O]) HashWithTime(inputTime time.Time, args ...I) O { return h.hash(parsedTime, append(args, h.secret)...) } +// ValidateWithoutTime recomputes a hash **without** the time component and +// compares it to *expected*. +// +// This is useful when you only need salting (secret) but still want a unified +// API together with the time‑aware helpers. +// +// # Example +// +// if !hasher.ValidateWithoutTime(expected, payload) { +// // tampered +// } func (h *Hash[I, O]) ValidateWithoutTime(expected O, data I, args ...I) bool { return h.Hash(data, args...) == expected } +// Validate recomputes the hash for the **current** time‑window and for *back* +// previous ones (inclusive) and returns whether any of them match *expected*. +// +// # Example +// +// expected := hasher.HashWithTime(time.Now().Add(-2*time.Hour), "ya_args") +// +// if ok := hasher.Validate(expected, "ya_args"); !ok { +// // expired +// } func (h *Hash[I, O]) Validate(expected O, args ...I) bool { for i := 0; i <= h.back; i++ { date := time.Now().Add(h.interval * -time.Duration(i)) @@ -66,6 +225,11 @@ func (h *Hash[I, O]) Validate(expected O, args ...I) bool { return false } +// ValidateCustomBack behaves like `Validate` but lets the caller specify a +// custom *back* window on a per‑call basis. +// +// This is handy when the acceptable drift is not known at construction time or +// when different endpoints require different policies. func (h *Hash[I, O]) ValidateCustomBack(expected O, back int, args ...I) bool { for i := 0; i <= back; i++ { date := time.Now().Add(h.interval * -time.Duration(i)) @@ -79,6 +243,17 @@ func (h *Hash[I, O]) ValidateCustomBack(expected O, back int, args ...I) bool { return false } +// FNVStringToInt64 is a ready‑to‑use 64‑bit FNV‑1a implementation compatible +// with the `HashFunc` signature. +// +// It concatenates *data* and *args* (already salted) and returns the 64‑bit +// digest as a signed integer (`int64`). +// +// # Example +// +// hasher := yahash.NewHash(yahash.FNVStringToInt64, "secret", time.Minute, 3) +// code := hasher.HashWithTime(time.Now(), "ya_args") +// fmt.Println(code) func FNVStringToInt64(data string, args ...string) int64 { hasher := fnv.New64() hasher.Write([]byte(data)) From fc9c553deacba87b25d42cd238c0c220d7689409 Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Wed, 25 Jun 2025 03:08:58 +0300 Subject: [PATCH 5/8] chore(yahash): `time.Now()` into variable --- yahash/yahash_test.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/yahash/yahash_test.go b/yahash/yahash_test.go index bd4a6c3..9019b5d 100644 --- a/yahash/yahash_test.go +++ b/yahash/yahash_test.go @@ -26,17 +26,21 @@ func TestHash64_DeterministicWorks(t *testing.T) { } func TestHash64WithTime_DeterministicWorks(t *testing.T) { - hash1 := testHash.HashWithTime(time.Now(), testDataForHash...) - hash2 := testHash.HashWithTime(time.Now(), testDataForHash...) + now := time.Now() + + hash1 := testHash.HashWithTime(now, testDataForHash...) + hash2 := testHash.HashWithTime(now, testDataForHash...) assert.Equal(t, hash1, hash2, fmt.Sprintf("Hash64WithTime not deterministic: got %d and %d", hash1, hash2)) } func TestHash_Matches_HashWithTime(t *testing.T) { + now := time.Now() + hash := testHash.Hash( - strconv.FormatInt(time.Now().Unix()/int64(time.Hour/time.Second), 10), testDataForHash..., + strconv.FormatInt(now.Unix()/int64(time.Hour/time.Second), 10), testDataForHash..., ) - hashWithTime := testHash.HashWithTime(time.Now(), testDataForHash...) + hashWithTime := testHash.HashWithTime(now, testDataForHash...) assert.Equal(t, hash, hashWithTime, fmt.Sprintf("Hash64 doesn't match to Hash64WithTime. hash64: %d, hash64WithTime: %d", hash, hashWithTime)) From e6307c34b6d3c9573ea25b2c9b54e8e3c94a80d0 Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Wed, 25 Jun 2025 03:11:49 +0300 Subject: [PATCH 6/8] chore(yahash): reduce code by `DRY` use func --- yahash/yahash.go | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/yahash/yahash.go b/yahash/yahash.go index a3c6fbb..65d420c 100644 --- a/yahash/yahash.go +++ b/yahash/yahash.go @@ -213,16 +213,7 @@ func (h *Hash[I, O]) ValidateWithoutTime(expected O, data I, args ...I) bool { // // expired // } func (h *Hash[I, O]) Validate(expected O, args ...I) bool { - for i := 0; i <= h.back; i++ { - date := time.Now().Add(h.interval * -time.Duration(i)) - generated := h.HashWithTime(date, args...) - - if generated == expected { - return true - } - } - - return false + return h.ValidateCustomBack(expected, h.back, args...) } // ValidateCustomBack behaves like `Validate` but lets the caller specify a From c0677d4b6856fe9b19bc2ca6b569d5624e7e1db8 Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Wed, 25 Jun 2025 03:14:25 +0300 Subject: [PATCH 7/8] perf(yahash): cache `time.Now()` before `for loop` --- yahash/yahash.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/yahash/yahash.go b/yahash/yahash.go index 65d420c..e983c20 100644 --- a/yahash/yahash.go +++ b/yahash/yahash.go @@ -222,8 +222,10 @@ func (h *Hash[I, O]) Validate(expected O, args ...I) bool { // This is handy when the acceptable drift is not known at construction time or // when different endpoints require different policies. func (h *Hash[I, O]) ValidateCustomBack(expected O, back int, args ...I) bool { + now := time.Now() + for i := 0; i <= back; i++ { - date := time.Now().Add(h.interval * -time.Duration(i)) + date := now.Add(h.interval * -time.Duration(i)) generated := h.HashWithTime(date, args...) if generated == expected { From 5d79bbc698e4349832595cc2f38c623ce3917e27 Mon Sep 17 00:00:00 2001 From: Vlad Lavrishko Date: Wed, 25 Jun 2025 17:09:52 +0300 Subject: [PATCH 8/8] chore(yacache): correct naming and types --- yahash/yahash.go | 48 +++++++++++++++++++++---------------------- yahash/yahash_test.go | 6 +++--- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/yahash/yahash.go b/yahash/yahash.go index e983c20..b285db1 100644 --- a/yahash/yahash.go +++ b/yahash/yahash.go @@ -73,7 +73,7 @@ import ( // HashableType describes the set of types that can be parsed from / converted to // a string by the *valueparser* package **and** that you intend to feed into the // supplied hashing function. In practice it will usually resolve to `string`, -// `int64`, or another scalar type supported by your parser. +// `int64`, or another scalar type supported by parser. type HashableType valueparser.ParsableType // HashFunc is the signature every hashing algorithm must satisfy in order to be @@ -117,19 +117,19 @@ type HashFunc[I HashableType, O comparable] func(data I, args ...I) O // // The zero value is **not** usable; always construct via `NewHash`. type Hash[I HashableType, O comparable] struct { - hash HashFunc[I, O] - interval time.Duration // ≥ 1s after constructor check - secret I - back int + hasher HashFunc[I, O] + stepInterval time.Duration // ≥ 1s after constructor check + secret I + backStepCount uint16 } // NewHash returns an initialised Hash helper. // -// • `interval` shorter than one second is automatically promoted to exactly one +// • `stepInterval` shorter than one second is automatically promoted to exactly one // second – sub-second windows rarely make sense and can break when system // clocks are not precise. // -// • `back` accepts *N* previous windows for validation +// • `backStepCount` accepts *N* previous windows for validation // // # Panics // @@ -141,20 +141,20 @@ type Hash[I HashableType, O comparable] struct { // hash := hasher.HashWithTime(time.Now()) // if !hasher.Validate(hash) { /* reject */ } func NewHash[I HashableType, O comparable]( - hash HashFunc[I, O], + hasher HashFunc[I, O], secret I, - interval time.Duration, - back int, + stepInterval time.Duration, + backStepCount uint16, ) Hash[I, O] { - if interval < time.Second { - interval = time.Second + if stepInterval < time.Second { + stepInterval = time.Second } return Hash[I, O]{ - hash: hash, - secret: secret, - interval: interval, - back: back, + hasher: hasher, + secret: secret, + stepInterval: stepInterval, + backStepCount: backStepCount, } } @@ -168,7 +168,7 @@ func NewHash[I HashableType, O comparable]( // // hash := hasher.Hash("yadata", "ya_args1", "ya_args2") func (h *Hash[I, O]) Hash(data I, args ...I) O { - return h.hash(data, append(args, h.secret)...) + return h.hasher(data, append(args, h.secret)...) } // HashWithTime is identical to `Hash` but replaces *data* with a @@ -182,9 +182,9 @@ func (h *Hash[I, O]) Hash(data I, args ...I) O { func (h *Hash[I, O]) HashWithTime(inputTime time.Time, args ...I) O { parsedTime, _ := valueparser. ParseValue[I]( - strconv.FormatInt(inputTime.Unix()/int64(h.interval/time.Second), 10)) // SAFETY: This cannot return error + strconv.FormatInt(inputTime.Unix()/int64(h.stepInterval/time.Second), 10)) // SAFETY: This cannot return error - return h.hash(parsedTime, append(args, h.secret)...) + return h.hasher(parsedTime, append(args, h.secret)...) } // ValidateWithoutTime recomputes a hash **without** the time component and @@ -213,19 +213,19 @@ func (h *Hash[I, O]) ValidateWithoutTime(expected O, data I, args ...I) bool { // // expired // } func (h *Hash[I, O]) Validate(expected O, args ...I) bool { - return h.ValidateCustomBack(expected, h.back, args...) + return h.ValidateWithCustomBackStepCount(expected, h.backStepCount, args...) } -// ValidateCustomBack behaves like `Validate` but lets the caller specify a +// ValidateWithCustomBackStepCount behaves like `Validate` but lets the caller specify a // custom *back* window on a per‑call basis. // // This is handy when the acceptable drift is not known at construction time or // when different endpoints require different policies. -func (h *Hash[I, O]) ValidateCustomBack(expected O, back int, args ...I) bool { +func (h *Hash[I, O]) ValidateWithCustomBackStepCount(expected O, backStepCount uint16, args ...I) bool { now := time.Now() - for i := 0; i <= back; i++ { - date := now.Add(h.interval * -time.Duration(i)) + for i := 0; i <= int(backStepCount); i++ { + date := now.Add(h.stepInterval * -time.Duration(i)) generated := h.HashWithTime(date, args...) if generated == expected { diff --git a/yahash/yahash_test.go b/yahash/yahash_test.go index 9019b5d..f31b2a4 100644 --- a/yahash/yahash_test.go +++ b/yahash/yahash_test.go @@ -83,18 +83,18 @@ func TestValidateHash_Works(t *testing.T) { }) }) - t.Run("[ValidateCustomBack]", func(t *testing.T) { + t.Run("[ValidateWithCustomBackStepCount]", func(t *testing.T) { t.Run("True", func(t *testing.T) { expected := testHash.HashWithTime(time.Now().Add(-time.Hour*6), testDataForHash...) - assert.True(t, testHash.ValidateCustomBack(expected, 7, testDataForHash...), + assert.True(t, testHash.ValidateWithCustomBackStepCount(expected, 7, testDataForHash...), "Got `False` by valid hash with correct date") }) t.Run("False", func(t *testing.T) { expected := testHash.HashWithTime(time.Now().Add(-time.Hour*16), testDataForHash...) - assert.False(t, testHash.ValidateCustomBack(expected, 10, testDataForHash...), + assert.False(t, testHash.ValidateWithCustomBackStepCount(expected, 10, testDataForHash...), "Got `True` by invalid hash with non correct date") }) })