From ebd9eea30e3def2ee769157b923c88262a887d01 Mon Sep 17 00:00:00 2001 From: Joe Kralicky Date: Wed, 6 Nov 2024 12:31:52 -0500 Subject: [PATCH] Optimize Policy RouteID (#5359) --- .../envoyconfig/route_configurations_test.go | 8 +- config/envoyconfig/routes_test.go | 199 ++++++++++-------- config/policy.go | 83 +++++--- config/policy_test.go | 192 +++++++++++++++++ internal/benchmarks/latency_bench_test.go | 3 + internal/hashutil/hashutil.go | 177 ++++++++++++++++ 6 files changed, 540 insertions(+), 122 deletions(-) diff --git a/config/envoyconfig/route_configurations_test.go b/config/envoyconfig/route_configurations_test.go index a5323c2a6..bb0bfa3c0 100644 --- a/config/envoyconfig/route_configurations_test.go +++ b/config/envoyconfig/route_configurations_test.go @@ -77,7 +77,7 @@ func TestBuilder_buildMainRouteConfiguration(t *testing.T) { ], "route": { "autoHostRewrite": true, - "cluster": "route-5d678ee30d16332b", + "cluster": "route-b8e37dd1f9d65ddd", "hashPolicy": [ { "header": { "headerName": "x-pomerium-routing-key" }, "terminal": true }, { "connectionProperties": { "sourceIp": true }, "terminal": true } @@ -94,7 +94,7 @@ func TestBuilder_buildMainRouteConfiguration(t *testing.T) { "checkSettings": { "contextExtensions": { "internal": "false", - "route_id": "6730505273956774699" + "route_id": "13322630463485271517" } } } @@ -130,7 +130,7 @@ func TestBuilder_buildMainRouteConfiguration(t *testing.T) { ], "route": { "autoHostRewrite": true, - "cluster": "route-5d678ee30d16332b", + "cluster": "route-b8e37dd1f9d65ddd", "hashPolicy": [ { "header": { "headerName": "x-pomerium-routing-key" }, "terminal": true }, { "connectionProperties": { "sourceIp": true }, "terminal": true } @@ -147,7 +147,7 @@ func TestBuilder_buildMainRouteConfiguration(t *testing.T) { "checkSettings": { "contextExtensions": { "internal": "false", - "route_id": "6730505273956774699" + "route_id": "13322630463485271517" } } } diff --git a/config/envoyconfig/routes_test.go b/config/envoyconfig/routes_test.go index 330a7796f..edb5937a7 100644 --- a/config/envoyconfig/routes_test.go +++ b/config/envoyconfig/routes_test.go @@ -303,79 +303,102 @@ func Test_buildPolicyRoutes(t *testing.T) { oneMinute := time.Minute ten := time.Second * 10 + // note: within each policy below, fields that do not affect the route ID + // are grouped separately, after the fields that do affect the route ID. + policies := []config.Policy{ + 0: { // skipped by host filter + From: "https://ignore.example.com", + To: mustParseWeightedURLs(t, "https://to.example.com"), + + PassIdentityHeaders: ptr(true), + }, + 1: { + From: "https://example.com", + To: mustParseWeightedURLs(t, "https://to.example.com"), + + PassIdentityHeaders: ptr(true), + }, + 2: { + From: "https://example.com", + To: mustParseWeightedURLs(t, "https://to.example.com"), + Path: "/some/path", + + AllowWebsockets: true, + PreserveHostHeader: true, + PassIdentityHeaders: ptr(true), + }, + 3: { + From: "https://example.com", + To: mustParseWeightedURLs(t, "https://to.example.com"), + Prefix: "/some/prefix/", + + SetRequestHeaders: map[string]string{"HEADER-KEY": "HEADER-VALUE"}, + UpstreamTimeout: &oneMinute, + PassIdentityHeaders: ptr(true), + }, + 4: { + From: "https://example.com", + To: mustParseWeightedURLs(t, "https://to.example.com"), + Regex: `^/[a]+$`, + + PassIdentityHeaders: ptr(true), + }, + 5: { // same route ID as 3 + From: "https://example.com", + To: mustParseWeightedURLs(t, "https://to.example.com"), + Prefix: "/some/prefix/", + + RemoveRequestHeaders: []string{"HEADER-KEY"}, + UpstreamTimeout: &oneMinute, + PassIdentityHeaders: ptr(true), + }, + 6: { // same route ID as 2 + From: "https://example.com", + To: mustParseWeightedURLs(t, "https://to.example.com"), + Path: "/some/path", + + AllowSPDY: true, + PreserveHostHeader: true, + PassIdentityHeaders: ptr(true), + }, + 7: { // same route ID as 2 + From: "https://example.com", + To: mustParseWeightedURLs(t, "https://to.example.com"), + Path: "/some/path", + + AllowSPDY: true, + AllowWebsockets: true, + PreserveHostHeader: true, + PassIdentityHeaders: ptr(true), + }, + 8: { + From: "https://example.com", + To: mustParseWeightedURLs(t, "https://to.example.com"), + Path: "/websocket-timeout", + + AllowWebsockets: true, + PreserveHostHeader: true, + PassIdentityHeaders: ptr(true), + UpstreamTimeout: &ten, + }, + } + routeIDs := []string{ + 1: "772697672458217856", + 2: "6032229746964560472", + 3: "13317665674438641304", + 4: "9768293332770157550", + 5: "13317665674438641304", // same as 3 + 6: "6032229746964560472", // same as 2 + 7: "6032229746964560472", // same as 2 + 8: "1591581179179639728", + } + b := &Builder{filemgr: filemgr.NewManager(), reproxy: reproxy.New()} routes, err := b.buildRoutesForPoliciesWithHost(&config.Config{Options: &config.Options{ CookieName: "pomerium", DefaultUpstreamTimeout: time.Second * 3, SharedKey: cryptutil.NewBase64Key(), - Policies: []config.Policy{ - { - From: "https://ignore.example.com", - To: mustParseWeightedURLs(t, "https://to.example.com"), - PassIdentityHeaders: ptr(true), - }, - { - From: "https://example.com", - To: mustParseWeightedURLs(t, "https://to.example.com"), - PassIdentityHeaders: ptr(true), - }, - { - From: "https://example.com", - To: mustParseWeightedURLs(t, "https://to.example.com"), - Path: "/some/path", - AllowWebsockets: true, - PreserveHostHeader: true, - PassIdentityHeaders: ptr(true), - }, - { - From: "https://example.com", - To: mustParseWeightedURLs(t, "https://to.example.com"), - Prefix: "/some/prefix/", - SetRequestHeaders: map[string]string{"HEADER-KEY": "HEADER-VALUE"}, - UpstreamTimeout: &oneMinute, - PassIdentityHeaders: ptr(true), - }, - { - From: "https://example.com", - To: mustParseWeightedURLs(t, "https://to.example.com"), - Regex: `^/[a]+$`, - PassIdentityHeaders: ptr(true), - }, - { - From: "https://example.com", - To: mustParseWeightedURLs(t, "https://to.example.com"), - Prefix: "/some/prefix/", - RemoveRequestHeaders: []string{"HEADER-KEY"}, - UpstreamTimeout: &oneMinute, - PassIdentityHeaders: ptr(true), - }, - { - From: "https://example.com", - To: mustParseWeightedURLs(t, "https://to.example.com"), - Path: "/some/path", - AllowSPDY: true, - PreserveHostHeader: true, - PassIdentityHeaders: ptr(true), - }, - { - From: "https://example.com", - To: mustParseWeightedURLs(t, "https://to.example.com"), - Path: "/some/path", - AllowSPDY: true, - AllowWebsockets: true, - PreserveHostHeader: true, - PassIdentityHeaders: ptr(true), - }, - { - From: "https://example.com", - To: mustParseWeightedURLs(t, "https://to.example.com"), - Path: "/websocket-timeout", - AllowWebsockets: true, - PreserveHostHeader: true, - PassIdentityHeaders: ptr(true), - UpstreamTimeout: &ten, - }, - }, + Policies: policies, }}, "example.com") require.NoError(t, err) @@ -445,7 +468,7 @@ func Test_buildPolicyRoutes(t *testing.T) { "checkSettings": { "contextExtensions": { "internal": "false", - "route_id": "16913502743845432363" + "route_id": "`+routeIDs[1]+`" } } } @@ -516,7 +539,7 @@ func Test_buildPolicyRoutes(t *testing.T) { "checkSettings": { "contextExtensions": { "internal": "false", - "route_id": "911713133804109577" + "route_id": "`+routeIDs[2]+`" } } } @@ -586,7 +609,7 @@ func Test_buildPolicyRoutes(t *testing.T) { "checkSettings": { "contextExtensions": { "internal": "false", - "route_id": "6407864870815560799" + "route_id": "`+routeIDs[3]+`" } } } @@ -658,7 +681,7 @@ func Test_buildPolicyRoutes(t *testing.T) { "checkSettings": { "contextExtensions": { "internal": "false", - "route_id": "1103677309004574500" + "route_id": "`+routeIDs[4]+`" } } } @@ -729,7 +752,7 @@ func Test_buildPolicyRoutes(t *testing.T) { "checkSettings": { "contextExtensions": { "internal": "false", - "route_id": "6407864870815560799" + "route_id": "`+routeIDs[5]+`" } } } @@ -799,7 +822,7 @@ func Test_buildPolicyRoutes(t *testing.T) { "checkSettings": { "contextExtensions": { "internal": "false", - "route_id": "911713133804109577" + "route_id": "`+routeIDs[6]+`" } } } @@ -870,7 +893,7 @@ func Test_buildPolicyRoutes(t *testing.T) { "checkSettings": { "contextExtensions": { "internal": "false", - "route_id": "911713133804109577" + "route_id": "`+routeIDs[7]+`" } } } @@ -941,7 +964,7 @@ func Test_buildPolicyRoutes(t *testing.T) { "checkSettings": { "contextExtensions": { "internal": "false", - "route_id": "17831746838845374842" + "route_id": "`+routeIDs[8]+`" } } } @@ -1124,7 +1147,7 @@ func Test_buildPolicyRoutes(t *testing.T) { "checkSettings": { "contextExtensions": { "internal": "false", - "route_id": "10474912405080199536" + "route_id": "11959552038839924732" } } } @@ -1196,7 +1219,7 @@ func Test_buildPolicyRoutes(t *testing.T) { "checkSettings": { "contextExtensions": { "internal": "false", - "route_id": "15730681265277585877" + "route_id": "9444248534316924938" } } } @@ -1294,7 +1317,7 @@ func Test_buildPolicyRoutes(t *testing.T) { "checkSettings": { "contextExtensions": { "internal": "false", - "route_id": "16598125949405432745" + "route_id": "5652544858774142715" } } } @@ -1366,14 +1389,14 @@ func Test_buildPolicyRoutes(t *testing.T) { "appendAction": "OVERWRITE_IF_EXISTS_OR_ADD", "header": { "key": "x-pomerium-reproxy-policy", - "value": "2222095689633600553" + "value": "5799631121007486501" } }, { "appendAction": "OVERWRITE_IF_EXISTS_OR_ADD", "header": { "key": "x-pomerium-reproxy-policy-hmac", - "value": "/cH0S/ODZYaW4CALohG926c+TH22+/bD79Kb82k8/Eg=" + "value": "v4w8DAUFdw2qw7RJLUZYBHWndqBOdz5Me6A+1vbDQPY=" } } ], @@ -1405,7 +1428,7 @@ func Test_buildPolicyRoutes(t *testing.T) { "checkSettings": { "contextExtensions": { "internal": "false", - "route_id": "2222095689633600553" + "route_id": "5799631121007486501" } } } @@ -1535,7 +1558,7 @@ func Test_buildPolicyRoutesRewrite(t *testing.T) { "checkSettings": { "contextExtensions": { "internal": "false", - "route_id": "13828028232508831592" + "route_id": "1410576726089372267" } } } @@ -1606,7 +1629,7 @@ func Test_buildPolicyRoutesRewrite(t *testing.T) { "checkSettings": { "contextExtensions": { "internal": "false", - "route_id": "13828028232508831592" + "route_id": "1410576726089372267" } } } @@ -1682,7 +1705,7 @@ func Test_buildPolicyRoutesRewrite(t *testing.T) { "checkSettings": { "contextExtensions": { "internal": "false", - "route_id": "13828028232508831592" + "route_id": "1410576726089372267" } } } @@ -1753,7 +1776,7 @@ func Test_buildPolicyRoutesRewrite(t *testing.T) { "checkSettings": { "contextExtensions": { "internal": "false", - "route_id": "13828028232508831592" + "route_id": "1410576726089372267" } } } @@ -1824,7 +1847,7 @@ func Test_buildPolicyRoutesRewrite(t *testing.T) { "checkSettings": { "contextExtensions": { "internal": "false", - "route_id": "13828028232508831592" + "route_id": "1410576726089372267" } } } @@ -1900,7 +1923,7 @@ func Test_buildPolicyRoutesRewrite(t *testing.T) { "checkSettings": { "contextExtensions": { "internal": "false", - "route_id": "13828028232508831592" + "route_id": "1410576726089372267" } } } diff --git a/config/policy.go b/config/policy.go index f8172804e..b67d1f314 100644 --- a/config/policy.go +++ b/config/policy.go @@ -654,30 +654,63 @@ func (p *Policy) Checksum() uint64 { return hashutil.MustHash(p) } -// RouteID returns a unique identifier for a route +// RouteID returns a unique identifier for a route. +// +// The following fields are used to compute the ID: +// - from +// - prefix +// - path +// - regex +// - to/redirect/response (whichever is set) func (p *Policy) RouteID() (uint64, error) { - id := routeID{ - From: p.From, - Prefix: p.Prefix, - Path: p.Path, - Regex: p.Regex, - } - - if len(p.To) > 0 { - dst, _, err := p.To.Flatten() - if err != nil { - return 0, err + // this function is in the hot path, try not to allocate too much memory here + hash := hashutil.NewDigest() + hash.WriteStringWithLen(p.From) + hash.WriteStringWithLen(p.Prefix) + hash.WriteStringWithLen(p.Path) + hash.WriteStringWithLen(p.Regex) + switch { + case len(p.To) > 0: + _, _ = hash.Write([]byte{1}) // case 1 + hash.WriteInt32(int32(len(p.To))) + for _, to := range p.To { + hash.WriteStringWithLen(to.URL.Scheme) + hash.WriteStringWithLen(to.URL.Opaque) + if to.URL.User == nil { + _, _ = hash.Write([]byte{0}) + } else { + _, _ = hash.Write([]byte{1}) + hash.WriteStringWithLen(to.URL.User.Username()) + p, _ := to.URL.User.Password() + hash.WriteStringWithLen(p) + } + hash.WriteStringWithLen(to.URL.Host) + hash.WriteStringWithLen(to.URL.Path) + hash.WriteStringWithLen(to.URL.RawPath) + hash.WriteBool(to.URL.OmitHost) + hash.WriteBool(to.URL.ForceQuery) + hash.WriteStringWithLen(to.URL.Fragment) + hash.WriteStringWithLen(to.URL.RawFragment) + hash.WriteUint32(to.LbWeight) } - id.To = dst - } else if p.Redirect != nil { - id.Redirect = p.Redirect - } else if p.Response != nil { - id.Response = p.Response - } else { + case p.Redirect != nil: + _, _ = hash.Write([]byte{2}) // case 2 + hash.WriteBoolPtr(p.Redirect.HTTPSRedirect) + hash.WriteStringPtrWithLen(p.Redirect.SchemeRedirect) + hash.WriteStringPtrWithLen(p.Redirect.HostRedirect) + hash.WriteUint32Ptr(p.Redirect.PortRedirect) + hash.WriteStringPtrWithLen(p.Redirect.PathRedirect) + hash.WriteStringPtrWithLen(p.Redirect.PrefixRewrite) + hash.WriteInt32Ptr(p.Redirect.ResponseCode) + hash.WriteBoolPtr(p.Redirect.StripQuery) + case p.Response != nil: + _, _ = hash.Write([]byte{3}) // case 3 + hash.WriteInt32(int32(p.Response.Status)) + hash.WriteStringWithLen(p.Response.Body) + default: return 0, errEitherToOrRedirectOrResponseRequired } - - return hashutil.Hash(id) + return hash.Sum64(), nil } func (p *Policy) MustRouteID() uint64 { @@ -811,16 +844,6 @@ func (p *Policy) GetPassIdentityHeaders(options *Options) bool { return false } -type routeID struct { - From string - To []string - Prefix string - Path string - Regex string - Redirect *PolicyRedirect - Response *DirectResponse -} - /* SortPolicies sorts policies to match the following SQL order: diff --git a/config/policy_test.go b/config/policy_test.go index 0b7fc0176..927d2faea 100644 --- a/config/policy_test.go +++ b/config/policy_test.go @@ -3,7 +3,9 @@ package config import ( "encoding/json" "fmt" + mathrand "math/rand/v2" "net/url" + "strings" "testing" envoy_config_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" @@ -13,6 +15,7 @@ import ( "google.golang.org/protobuf/proto" "github.com/pomerium/pomerium/internal/urlutil" + "github.com/pomerium/pomerium/pkg/cryptutil" ) func Test_PolicyValidate(t *testing.T) { @@ -421,3 +424,192 @@ func mustParseWeightedURLs(t testing.TB, urls ...string) []WeightedURL { require.NoError(t, err) return wu } + +func TestRouteID(t *testing.T) { + randomString := func() string { + return strings.TrimSuffix(cryptutil.NewRandomStringN(mathrand.IntN(31)+1), "=") + } + randomBool := func() bool { + return mathrand.N(2) == 0 + } + randomURL := func() *url.URL { + u, err := url.Parse(fmt.Sprintf("https://%s.example.com/%s?foo=%s#%s", + randomString(), randomString(), randomString(), randomString())) + require.NoError(t, err) + return u + } + baseFieldMutators := []func(p *Policy){ + func(p *Policy) { p.From = randomString() }, + func(p *Policy) { p.Prefix = randomString() }, + func(p *Policy) { p.Path = randomString() }, + func(p *Policy) { p.Regex = randomString() }, + } + toMutators := func(p *Policy) { + p.To = make(WeightedURLs, mathrand.N(9)+1) + for i := 0; i < len(p.To); i++ { + p.To[i] = WeightedURL{URL: *randomURL(), LbWeight: mathrand.Uint32()} + } + } + redirectMutators := []func(p *PolicyRedirect){ + func(p *PolicyRedirect) { p.HTTPSRedirect = randomPtr(10, randomBool()) }, + func(p *PolicyRedirect) { p.SchemeRedirect = randomPtr(10, randomString()) }, + func(p *PolicyRedirect) { p.HostRedirect = randomPtr(10, randomString()) }, + func(p *PolicyRedirect) { p.PortRedirect = randomPtr(10, mathrand.Uint32()) }, + func(p *PolicyRedirect) { p.PathRedirect = randomPtr(10, randomString()) }, + func(p *PolicyRedirect) { p.PrefixRewrite = randomPtr(10, randomString()) }, + func(p *PolicyRedirect) { p.ResponseCode = randomPtr(10, mathrand.Int32()) }, + func(p *PolicyRedirect) { p.StripQuery = randomPtr(10, randomBool()) }, + } + responseMutators := []func(p *DirectResponse){ + func(p *DirectResponse) { p.Status = mathrand.Int() }, + func(p *DirectResponse) { p.Body = randomString() }, + } + + t.Run("random policies", func(t *testing.T) { + hashes := make(map[uint64]struct{}, 10000) + for i := 0; i < 10000; i++ { + p := Policy{} + for _, m := range baseFieldMutators { + m(&p) + } + switch mathrand.IntN(3) { + case 0: + toMutators(&p) + case 1: + p.Redirect = &PolicyRedirect{} + for _, m := range redirectMutators { + m(p.Redirect) + } + case 2: + p.Response = &DirectResponse{} + for _, m := range responseMutators { + m(p.Response) + } + } + + routeID, err := p.RouteID() + require.NoError(t, err) + hashes[routeID] = struct{}{} // odds of a collision should be pretty low here + + // check that computing the route id again results in the same value + routeID2, err := p.RouteID() + require.NoError(t, err) + assert.Equal(t, routeID, routeID2) + } + assert.Len(t, hashes, 10000) + }) + t.Run("incremental policy", func(t *testing.T) { + hashes := make(map[uint64]Policy, 5000) + + p := Policy{} + + checkAdd := func(p *Policy) { + routeID, err := p.RouteID() + require.NoError(t, err) + if existing, ok := hashes[routeID]; ok { + require.Equal(t, existing, *p) + } else { + hashes[routeID] = *p + } + + // check that computing the route id again results in the same value + routeID2, err := p.RouteID() + require.NoError(t, err) + assert.Equal(t, routeID, routeID2) + } + + // to + toMutators(&p) + checkAdd(&p) + + // set base fields + for _, m := range baseFieldMutators { + m(&p) + checkAdd(&p) + } + + // redirect + p.To = nil + p.Redirect = &PolicyRedirect{} + for range 1000 { + for _, m := range redirectMutators { + m(p.Redirect) + checkAdd(&p) + } + } + + // update base fields + for _, m := range baseFieldMutators { + m(&p) + checkAdd(&p) + } + + // direct response + p.Redirect = nil + p.Response = &DirectResponse{} + for range 1000 { + for _, m := range responseMutators { + m(p.Response) + checkAdd(&p) + } + } + + // update base fields + for _, m := range baseFieldMutators { + m(&p) + checkAdd(&p) + } + + // sanity check + assert.Greater(t, len(hashes), 2000) + }) + t.Run("field separation", func(t *testing.T) { + cases := []struct { + a, b *Policy + }{ + { + &Policy{From: "foo", Prefix: "bar"}, + &Policy{From: "f", Prefix: "oobar"}, + }, + { + &Policy{From: "foo", Prefix: "bar"}, + &Policy{From: "foobar", Prefix: ""}, + }, + { + &Policy{From: "foobar", Prefix: ""}, + &Policy{From: "", Prefix: "foobar"}, + }, + { + &Policy{From: "foo", Prefix: "", Path: "bar"}, + &Policy{From: "foo", Prefix: "bar", Path: ""}, + }, + { + &Policy{From: "", Prefix: "foo", Path: "bar"}, + &Policy{From: "foo", Prefix: "bar", Path: ""}, + }, + { + &Policy{From: "", Prefix: "foo", Path: "bar"}, + &Policy{From: "foo", Prefix: "", Path: "bar"}, + }, + } + for _, c := range cases { + c.a.To = mustParseWeightedURLs(t, "https://foo") + c.b.To = mustParseWeightedURLs(t, "https://foo") + } + + for _, c := range cases { + a, err := c.a.RouteID() + require.NoError(t, err) + b, err := c.b.RouteID() + require.NoError(t, err) + assert.NotEqual(t, a, b) + } + }) +} + +func randomPtr[T any](nilChance int, t T) *T { + if mathrand.N(nilChance) == 0 { + return nil + } + return &t +} diff --git a/internal/benchmarks/latency_bench_test.go b/internal/benchmarks/latency_bench_test.go index f936d7d8d..489bec2ff 100644 --- a/internal/benchmarks/latency_bench_test.go +++ b/internal/benchmarks/latency_bench_test.go @@ -6,6 +6,7 @@ import ( "io" "math/rand/v2" "net/http" + "runtime" "testing" "github.com/pomerium/pomerium/internal/testenv" @@ -26,6 +27,7 @@ func init() { } func TestRequestLatency(t *testing.T) { + runtime.MemProfileRate = 0 env := testenv.New(t, testenv.Silent()) users := []*scenarios.User{} for i := range numRoutes { @@ -51,6 +53,7 @@ func TestRequestLatency(t *testing.T) { env.Start() snippets.WaitStartupComplete(env) + runtime.MemProfileRate = 512 * 1024 out := testing.Benchmark(func(b *testing.B) { b.ReportAllocs() diff --git a/internal/hashutil/hashutil.go b/internal/hashutil/hashutil.go index d5f0a335d..8c74126fe 100644 --- a/internal/hashutil/hashutil.go +++ b/internal/hashutil/hashutil.go @@ -1,9 +1,13 @@ // Package hashutil provides NON-CRYPTOGRAPHIC utility functions for hashing. // // http://cyan4973.github.io/xxHash/ +// +//nolint:errcheck package hashutil import ( + "encoding/binary" + "github.com/cespare/xxhash/v2" "github.com/mitchellh/hashstructure/v2" ) @@ -27,3 +31,176 @@ func Hash(v any) (uint64, error) { } return hashstructure.Hash(v, hashstructure.FormatV2, opts) } + +type Digest struct { + xxhash.Digest +} + +func NewDigest() *Digest { + var d Digest + d.Reset() + return &d +} + +// WriteStringWithLen writes the string's length, then its contents to the hash. +func (d *Digest) WriteStringWithLen(s string) { + d.WriteInt32(int32(len(s))) + d.WriteString(s) +} + +// WriteStringWithLen writes the byte array's length, then its contents to +// the hash. +func (d *Digest) WriteWithLen(b []byte) { + d.WriteInt32(int32(len(b))) + d.Write(b) +} + +// WriteBool writes a single byte (1 or 0) to the hash. +func (d *Digest) WriteBool(b bool) { + if b { + d.Write([]byte{1}) + } else { + d.Write([]byte{0}) + } +} + +// WriteUint32 writes a uint16 to the hash. +func (d *Digest) WriteUint16(t uint16) { + var buf [2]byte + binary.LittleEndian.PutUint16(buf[:], t) + d.Write(buf[:]) +} + +// WriteUint32 writes a uint32 to the hash. +func (d *Digest) WriteUint32(t uint32) { + var buf [4]byte + binary.LittleEndian.PutUint32(buf[:], t) + d.Write(buf[:]) +} + +// WriteUint32 writes a uint64 to the hash. +func (d *Digest) WriteUint64(t uint64) { + var buf [8]byte + binary.LittleEndian.PutUint64(buf[:], t) + d.Write(buf[:]) +} + +// WriteInt16 writes an int16 to the hash. +func (d *Digest) WriteInt16(t int16) { + var buf [2]byte + binary.LittleEndian.PutUint16(buf[:], uint16(t)) + d.Write(buf[:]) +} + +// WriteInt32 writes an int32 to the hash. +func (d *Digest) WriteInt32(t int32) { + var buf [4]byte + binary.LittleEndian.PutUint32(buf[:], uint32(t)) + d.Write(buf[:]) +} + +// WriteInt64 writes an int64 to the hash. +func (d *Digest) WriteInt64(t int64) { + var buf [8]byte + binary.LittleEndian.PutUint64(buf[:], uint64(t)) + d.Write(buf[:]) +} + +// WriteStringPtr writes one byte (1 or 0) indicating whether the pointer is non-nil, +// followed by the value if present. +func (d *Digest) WriteStringPtr(t *string) { + if t == nil { + d.Write([]byte{0}) + } else { + d.Write([]byte{1}) + d.WriteString(*t) + } +} + +// WriteStringPtr writes one byte (1 or 0) indicating whether the pointer is non-nil, +// followed by the string's length and value, if present. +func (d *Digest) WriteStringPtrWithLen(t *string) { + if t == nil { + d.Write([]byte{0}) + } else { + d.Write([]byte{1}) + d.WriteStringWithLen(*t) + } +} + +// WriteBoolPtr writes one byte (1 or 0) indicating whether the pointer is non-nil, +// followed by the value if present. +func (d *Digest) WriteBoolPtr(t *bool) { + if t == nil { + d.Write([]byte{0}) + } else { + d.Write([]byte{1}) + d.WriteBool(*t) + } +} + +// WriteUint16Ptr writes one byte (1 or 0) indicating whether the pointer is non-nil, +// followed by the value if present. +func (d *Digest) WriteUint16Ptr(t *uint16) { + if t == nil { + d.Write([]byte{0}) + } else { + d.Write([]byte{1}) + d.WriteUint16(*t) + } +} + +// WriteUint32Ptr writes one byte (1 or 0) indicating whether the pointer is non-nil, +// followed by the value if present. +func (d *Digest) WriteUint32Ptr(t *uint32) { + if t == nil { + d.Write([]byte{0}) + } else { + d.Write([]byte{1}) + d.WriteUint32(*t) + } +} + +// WriteUint64Ptr writes one byte (1 or 0) indicating whether the pointer is non-nil, +// followed by the value if present. +func (d *Digest) WriteUint64Ptr(t *uint64) { + if t == nil { + d.Write([]byte{0}) + } else { + d.Write([]byte{1}) + d.WriteUint64(*t) + } +} + +// WriteInt16Ptr writes one byte (1 or 0) indicating whether the pointer is non-nil, +// followed by the value if present. +func (d *Digest) WriteInt16Ptr(t *int16) { + if t == nil { + d.Write([]byte{0}) + } else { + d.Write([]byte{1}) + d.WriteInt16(*t) + } +} + +// WriteInt32Ptr writes one byte (1 or 0) indicating whether the pointer is non-nil, +// followed by the value if present. +func (d *Digest) WriteInt32Ptr(t *int32) { + if t == nil { + d.Write([]byte{0}) + } else { + d.Write([]byte{1}) + d.WriteInt32(*t) + } +} + +// WriteInt64Ptr writes one byte (1 or 0) indicating whether the pointer is non-nil, +// followed by the value if present. +func (d *Digest) WriteInt64Ptr(t *int64) { + if t == nil { + d.Write([]byte{0}) + } else { + d.Write([]byte{1}) + d.WriteInt64(*t) + } +}