From a5ec54239312c160b82e303f159c4bdb2e030ab5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Sun, 28 Jun 2026 11:46:26 +0200 Subject: [PATCH] Add encoding.HexDecode/Encode Fixes #15068 See #15060 --- tpl/encoding/encoding.go | 23 +++++++++ tpl/encoding/encoding_test.go | 54 +++++++++++++++++++++ tpl/encoding/init.go | 14 ++++++ tpl/templates/decorator_integration_test.go | 46 ++++++++++++++++++ 4 files changed, 137 insertions(+) diff --git a/tpl/encoding/encoding.go b/tpl/encoding/encoding.go index 6ac0e4f1e..1f7cf7b79 100644 --- a/tpl/encoding/encoding.go +++ b/tpl/encoding/encoding.go @@ -16,6 +16,7 @@ package encoding import ( "encoding/base64" + "encoding/hex" "encoding/json" "errors" "html/template" @@ -56,6 +57,28 @@ func (ns *Namespace) Base64Encode(content any) (string, error) { return base64.StdEncoding.EncodeToString([]byte(conv)), nil } +// HexDecode returns the hex decoding of the given content. +func (ns *Namespace) HexDecode(content any) (string, error) { + conv, err := cast.ToStringE(content) + if err != nil { + return "", err + } + b, err := hex.DecodeString(conv) + if err != nil { + return "", err + } + return string(b), nil +} + +// HexEncode returns the hex encoding of the given content. +func (ns *Namespace) HexEncode(content any) (string, error) { + conv, err := cast.ToStringE(content) + if err != nil { + return "", err + } + return hex.EncodeToString([]byte(conv)), nil +} + // Jsonify encodes a given object to JSON. To pretty print the JSON, pass a map // or dictionary of options as the first value in args. Supported options are // "prefix" and "indent". Each JSON element in the output will begin on a new diff --git a/tpl/encoding/encoding_test.go b/tpl/encoding/encoding_test.go index 8e6e2da48..116bef408 100644 --- a/tpl/encoding/encoding_test.go +++ b/tpl/encoding/encoding_test.go @@ -77,6 +77,60 @@ func TestBase64Encode(t *testing.T) { } } +func TestHexDecode(t *testing.T) { + t.Parallel() + c := qt.New(t) + + ns := New() + + for _, test := range []struct { + v any + expect any + }{ + {"616263313233213f242a2628292d3d407e", "abc123!?$*&()-=@~"}, + // errors + {t, false}, + } { + + result, err := ns.HexDecode(test.v) + + if b, ok := test.expect.(bool); ok && !b { + c.Assert(err, qt.Not(qt.IsNil)) + continue + } + + c.Assert(err, qt.IsNil) + c.Assert(result, qt.Equals, test.expect) + } +} + +func TestHexEncode(t *testing.T) { + t.Parallel() + c := qt.New(t) + + ns := New() + + for _, test := range []struct { + v any + expect any + }{ + {"abc123!?$*&()-=@~", "616263313233213f242a2628292d3d407e"}, + // errors + {t, false}, + } { + + result, err := ns.HexEncode(test.v) + + if b, ok := test.expect.(bool); ok && !b { + c.Assert(err, qt.Not(qt.IsNil)) + continue + } + + c.Assert(err, qt.IsNil) + c.Assert(result, qt.Equals, test.expect) + } +} + func TestJsonify(t *testing.T) { t.Parallel() c := qt.New(t) diff --git a/tpl/encoding/init.go b/tpl/encoding/init.go index 1c3322d6e..aab411e4d 100644 --- a/tpl/encoding/init.go +++ b/tpl/encoding/init.go @@ -46,6 +46,20 @@ func init() { }, ) + ns.AddMethodMapping(ctx.HexDecode, + nil, + [][2]string{ + {`{{ "48656c6c6f20776f726c64" | encoding.HexDecode }}`, `Hello world`}, + }, + ) + + ns.AddMethodMapping(ctx.HexEncode, + nil, + [][2]string{ + {`{{ "Hello world" | encoding.HexEncode }}`, `48656c6c6f20776f726c64`}, + }, + ) + ns.AddMethodMapping(ctx.Jsonify, []string{"jsonify"}, [][2]string{ diff --git a/tpl/templates/decorator_integration_test.go b/tpl/templates/decorator_integration_test.go index 253a09316..e671c446c 100644 --- a/tpl/templates/decorator_integration_test.go +++ b/tpl/templates/decorator_integration_test.go @@ -453,3 +453,49 @@ Home. {{ partial "a" }}:Done. } } } + +func TestDecoratorFingerprintScript(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +baseURL = "https://example.org/" +[outputs] +home = ["html", "headers"] +[mediaTypes] +[mediaTypes."text/netlify"] +delimiter = "" +[outputFormats] +[outputFormats.headers] +baseName = "_headers" +isPlainText = true +mediatype = "text/netlify" +notAlternative = true +-- layouts/_partials/script.html -- +{{ $s := (templates.Inner .) }} +{{ $r := resources.FromString ($s | xxhash) $s | fingerprint }} +{{ $hash := $s | crypto.SHA256 | encoding.HexDecode | encoding.Base64Encode }} +{{ $sri := (printf "sha256-%s" $hash) }} +{{ if ne $sri $r.Data.Integrity }} + {{ errorf "SRI hash mismatch: %s != %s" $sri $r.Data.Integrity }} +{{ end }} +{{ hugo.Store.SetInMap "srihashes" $sri true }} + +-- layouts/home.html -- +Home. +{{ with partial "script.html" . }} +console.log("Hello world"); +{{ end }} +{{ with partial "script.html" . }} +console.log("Hugo rocks!"); +{{ end }} +-- layouts/home.headers -- +{{ $hashes := hugo.Store.Get "srihashes" -}} +Content-Security-Policy: script-src 'self'{{ range $k, $v := $hashes }} '{{ $k }}'{{ end }}; +` + + b := hugolib.Test(t, files) + + b.AssertFileContentExact("public/index.html", "") + b.AssertFileContentExact("public/_headers", "Content-Security-Policy: script-src 'self' 'sha256-t6z9j4/CagmaDDbn6nHbntzt8PRk8HZ1TwrgdYuZjbE=' 'sha256-wVbS8f78ttoSSGU80KghGoPcTO8DXqT0Uc/87a7zMdk=';") +}