diff --git a/go.mod b/go.mod index 2cff6b5e0..5a10b2480 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 0498c7f8d..ea4788a97 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 412a19212..677f59139 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. // {"newIn": "0.98.0" } 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 418fbd9fb..0527fba06 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 000000000..d4a80b342 --- /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/hash/hash_test.go b/tpl/hash/hash_test.go new file mode 100644 index 000000000..ff5d59a9a --- /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) + } +} diff --git a/tpl/tplimpl/template_funcs.go b/tpl/tplimpl/template_funcs.go index 815270631..3daba74a0 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"