Repository: gohugoio/hugo (language: go)

Issue:
Add hash.XxHash
Also move the non crypto hash funcs into this new package.

https://github.com/cespare/xxhash


Discussion / hints:

Fix patch:
diff --git a/go.mod b/go.mod
index 2cff6b5e079..5a10b2480aa 100644
--- a/go.mod
+++ b/go.mod
@@ -21,6 +21,7 @@ require (
 	github.com/bep/overlayfs v0.9.2
 	github.com/bep/simplecobra v0.4.0
 	github.com/bep/tmc v0.5.1
+	github.com/cespare/xxhash/v2 v2.3.0
 	github.com/clbanning/mxj/v2 v2.7.0
 	github.com/cli/safeexec v1.0.1
 	github.com/disintegration/gift v1.2.1
diff --git a/go.sum b/go.sum
index 0498c7f8d05..ea4788a9727 100644
--- a/go.sum
+++ b/go.sum
@@ -147,6 +147,8 @@ github.com/bep/tmc v0.5.1/go.mod h1:tGYHN8fS85aJPhDLgXETVKp+PR382OvFi2+q2GkGsq0=
 github.com/bep/workers v1.0.0 h1:U+H8YmEaBCEaFZBst7GcRVEoqeRC9dzH2dWOwGmOchg=
 github.com/bep/workers v1.0.0/go.mod h1:7kIESOB86HfR2379pwoMWNy8B50D7r99fRLUyPSNyCs=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
 github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
diff --git a/tpl/crypto/crypto.go b/tpl/crypto/crypto.go
index 412a192128b..677f59139ff 100644
--- a/tpl/crypto/crypto.go
+++ b/tpl/crypto/crypto.go
@@ -25,6 +25,7 @@ import (
 	"hash"
 	"hash/fnv"
 
+	"github.com/gohugoio/hugo/common/hugo"
 	"github.com/spf13/cast"
 )
 
@@ -72,6 +73,7 @@ func (ns *Namespace) SHA256(v any) (string, error) {
 // FNV32a hashes v using fnv32a algorithm.
 // <docsmeta>{"newIn": "0.98.0" }</docsmeta>
 func (ns *Namespace) FNV32a(v any) (int, error) {
+	hugo.Deprecate("crypto.FNV32a", "Use hash.FNV32a.", "v0.129.0")
 	conv, err := cast.ToStringE(v)
 	if err != nil {
 		return 0, err
diff --git a/tpl/crypto/init.go b/tpl/crypto/init.go
index 418fbd9fb47..0527fba06d0 100644
--- a/tpl/crypto/init.go
+++ b/tpl/crypto/init.go
@@ -53,13 +53,6 @@ func init() {
 			},
 		)
 
-		ns.AddMethodMapping(ctx.FNV32a,
-			nil,
-			[][2]string{
-				{`{{ crypto.FNV32a "Hugo Rocks!!" }}`, `1515779328`},
-			},
-		)
-
 		ns.AddMethodMapping(ctx.HMAC,
 			[]string{"hmac"},
 			[][2]string{
diff --git a/tpl/hash/hash.go b/tpl/hash/hash.go
new file mode 100644
index 00000000000..d4a80b342a8
--- /dev/null
+++ b/tpl/hash/hash.go
@@ -0,0 +1,93 @@
+// Copyright 2024 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package hash provides non-cryptographic hash functions for template use.
+package hash
+
+import (
+	"context"
+	"encoding/hex"
+	"hash/fnv"
+
+	"github.com/cespare/xxhash/v2"
+	"github.com/gohugoio/hugo/deps"
+	"github.com/gohugoio/hugo/tpl/internal"
+	"github.com/spf13/cast"
+)
+
+// New returns a new instance of the hash-namespaced template functions.
+func New() *Namespace {
+	return &Namespace{}
+}
+
+// Namespace provides template functions for the "hash" namespace.
+type Namespace struct{}
+
+// FNV32a hashes v using fnv32a algorithm.
+func (ns *Namespace) FNV32a(v any) (int, error) {
+	conv, err := cast.ToStringE(v)
+	if err != nil {
+		return 0, err
+	}
+	algorithm := fnv.New32a()
+	algorithm.Write([]byte(conv))
+	return int(algorithm.Sum32()), nil
+}
+
+// XxHash returns the xxHash of the input string.
+func (ns *Namespace) XxHash(v any) (string, error) {
+	conv, err := cast.ToStringE(v)
+	if err != nil {
+		return "", err
+	}
+
+	hasher := xxhash.New()
+
+	_, err = hasher.WriteString(conv)
+	if err != nil {
+		return "", err
+	}
+	hash := hasher.Sum(nil)
+	return hex.EncodeToString(hash), nil
+}
+
+const name = "hash"
+
+func init() {
+	f := func(d *deps.Deps) *internal.TemplateFuncsNamespace {
+		ctx := New()
+
+		ns := &internal.TemplateFuncsNamespace{
+			Name:    name,
+			Context: func(cctx context.Context, args ...any) (any, error) { return ctx, nil },
+		}
+
+		ns.AddMethodMapping(ctx.XxHash,
+			[]string{"xxhash"},
+			[][2]string{
+				{`{{ hash.XxHash "The quick brown fox jumps over the lazy dog" }}`, `0b242d361fda71bc`},
+			},
+		)
+
+		ns.AddMethodMapping(ctx.FNV32a,
+			nil,
+			[][2]string{
+				{`{{ hash.FNV32a "Hugo Rocks!!" }}`, `1515779328`},
+			},
+		)
+
+		return ns
+	}
+
+	internal.AddTemplateFuncsNamespace(f)
+}
diff --git a/tpl/tplimpl/template_funcs.go b/tpl/tplimpl/template_funcs.go
index 81527063149..3daba74a03e 100644
--- a/tpl/tplimpl/template_funcs.go
+++ b/tpl/tplimpl/template_funcs.go
@@ -43,6 +43,7 @@ import (
 	_ "github.com/gohugoio/hugo/tpl/diagrams"
 	_ "github.com/gohugoio/hugo/tpl/encoding"
 	_ "github.com/gohugoio/hugo/tpl/fmt"
+	_ "github.com/gohugoio/hugo/tpl/hash"
 	_ "github.com/gohugoio/hugo/tpl/hugo"
 	_ "github.com/gohugoio/hugo/tpl/images"
 	_ "github.com/gohugoio/hugo/tpl/inflect"


Test patch:
diff --git a/tpl/hash/hash_test.go b/tpl/hash/hash_test.go
new file mode 100644
index 00000000000..ff5d59a9a03
--- /dev/null
+++ b/tpl/hash/hash_test.go
@@ -0,0 +1,84 @@
+// Copyright 2024 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package hash provides non-cryptographic hash functions for template use.
+package hash
+
+import (
+	"crypto/md5"
+	"encoding/hex"
+	"fmt"
+	"strings"
+	"testing"
+
+	qt "github.com/frankban/quicktest"
+	"github.com/spf13/cast"
+)
+
+func TestXxHash(t *testing.T) {
+	t.Parallel()
+	c := qt.New(t)
+
+	ns := New()
+
+	h, err := ns.XxHash("The quick brown fox jumps over the lazy dog")
+	c.Assert(err, qt.IsNil)
+	// Facit: https://asecuritysite.com/encryption/xxhash?val=The%20quick%20brown%20fox%20jumps%20over%20the%20lazy%20dog
+	c.Assert(h, qt.Equals, "0b242d361fda71bc")
+}
+
+func BenchmarkXxHash(b *testing.B) {
+	const inputSmall = "The quick brown fox jumps over the lazy dog"
+	inputLarge := strings.Repeat(inputSmall, 100)
+
+	runBench := func(name, input string, b *testing.B, fn func(v any)) {
+		b.Run(fmt.Sprintf("%s_%d", name, len(input)), func(b *testing.B) {
+			for i := 0; i < b.N; i++ {
+				fn(input)
+			}
+		})
+	}
+
+	ns := New()
+	fnXxHash := func(v any) {
+		_, err := ns.XxHash(v)
+		if err != nil {
+			panic(err)
+		}
+	}
+
+	fnFNv32a := func(v any) {
+		_, err := ns.FNV32a(v)
+		if err != nil {
+			panic(err)
+		}
+	}
+
+	// Copied from the crypto tpl/crypto package,
+	// just to have something to compare the above with.
+	fnMD5 := func(v any) {
+		conv, err := cast.ToStringE(v)
+		if err != nil {
+			panic(err)
+		}
+
+		hash := md5.Sum([]byte(conv))
+		_ = hex.EncodeToString(hash[:])
+	}
+
+	for _, input := range []string{inputSmall, inputLarge} {
+		runBench("xxHash", input, b, fnXxHash)
+		runBench("mdb5", input, b, fnMD5)
+		runBench("fnv32a", input, b, fnFNv32a)
+	}
+}