From f2b1d49150d05b1462a45a29360f96fc51d151f6 Mon Sep 17 00:00:00 2001 From: Cameron Moore Date: Fri, 5 Apr 2024 18:53:05 -0500 Subject: [PATCH 01/19] Normalize template context for external_template Update the template context for the external_template so that it has the same context as the built-in templates. Fixes #46 --- cmd/dmarc-report-converter/output.go | 32 +++++++++++++++++++--------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/cmd/dmarc-report-converter/output.go b/cmd/dmarc-report-converter/output.go index 5f2d643..1188f43 100644 --- a/cmd/dmarc-report-converter/output.go +++ b/cmd/dmarc-report-converter/output.go @@ -3,6 +3,7 @@ package main import ( "bytes" "encoding/json" + "encoding/xml" "fmt" "io" "log" @@ -65,12 +66,28 @@ func (o *output) do(d dmarc.Report) error { } func (o *output) template(d dmarc.Report) error { - err := o.cfg.Output.template.Execute(o.w, d) - if err != nil { - return err + data := struct { + AssetsPath string + Report dmarc.Report + + // Deprecated + XMLName xml.Name + ReportMetadata dmarc.ReportMetadata + PolicyPublished dmarc.PolicyPublished + Records []dmarc.Record + MessagesStats dmarc.MessagesStats + }{ + o.cfg.Output.AssetsPath, + d, + + d.XMLName, + d.ReportMetadata, + d.PolicyPublished, + d.Records, + d.MessagesStats, } - return nil + return o.cfg.Output.template.Execute(o.w, data) } func (o *output) templateHTML(d dmarc.Report) error { @@ -79,12 +96,7 @@ func (o *output) templateHTML(d dmarc.Report) error { Report dmarc.Report }{o.cfg.Output.AssetsPath, d} - err := o.cfg.Output.template.Execute(o.w, data) - if err != nil { - return err - } - - return nil + return o.cfg.Output.template.Execute(o.w, data) } func (o *output) json(d dmarc.Report) error { From d6f31e7a263e5bf5b7f7d5fd31f8dca6016eee15 Mon Sep 17 00:00:00 2001 From: Cameron Moore Date: Fri, 5 Apr 2024 19:54:46 -0500 Subject: [PATCH 02/19] Add concurrent DNS lookup support Add a limited worker pool to do concurrent DNS lookups. Add a `lookup_limit` config parameter to control pool size (default=50). I'm using `github.com/sourcegraph/conc` to simplify the goroutine management. Since `conc` uses generics, I updated the minimum Go version in `go.mod` to 1.18. Updates #10 --- README.md | 2 + cmd/dmarc-report-converter/config.go | 5 + cmd/dmarc-report-converter/files.go | 34 + config/config.dist.yaml | 3 + go.mod | 13 +- go.sum | 22 +- pkg/dmarc/dmarc.go | 21 - vendor/github.com/emersion/go-imap/go.mod | 9 - vendor/github.com/emersion/go-imap/go.sum | 10 - vendor/github.com/emersion/go-message/go.mod | 8 - vendor/github.com/emersion/go-message/go.sum | 34 - vendor/github.com/emersion/go-sasl/go.mod | 3 - vendor/github.com/hashicorp/logutils/go.mod | 1 - .../github.com/sourcegraph/conc/.golangci.yml | 11 + vendor/github.com/sourcegraph/conc/LICENSE | 21 + vendor/github.com/sourcegraph/conc/README.md | 464 ++++++++++++ .../internal/multierror/multierror_go119.go | 10 + .../internal/multierror/multierror_go120.go | 10 + .../sourcegraph/conc/panics/panics.go | 102 +++ .../github.com/sourcegraph/conc/panics/try.go | 11 + .../sourcegraph/conc/pool/context_pool.go | 94 +++ .../sourcegraph/conc/pool/error_pool.go | 97 +++ .../github.com/sourcegraph/conc/pool/pool.go | 168 +++++ .../conc/pool/result_context_pool.go | 75 ++ .../conc/pool/result_error_pool.go | 80 ++ .../sourcegraph/conc/pool/result_pool.go | 93 +++ .../github.com/sourcegraph/conc/waitgroup.go | 52 ++ vendor/go.uber.org/atomic/.codecov.yml | 19 + vendor/go.uber.org/atomic/.gitignore | 12 + vendor/go.uber.org/atomic/.travis.yml | 27 + vendor/go.uber.org/atomic/CHANGELOG.md | 76 ++ vendor/go.uber.org/atomic/LICENSE.txt | 19 + vendor/go.uber.org/atomic/Makefile | 78 ++ vendor/go.uber.org/atomic/README.md | 63 ++ vendor/go.uber.org/atomic/bool.go | 81 +++ vendor/go.uber.org/atomic/bool_ext.go | 53 ++ vendor/go.uber.org/atomic/doc.go | 23 + vendor/go.uber.org/atomic/duration.go | 82 +++ vendor/go.uber.org/atomic/duration_ext.go | 40 + vendor/go.uber.org/atomic/error.go | 51 ++ vendor/go.uber.org/atomic/error_ext.go | 39 + vendor/go.uber.org/atomic/float64.go | 76 ++ vendor/go.uber.org/atomic/float64_ext.go | 47 ++ vendor/go.uber.org/atomic/gen.go | 26 + vendor/go.uber.org/atomic/int32.go | 102 +++ vendor/go.uber.org/atomic/int64.go | 102 +++ vendor/go.uber.org/atomic/nocmp.go | 35 + vendor/go.uber.org/atomic/string.go | 54 ++ vendor/go.uber.org/atomic/string_ext.go | 43 ++ vendor/go.uber.org/atomic/uint32.go | 102 +++ vendor/go.uber.org/atomic/uint64.go | 102 +++ vendor/go.uber.org/atomic/value.go | 31 + vendor/go.uber.org/multierr/.codecov.yml | 15 + vendor/go.uber.org/multierr/.gitignore | 4 + vendor/go.uber.org/multierr/CHANGELOG.md | 80 ++ vendor/go.uber.org/multierr/LICENSE.txt | 19 + vendor/go.uber.org/multierr/Makefile | 38 + vendor/go.uber.org/multierr/README.md | 23 + vendor/go.uber.org/multierr/error.go | 681 ++++++++++++++++++ vendor/go.uber.org/multierr/glide.yaml | 8 + vendor/gopkg.in/yaml.v2/go.mod | 5 - vendor/modules.txt | 21 + 62 files changed, 3636 insertions(+), 94 deletions(-) delete mode 100644 vendor/github.com/emersion/go-imap/go.mod delete mode 100644 vendor/github.com/emersion/go-imap/go.sum delete mode 100644 vendor/github.com/emersion/go-message/go.mod delete mode 100644 vendor/github.com/emersion/go-message/go.sum delete mode 100644 vendor/github.com/emersion/go-sasl/go.mod delete mode 100644 vendor/github.com/hashicorp/logutils/go.mod create mode 100644 vendor/github.com/sourcegraph/conc/.golangci.yml create mode 100644 vendor/github.com/sourcegraph/conc/LICENSE create mode 100644 vendor/github.com/sourcegraph/conc/README.md create mode 100644 vendor/github.com/sourcegraph/conc/internal/multierror/multierror_go119.go create mode 100644 vendor/github.com/sourcegraph/conc/internal/multierror/multierror_go120.go create mode 100644 vendor/github.com/sourcegraph/conc/panics/panics.go create mode 100644 vendor/github.com/sourcegraph/conc/panics/try.go create mode 100644 vendor/github.com/sourcegraph/conc/pool/context_pool.go create mode 100644 vendor/github.com/sourcegraph/conc/pool/error_pool.go create mode 100644 vendor/github.com/sourcegraph/conc/pool/pool.go create mode 100644 vendor/github.com/sourcegraph/conc/pool/result_context_pool.go create mode 100644 vendor/github.com/sourcegraph/conc/pool/result_error_pool.go create mode 100644 vendor/github.com/sourcegraph/conc/pool/result_pool.go create mode 100644 vendor/github.com/sourcegraph/conc/waitgroup.go create mode 100644 vendor/go.uber.org/atomic/.codecov.yml create mode 100644 vendor/go.uber.org/atomic/.gitignore create mode 100644 vendor/go.uber.org/atomic/.travis.yml create mode 100644 vendor/go.uber.org/atomic/CHANGELOG.md create mode 100644 vendor/go.uber.org/atomic/LICENSE.txt create mode 100644 vendor/go.uber.org/atomic/Makefile create mode 100644 vendor/go.uber.org/atomic/README.md create mode 100644 vendor/go.uber.org/atomic/bool.go create mode 100644 vendor/go.uber.org/atomic/bool_ext.go create mode 100644 vendor/go.uber.org/atomic/doc.go create mode 100644 vendor/go.uber.org/atomic/duration.go create mode 100644 vendor/go.uber.org/atomic/duration_ext.go create mode 100644 vendor/go.uber.org/atomic/error.go create mode 100644 vendor/go.uber.org/atomic/error_ext.go create mode 100644 vendor/go.uber.org/atomic/float64.go create mode 100644 vendor/go.uber.org/atomic/float64_ext.go create mode 100644 vendor/go.uber.org/atomic/gen.go create mode 100644 vendor/go.uber.org/atomic/int32.go create mode 100644 vendor/go.uber.org/atomic/int64.go create mode 100644 vendor/go.uber.org/atomic/nocmp.go create mode 100644 vendor/go.uber.org/atomic/string.go create mode 100644 vendor/go.uber.org/atomic/string_ext.go create mode 100644 vendor/go.uber.org/atomic/uint32.go create mode 100644 vendor/go.uber.org/atomic/uint64.go create mode 100644 vendor/go.uber.org/atomic/value.go create mode 100644 vendor/go.uber.org/multierr/.codecov.yml create mode 100644 vendor/go.uber.org/multierr/.gitignore create mode 100644 vendor/go.uber.org/multierr/CHANGELOG.md create mode 100644 vendor/go.uber.org/multierr/LICENSE.txt create mode 100644 vendor/go.uber.org/multierr/Makefile create mode 100644 vendor/go.uber.org/multierr/README.md create mode 100644 vendor/go.uber.org/multierr/error.go create mode 100644 vendor/go.uber.org/multierr/glide.yaml delete mode 100644 vendor/gopkg.in/yaml.v2/go.mod diff --git a/README.md b/README.md index 750d077..3e0d99e 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,8 @@ Copy config/config.dist.yaml to config.yaml and change parameters: * **lookup_addr** (bool): perform reverse lookup? If enabled, may take some time. +* **lookup_limit** (int): limit lookup pool size; must be positive; default = 50 + * **merge_reports** (bool): merge multiple similar reports to one? * **log_debug** (bool): print debug log messages? diff --git a/cmd/dmarc-report-converter/config.go b/cmd/dmarc-report-converter/config.go index 4bec34d..bda5ece 100644 --- a/cmd/dmarc-report-converter/config.go +++ b/cmd/dmarc-report-converter/config.go @@ -13,6 +13,7 @@ type config struct { Input Input `yaml:"input"` Output Output `yaml:"output"` LookupAddr bool `yaml:"lookup_addr"` + LookupLimit int `yaml:"lookup_limit"` MergeReports bool `yaml:"merge_reports"` LogDebug bool `yaml:"log_debug"` LogDatetime bool `yaml:"log_datetime"` @@ -72,6 +73,10 @@ func loadConfig(path string) (*config, error) { return nil, err } + if c.LookupLimit < 1 { + c.LookupLimit = 50 + } + if c.Input.Dir == "" { return nil, fmt.Errorf("input.dir is not configured") } diff --git a/cmd/dmarc-report-converter/files.go b/cmd/dmarc-report-converter/files.go index 3d72c11..f2068d4 100644 --- a/cmd/dmarc-report-converter/files.go +++ b/cmd/dmarc-report-converter/files.go @@ -2,9 +2,12 @@ package main import ( "log" + "net" "os" "path/filepath" + "time" + "github.com/sourcegraph/conc/pool" "github.com/tierpod/dmarc-report-converter/pkg/dmarc" ) @@ -34,6 +37,12 @@ func (c *filesConverter) ConvertWrite() error { c.convert() + if c.cfg.LookupAddr { + start := time.Now() + c.doDNSLookups() + log.Printf("[INFO] DNS lookups completed in %v", time.Since(start)) + } + if c.cfg.MergeReports { err = c.merge() if err != nil { @@ -53,6 +62,31 @@ func (c *filesConverter) ConvertWrite() error { return nil } +// doDNSLookups uses a limited goroutine pool to do concurrent DNS lookups. +func (c *filesConverter) doDNSLookups() { + p := pool.New().WithMaxGoroutines(c.cfg.LookupLimit) + + for _, r := range c.reports { + r := r + start := time.Now() + + for i, record := range r.Records { + i := i + record := record + + p.Go(func() { + hostnames, err := net.LookupAddr(record.Row.SourceIP) + if err == nil { + r.Records[i].Row.SourceHostname = hostnames[0] + } + }) + } + log.Printf("[DEBUG] files: %d DNS lookups for %s in %v", len(r.Records), r.ReportMetadata.OrgName, time.Since(start)) + } + + p.Wait() +} + func (c *filesConverter) find() error { emlFiles, err := filepath.Glob(filepath.Join(c.cfg.Input.Dir, "*.eml")) if err != nil { diff --git a/config/config.dist.yaml b/config/config.dist.yaml index 6b7eaa4..4397f38 100644 --- a/config/config.dist.yaml +++ b/config/config.dist.yaml @@ -30,6 +30,9 @@ output: # perform reverse lookups? lookup_addr: no +# limit lookup pool size; must be positive; default = 50 +#lookup_limit: 50 + # merge multiple similar reports to one? merge_reports: yes diff --git a/go.mod b/go.mod index 625aceb..0116638 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,20 @@ module github.com/tierpod/dmarc-report-converter -go 1.13 +go 1.18 require ( github.com/emersion/go-imap v1.2.1 github.com/emersion/go-message v0.18.0 - github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 // indirect github.com/hashicorp/logutils v1.0.0 + github.com/sourcegraph/conc v0.3.0 gopkg.in/yaml.v2 v2.4.0 ) + +require ( + github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 // indirect + github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect + github.com/kr/text v0.2.0 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/text v0.14.0 // indirect +) diff --git a/go.sum b/go.sum index ecf609d..786cc59 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA= github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY= github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4= @@ -10,7 +14,22 @@ github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwo github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -43,7 +62,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/pkg/dmarc/dmarc.go b/pkg/dmarc/dmarc.go index d106962..ed57b10 100644 --- a/pkg/dmarc/dmarc.go +++ b/pkg/dmarc/dmarc.go @@ -6,7 +6,6 @@ import ( "encoding/xml" "fmt" "math" - "net" "sort" "time" ) @@ -225,27 +224,7 @@ func Parse(b []byte, lookupAddr bool) (Report, error) { } r.SortRecords() - - if lookupAddr { - doPTRlookup(&r) - } - r.CalculateStats() return r, nil } - -func doPTRlookup(r *Report) error { - for i, record := range r.Records { - var hostname string - hostnames, err := net.LookupAddr(record.Row.SourceIP) - if err != nil { - hostname = "" - } else { - hostname = hostnames[0] - } - r.Records[i].Row.SourceHostname = hostname - } - - return nil -} diff --git a/vendor/github.com/emersion/go-imap/go.mod b/vendor/github.com/emersion/go-imap/go.mod deleted file mode 100644 index 9b7f79b..0000000 --- a/vendor/github.com/emersion/go-imap/go.mod +++ /dev/null @@ -1,9 +0,0 @@ -module github.com/emersion/go-imap - -go 1.13 - -require ( - github.com/emersion/go-message v0.15.0 - github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 - golang.org/x/text v0.3.7 -) diff --git a/vendor/github.com/emersion/go-imap/go.sum b/vendor/github.com/emersion/go-imap/go.sum deleted file mode 100644 index 7acaab2..0000000 --- a/vendor/github.com/emersion/go-imap/go.sum +++ /dev/null @@ -1,10 +0,0 @@ -github.com/emersion/go-message v0.15.0 h1:urgKGqt2JAc9NFJcgncQcohHdiYb803YTH9OQwHBHIY= -github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4= -github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ= -github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= -github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY= -github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/vendor/github.com/emersion/go-message/go.mod b/vendor/github.com/emersion/go-message/go.mod deleted file mode 100644 index 4491717..0000000 --- a/vendor/github.com/emersion/go-message/go.mod +++ /dev/null @@ -1,8 +0,0 @@ -module github.com/emersion/go-message - -go 1.14 - -require ( - github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 - golang.org/x/text v0.14.0 -) diff --git a/vendor/github.com/emersion/go-message/go.sum b/vendor/github.com/emersion/go-message/go.sum deleted file mode 100644 index bb451ed..0000000 --- a/vendor/github.com/emersion/go-message/go.sum +++ /dev/null @@ -1,34 +0,0 @@ -github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY= -github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/vendor/github.com/emersion/go-sasl/go.mod b/vendor/github.com/emersion/go-sasl/go.mod deleted file mode 100644 index dc3c9a4..0000000 --- a/vendor/github.com/emersion/go-sasl/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/emersion/go-sasl - -go 1.12 diff --git a/vendor/github.com/hashicorp/logutils/go.mod b/vendor/github.com/hashicorp/logutils/go.mod deleted file mode 100644 index ba38a45..0000000 --- a/vendor/github.com/hashicorp/logutils/go.mod +++ /dev/null @@ -1 +0,0 @@ -module github.com/hashicorp/logutils diff --git a/vendor/github.com/sourcegraph/conc/.golangci.yml b/vendor/github.com/sourcegraph/conc/.golangci.yml new file mode 100644 index 0000000..ae65a76 --- /dev/null +++ b/vendor/github.com/sourcegraph/conc/.golangci.yml @@ -0,0 +1,11 @@ +linters: + disable-all: true + enable: + - errcheck + - godot + - gosimple + - govet + - ineffassign + - staticcheck + - typecheck + - unused diff --git a/vendor/github.com/sourcegraph/conc/LICENSE b/vendor/github.com/sourcegraph/conc/LICENSE new file mode 100644 index 0000000..1081f4e --- /dev/null +++ b/vendor/github.com/sourcegraph/conc/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Sourcegraph + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/sourcegraph/conc/README.md b/vendor/github.com/sourcegraph/conc/README.md new file mode 100644 index 0000000..1c87c3c --- /dev/null +++ b/vendor/github.com/sourcegraph/conc/README.md @@ -0,0 +1,464 @@ +![conch](https://user-images.githubusercontent.com/12631702/210295964-785cc63d-d697-420c-99ff-f492eb81dec9.svg) + +# `conc`: better structured concurrency for go + +[![Go Reference](https://pkg.go.dev/badge/github.com/sourcegraph/conc.svg)](https://pkg.go.dev/github.com/sourcegraph/conc) +[![Sourcegraph](https://img.shields.io/badge/view%20on-sourcegraph-A112FE?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAEZklEQVRoQ+2aXWgUZxSG3292sxtNN43BhBakFPyhxSujRSxiU1pr7SaGXqgUxOIEW0IFkeYighYUxAuLUlq0lrq2iCDpjWtmFVtoG6QVNOCFVShVLyxIk0DVjZLMxt3xTGTccd2ZOd/8JBHci0CY9zvnPPN+/7sCIXwKavOwAcy2QgngQiIztDSE0OwQlDPYR1ebiaH6J5kZChyfW12gRG4QVgGTBfMchMbFP9Sn5nlZL2D0JjLD6710lc+z0NfqSGTXQRQ4bX07Mq423yoBL3OSyHSvUxirMuaEvgbJWrdcvkHMoJwxYuq4INUhyuWvQa1jvdMGxAvCxJlyEC9XOBCWL04wwRzpbDoDQ7wfZJzIQLi5Eggk6DiRhZgWIAbE3NrM4A3LPT8Q7UgqAqLqTmLSHLGPkyzG/qXEczhd0q6RH+zaSBfaUoc4iQx19pIClIscrTkNZzG6gd7qMY6eC2Hqyo705ZfTf+eqJmhMzcSbYtQpOXc92ZsZjLVAL4YNUQbJ5Ttg4CQrQdGYj44Xr9m1XJCzmZusFDJOWNpHjmh5x624a2ZFtOKDVL+uNo2TuXE3bZQQZUf8gtgqP31uI94Z/rMqix+IGiRfWw3xN9dCgVx+L3WrHm4Dju6PXz/EkjuXJ6R+IGgyOE1TbZqTq9y1eo0EZo7oMo1ktPu3xjHvuiLT5AFNszUyDULtWpzE2/fEsey8O5TbWuGWwxrs5rS7nFNMWJrNh2No74s9Ec4vRNmRRzPXMP19fBMSVsGcOJ98G8N3Wl2gXcbTjbX7vUBxLaeASDQCm5Cu/0E2tvtb0Ea+BowtskFD0wvlc6Rf2M+Jx7dTu7ubFr2dnKDRaMQe2v/tcIrNB7FH0O50AcrBaApmRDVwFO31ql3pD8QW4dP0feNwl/Q+kFEtRyIGyaWXnpy1OO0qNJWHo1y6iCmAGkBb/Ru+HenDWIF2mo4r8G+tRRzoniSn2uqFLxANhe9LKHVyTbz6egk9+x5w5fK6ulSNNMhZ/Feno+GebLZV6isTTa6k5qNl5RnZ5u56Ib6SBvFzaWBBVFZzvnERWlt/Cg4l27XChLCqFyLekjhy6xJyoytgjPf7opIB8QPx7sYFiMXHPGt76m741MhCKMZfng0nBOIjmoJPsLqWHwgFpe6V6qtfcopxveR2Oy+J0ntIN/zCWkf8QNAJ7y6d8Bq4lxLc2/qJl5K7t432XwcqX5CrI34gzATWuYILQtdQPyePDK3iuOekCR3Efjhig1B1Uq5UoXEEoZX7d1q535J5S9VOeFyYyEBku5XTMXXKQTToX5Rg7OI44nbW5oKYeYK4EniMeF0YFNSmb+grhc84LyRCEP1/OurOcipCQbKxDeK2V5FcVyIDMQvsgz5gwFhcWWwKyRlvQ3gv29RwWoDYAbIofNyBxI9eDlQ+n3YgsgCWnr4MStGXQXmv9pF2La/k3OccV54JEBM4yp9EsXa/3LfO0dGPcYq0Y7DfZB8nJzZw2rppHgKgVHs8L5wvRwAAAABJRU5ErkJggg==)](https://sourcegraph.com/github.com/sourcegraph/conc) +[![Go Report Card](https://goreportcard.com/badge/github.com/sourcegraph/conc)](https://goreportcard.com/report/github.com/sourcegraph/conc) +[![codecov](https://codecov.io/gh/sourcegraph/conc/branch/main/graph/badge.svg?token=MQZTEA1QWT)](https://codecov.io/gh/sourcegraph/conc) +[![Discord](https://img.shields.io/badge/discord-chat-%235765F2)](https://discord.gg/bvXQXmtRjN) + +`conc` is your toolbelt for structured concurrency in go, making common tasks +easier and safer. + +```sh +go get github.com/sourcegraph/conc +``` + +# At a glance + +- Use [`conc.WaitGroup`](https://pkg.go.dev/github.com/sourcegraph/conc#WaitGroup) if you just want a safer version of `sync.WaitGroup` +- Use [`pool.Pool`](https://pkg.go.dev/github.com/sourcegraph/conc/pool#Pool) if you want a concurrency-limited task runner +- Use [`pool.ResultPool`](https://pkg.go.dev/github.com/sourcegraph/conc/pool#ResultPool) if you want a concurrent task runner that collects task results +- Use [`pool.(Result)?ErrorPool`](https://pkg.go.dev/github.com/sourcegraph/conc/pool#ErrorPool) if your tasks are fallible +- Use [`pool.(Result)?ContextPool`](https://pkg.go.dev/github.com/sourcegraph/conc/pool#ContextPool) if your tasks should be canceled on failure +- Use [`stream.Stream`](https://pkg.go.dev/github.com/sourcegraph/conc/stream#Stream) if you want to process an ordered stream of tasks in parallel with serial callbacks +- Use [`iter.Map`](https://pkg.go.dev/github.com/sourcegraph/conc/iter#Map) if you want to concurrently map a slice +- Use [`iter.ForEach`](https://pkg.go.dev/github.com/sourcegraph/conc/iter#ForEach) if you want to concurrently iterate over a slice +- Use [`panics.Catcher`](https://pkg.go.dev/github.com/sourcegraph/conc/panics#Catcher) if you want to catch panics in your own goroutines + +All pools are created with +[`pool.New()`](https://pkg.go.dev/github.com/sourcegraph/conc/pool#New) +or +[`pool.NewWithResults[T]()`](https://pkg.go.dev/github.com/sourcegraph/conc/pool#NewWithResults), +then configured with methods: + +- [`p.WithMaxGoroutines()`](https://pkg.go.dev/github.com/sourcegraph/conc/pool#Pool.MaxGoroutines) configures the maximum number of goroutines in the pool +- [`p.WithErrors()`](https://pkg.go.dev/github.com/sourcegraph/conc/pool#Pool.WithErrors) configures the pool to run tasks that return errors +- [`p.WithContext(ctx)`](https://pkg.go.dev/github.com/sourcegraph/conc/pool#Pool.WithContext) configures the pool to run tasks that should be canceled on first error +- [`p.WithFirstError()`](https://pkg.go.dev/github.com/sourcegraph/conc/pool#ErrorPool.WithFirstError) configures error pools to only keep the first returned error rather than an aggregated error +- [`p.WithCollectErrored()`](https://pkg.go.dev/github.com/sourcegraph/conc/pool#ResultContextPool.WithCollectErrored) configures result pools to collect results even when the task errored + +# Goals + +The main goals of the package are: +1) Make it harder to leak goroutines +2) Handle panics gracefully +3) Make concurrent code easier to read + +## Goal #1: Make it harder to leak goroutines + +A common pain point when working with goroutines is cleaning them up. It's +really easy to fire off a `go` statement and fail to properly wait for it to +complete. + +`conc` takes the opinionated stance that all concurrency should be scoped. +That is, goroutines should have an owner and that owner should always +ensure that its owned goroutines exit properly. + +In `conc`, the owner of a goroutine is always a `conc.WaitGroup`. Goroutines +are spawned in a `WaitGroup` with `(*WaitGroup).Go()`, and +`(*WaitGroup).Wait()` should always be called before the `WaitGroup` goes out +of scope. + +In some cases, you might want a spawned goroutine to outlast the scope of the +caller. In that case, you could pass a `WaitGroup` into the spawning function. + +```go +func main() { + var wg conc.WaitGroup + defer wg.Wait() + + startTheThing(&wg) +} + +func startTheThing(wg *conc.WaitGroup) { + wg.Go(func() { ... }) +} +``` + +For some more discussion on why scoped concurrency is nice, check out [this +blog +post](https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/). + +## Goal #2: Handle panics gracefully + +A frequent problem with goroutines in long-running applications is handling +panics. A goroutine spawned without a panic handler will crash the whole process +on panic. This is usually undesirable. + +However, if you do add a panic handler to a goroutine, what do you do with the +panic once you catch it? Some options: +1) Ignore it +2) Log it +3) Turn it into an error and return that to the goroutine spawner +4) Propagate the panic to the goroutine spawner + +Ignoring panics is a bad idea since panics usually mean there is actually +something wrong and someone should fix it. + +Just logging panics isn't great either because then there is no indication to the spawner +that something bad happened, and it might just continue on as normal even though your +program is in a really bad state. + +Both (3) and (4) are reasonable options, but both require the goroutine to have +an owner that can actually receive the message that something went wrong. This +is generally not true with a goroutine spawned with `go`, but in the `conc` +package, all goroutines have an owner that must collect the spawned goroutine. +In the conc package, any call to `Wait()` will panic if any of the spawned goroutines +panicked. Additionally, it decorates the panic value with a stacktrace from the child +goroutine so that you don't lose information about what caused the panic. + +Doing this all correctly every time you spawn something with `go` is not +trivial and it requires a lot of boilerplate that makes the important parts of +the code more difficult to read, so `conc` does this for you. + + + + + + + + + + +
stdlibconc
+ +```go +type caughtPanicError struct { + val any + stack []byte +} + +func (e *caughtPanicError) Error() string { + return fmt.Sprintf( + "panic: %q\n%s", + e.val, + string(e.stack) + ) +} + +func main() { + done := make(chan error) + go func() { + defer func() { + if v := recover(); v != nil { + done <- &caughtPanicError{ + val: v, + stack: debug.Stack() + } + } else { + done <- nil + } + }() + doSomethingThatMightPanic() + }() + err := <-done + if err != nil { + panic(err) + } +} +``` + + +```go +func main() { + var wg conc.WaitGroup + wg.Go(doSomethingThatMightPanic) + // panics with a nice stacktrace + wg.Wait() +} +``` +
+ +## Goal #3: Make concurrent code easier to read + +Doing concurrency correctly is difficult. Doing it in a way that doesn't +obfuscate what the code is actually doing is more difficult. The `conc` package +attempts to make common operations easier by abstracting as much boilerplate +complexity as possible. + +Want to run a set of concurrent tasks with a bounded set of goroutines? Use +`pool.New()`. Want to process an ordered stream of results concurrently, but +still maintain order? Try `stream.New()`. What about a concurrent map over +a slice? Take a peek at `iter.Map()`. + +Browse some examples below for some comparisons with doing these by hand. + +# Examples + +Each of these examples forgoes propagating panics for simplicity. To see +what kind of complexity that would add, check out the "Goal #2" header above. + +Spawn a set of goroutines and waiting for them to finish: + + + + + + + + + + +
stdlibconc
+ +```go +func main() { + var wg sync.WaitGroup + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + defer wg.Done() + // crashes on panic! + doSomething() + }() + } + wg.Wait() +} +``` + + +```go +func main() { + var wg conc.WaitGroup + for i := 0; i < 10; i++ { + wg.Go(doSomething) + } + wg.Wait() +} +``` +
+ +Process each element of a stream in a static pool of goroutines: + + + + + + + + + + +
stdlibconc
+ +```go +func process(stream chan int) { + var wg sync.WaitGroup + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for elem := range stream { + handle(elem) + } + }() + } + wg.Wait() +} +``` + + +```go +func process(stream chan int) { + p := pool.New().WithMaxGoroutines(10) + for elem := range stream { + elem := elem + p.Go(func() { + handle(elem) + }) + } + p.Wait() +} +``` +
+ +Process each element of a slice in a static pool of goroutines: + + + + + + + + + + +
stdlibconc
+ +```go +func process(values []int) { + feeder := make(chan int, 8) + + var wg sync.WaitGroup + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for elem := range feeder { + handle(elem) + } + }() + } + + for _, value := range values { + feeder <- value + } + close(feeder) + wg.Wait() +} +``` + + +```go +func process(values []int) { + iter.ForEach(values, handle) +} +``` +
+ +Concurrently map a slice: + + + + + + + + + + +
stdlibconc
+ +```go +func concMap( + input []int, + f func(int) int, +) []int { + res := make([]int, len(input)) + var idx atomic.Int64 + + var wg sync.WaitGroup + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + defer wg.Done() + + for { + i := int(idx.Add(1) - 1) + if i >= len(input) { + return + } + + res[i] = f(input[i]) + } + }() + } + wg.Wait() + return res +} +``` + + +```go +func concMap( + input []int, + f func(*int) int, +) []int { + return iter.Map(input, f) +} +``` +
+ +Process an ordered stream concurrently: + + + + + + + + + + + +
stdlibconc
+ +```go +func mapStream( + in chan int, + out chan int, + f func(int) int, +) { + tasks := make(chan func()) + taskResults := make(chan chan int) + + // Worker goroutines + var workerWg sync.WaitGroup + for i := 0; i < 10; i++ { + workerWg.Add(1) + go func() { + defer workerWg.Done() + for task := range tasks { + task() + } + }() + } + + // Ordered reader goroutines + var readerWg sync.WaitGroup + readerWg.Add(1) + go func() { + defer readerWg.Done() + for result := range taskResults { + item := <-result + out <- item + } + }() + + // Feed the workers with tasks + for elem := range in { + resultCh := make(chan int, 1) + taskResults <- resultCh + tasks <- func() { + resultCh <- f(elem) + } + } + + // We've exhausted input. + // Wait for everything to finish + close(tasks) + workerWg.Wait() + close(taskResults) + readerWg.Wait() +} +``` + + +```go +func mapStream( + in chan int, + out chan int, + f func(int) int, +) { + s := stream.New().WithMaxGoroutines(10) + for elem := range in { + elem := elem + s.Go(func() stream.Callback { + res := f(elem) + return func() { out <- res } + }) + } + s.Wait() +} +``` +
+ +# Status + +This package is currently pre-1.0. There are likely to be minor breaking +changes before a 1.0 release as we stabilize the APIs and tweak defaults. +Please open an issue if you have questions, concerns, or requests that you'd +like addressed before the 1.0 release. Currently, a 1.0 is targeted for +March 2023. diff --git a/vendor/github.com/sourcegraph/conc/internal/multierror/multierror_go119.go b/vendor/github.com/sourcegraph/conc/internal/multierror/multierror_go119.go new file mode 100644 index 0000000..7087e32 --- /dev/null +++ b/vendor/github.com/sourcegraph/conc/internal/multierror/multierror_go119.go @@ -0,0 +1,10 @@ +//go:build !go1.20 +// +build !go1.20 + +package multierror + +import "go.uber.org/multierr" + +var ( + Join = multierr.Combine +) diff --git a/vendor/github.com/sourcegraph/conc/internal/multierror/multierror_go120.go b/vendor/github.com/sourcegraph/conc/internal/multierror/multierror_go120.go new file mode 100644 index 0000000..39cff82 --- /dev/null +++ b/vendor/github.com/sourcegraph/conc/internal/multierror/multierror_go120.go @@ -0,0 +1,10 @@ +//go:build go1.20 +// +build go1.20 + +package multierror + +import "errors" + +var ( + Join = errors.Join +) diff --git a/vendor/github.com/sourcegraph/conc/panics/panics.go b/vendor/github.com/sourcegraph/conc/panics/panics.go new file mode 100644 index 0000000..abbed7f --- /dev/null +++ b/vendor/github.com/sourcegraph/conc/panics/panics.go @@ -0,0 +1,102 @@ +package panics + +import ( + "fmt" + "runtime" + "runtime/debug" + "sync/atomic" +) + +// Catcher is used to catch panics. You can execute a function with Try, +// which will catch any spawned panic. Try can be called any number of times, +// from any number of goroutines. Once all calls to Try have completed, you can +// get the value of the first panic (if any) with Recovered(), or you can just +// propagate the panic (re-panic) with Repanic(). +type Catcher struct { + recovered atomic.Pointer[Recovered] +} + +// Try executes f, catching any panic it might spawn. It is safe +// to call from multiple goroutines simultaneously. +func (p *Catcher) Try(f func()) { + defer p.tryRecover() + f() +} + +func (p *Catcher) tryRecover() { + if val := recover(); val != nil { + rp := NewRecovered(1, val) + p.recovered.CompareAndSwap(nil, &rp) + } +} + +// Repanic panics if any calls to Try caught a panic. It will panic with the +// value of the first panic caught, wrapped in a panics.Recovered with caller +// information. +func (p *Catcher) Repanic() { + if val := p.Recovered(); val != nil { + panic(val) + } +} + +// Recovered returns the value of the first panic caught by Try, or nil if +// no calls to Try panicked. +func (p *Catcher) Recovered() *Recovered { + return p.recovered.Load() +} + +// NewRecovered creates a panics.Recovered from a panic value and a collected +// stacktrace. The skip parameter allows the caller to skip stack frames when +// collecting the stacktrace. Calling with a skip of 0 means include the call to +// NewRecovered in the stacktrace. +func NewRecovered(skip int, value any) Recovered { + // 64 frames should be plenty + var callers [64]uintptr + n := runtime.Callers(skip+1, callers[:]) + return Recovered{ + Value: value, + Callers: callers[:n], + Stack: debug.Stack(), + } +} + +// Recovered is a panic that was caught with recover(). +type Recovered struct { + // The original value of the panic. + Value any + // The caller list as returned by runtime.Callers when the panic was + // recovered. Can be used to produce a more detailed stack information with + // runtime.CallersFrames. + Callers []uintptr + // The formatted stacktrace from the goroutine where the panic was recovered. + // Easier to use than Callers. + Stack []byte +} + +// String renders a human-readable formatting of the panic. +func (p *Recovered) String() string { + return fmt.Sprintf("panic: %v\nstacktrace:\n%s\n", p.Value, p.Stack) +} + +// AsError casts the panic into an error implementation. The implementation +// is unwrappable with the cause of the panic, if the panic was provided one. +func (p *Recovered) AsError() error { + if p == nil { + return nil + } + return &ErrRecovered{*p} +} + +// ErrRecovered wraps a panics.Recovered in an error implementation. +type ErrRecovered struct{ Recovered } + +var _ error = (*ErrRecovered)(nil) + +func (p *ErrRecovered) Error() string { return p.String() } + +func (p *ErrRecovered) Unwrap() error { + if err, ok := p.Value.(error); ok { + return err + } + return nil +} diff --git a/vendor/github.com/sourcegraph/conc/panics/try.go b/vendor/github.com/sourcegraph/conc/panics/try.go new file mode 100644 index 0000000..4ded92a --- /dev/null +++ b/vendor/github.com/sourcegraph/conc/panics/try.go @@ -0,0 +1,11 @@ +package panics + +// Try executes f, catching and returning any panic it might spawn. +// +// The recovered panic can be propagated with panic(), or handled as a normal error with +// (*panics.Recovered).AsError(). +func Try(f func()) *Recovered { + var c Catcher + c.Try(f) + return c.Recovered() +} diff --git a/vendor/github.com/sourcegraph/conc/pool/context_pool.go b/vendor/github.com/sourcegraph/conc/pool/context_pool.go new file mode 100644 index 0000000..b2d7f8a --- /dev/null +++ b/vendor/github.com/sourcegraph/conc/pool/context_pool.go @@ -0,0 +1,94 @@ +package pool + +import ( + "context" +) + +// ContextPool is a pool that runs tasks that take a context. +// A new ContextPool should be created with `New().WithContext(ctx)`. +// +// The configuration methods (With*) will panic if they are used after calling +// Go() for the first time. +type ContextPool struct { + errorPool ErrorPool + + ctx context.Context + cancel context.CancelFunc + + cancelOnError bool +} + +// Go submits a task. If it returns an error, the error will be +// collected and returned by Wait(). If all goroutines in the pool +// are busy, a call to Go() will block until the task can be started. +func (p *ContextPool) Go(f func(ctx context.Context) error) { + p.errorPool.Go(func() error { + if p.cancelOnError { + // If we are cancelling on error, then we also want to cancel if a + // panic is raised. To do this, we need to recover, cancel, and then + // re-throw the caught panic. + defer func() { + if r := recover(); r != nil { + p.cancel() + panic(r) + } + }() + } + + err := f(p.ctx) + if err != nil && p.cancelOnError { + // Leaky abstraction warning: We add the error directly because + // otherwise, canceling could cause another goroutine to exit and + // return an error before this error was added, which breaks the + // expectations of WithFirstError(). + p.errorPool.addErr(err) + p.cancel() + return nil + } + return err + }) +} + +// Wait cleans up all spawned goroutines, propagates any panics, and +// returns an error if any of the tasks errored. +func (p *ContextPool) Wait() error { + // Make sure we call cancel after pool is done to avoid memory leakage. + defer p.cancel() + return p.errorPool.Wait() +} + +// WithFirstError configures the pool to only return the first error +// returned by a task. By default, Wait() will return a combined error. +// This is particularly useful for (*ContextPool).WithCancelOnError(), +// where all errors after the first are likely to be context.Canceled. +func (p *ContextPool) WithFirstError() *ContextPool { + p.panicIfInitialized() + p.errorPool.WithFirstError() + return p +} + +// WithCancelOnError configures the pool to cancel its context as soon as +// any task returns an error or panics. By default, the pool's context is not +// canceled until the parent context is canceled. +// +// In this case, all errors returned from the pool after the first will +// likely be context.Canceled - you may want to also use +// (*ContextPool).WithFirstError() to configure the pool to only return +// the first error. +func (p *ContextPool) WithCancelOnError() *ContextPool { + p.panicIfInitialized() + p.cancelOnError = true + return p +} + +// WithMaxGoroutines limits the number of goroutines in a pool. +// Defaults to unlimited. Panics if n < 1. +func (p *ContextPool) WithMaxGoroutines(n int) *ContextPool { + p.panicIfInitialized() + p.errorPool.WithMaxGoroutines(n) + return p +} + +func (p *ContextPool) panicIfInitialized() { + p.errorPool.panicIfInitialized() +} diff --git a/vendor/github.com/sourcegraph/conc/pool/error_pool.go b/vendor/github.com/sourcegraph/conc/pool/error_pool.go new file mode 100644 index 0000000..6e5aa99 --- /dev/null +++ b/vendor/github.com/sourcegraph/conc/pool/error_pool.go @@ -0,0 +1,97 @@ +package pool + +import ( + "context" + "sync" + + "github.com/sourcegraph/conc/internal/multierror" +) + +// ErrorPool is a pool that runs tasks that may return an error. +// Errors are collected and returned by Wait(). +// +// The configuration methods (With*) will panic if they are used after calling +// Go() for the first time. +// +// A new ErrorPool should be created using `New().WithErrors()`. +type ErrorPool struct { + pool Pool + + onlyFirstError bool + + mu sync.Mutex + errs error +} + +// Go submits a task to the pool. If all goroutines in the pool +// are busy, a call to Go() will block until the task can be started. +func (p *ErrorPool) Go(f func() error) { + p.pool.Go(func() { + p.addErr(f()) + }) +} + +// Wait cleans up any spawned goroutines, propagating any panics and +// returning any errors from tasks. +func (p *ErrorPool) Wait() error { + p.pool.Wait() + return p.errs +} + +// WithContext converts the pool to a ContextPool for tasks that should +// run under the same context, such that they each respect shared cancellation. +// For example, WithCancelOnError can be configured on the returned pool to +// signal that all goroutines should be cancelled upon the first error. +func (p *ErrorPool) WithContext(ctx context.Context) *ContextPool { + p.panicIfInitialized() + ctx, cancel := context.WithCancel(ctx) + return &ContextPool{ + errorPool: p.deref(), + ctx: ctx, + cancel: cancel, + } +} + +// WithFirstError configures the pool to only return the first error +// returned by a task. By default, Wait() will return a combined error. +func (p *ErrorPool) WithFirstError() *ErrorPool { + p.panicIfInitialized() + p.onlyFirstError = true + return p +} + +// WithMaxGoroutines limits the number of goroutines in a pool. +// Defaults to unlimited. Panics if n < 1. +func (p *ErrorPool) WithMaxGoroutines(n int) *ErrorPool { + p.panicIfInitialized() + p.pool.WithMaxGoroutines(n) + return p +} + +// deref is a helper that creates a shallow copy of the pool with the same +// settings. We don't want to just dereference the pointer because that makes +// the copylock lint angry. +func (p *ErrorPool) deref() ErrorPool { + return ErrorPool{ + pool: p.pool.deref(), + onlyFirstError: p.onlyFirstError, + } +} + +func (p *ErrorPool) panicIfInitialized() { + p.pool.panicIfInitialized() +} + +func (p *ErrorPool) addErr(err error) { + if err != nil { + p.mu.Lock() + if p.onlyFirstError { + if p.errs == nil { + p.errs = err + } + } else { + p.errs = multierror.Join(p.errs, err) + } + p.mu.Unlock() + } +} diff --git a/vendor/github.com/sourcegraph/conc/pool/pool.go b/vendor/github.com/sourcegraph/conc/pool/pool.go new file mode 100644 index 0000000..b63eb19 --- /dev/null +++ b/vendor/github.com/sourcegraph/conc/pool/pool.go @@ -0,0 +1,168 @@ +package pool + +import ( + "context" + "sync" + + "github.com/sourcegraph/conc" +) + +// New creates a new Pool. +func New() *Pool { + return &Pool{} +} + +// Pool is a pool of goroutines used to execute tasks concurrently. +// +// Tasks are submitted with Go(). Once all your tasks have been submitted, you +// must call Wait() to clean up any spawned goroutines and propagate any +// panics. +// +// Goroutines are started lazily, so creating a new pool is cheap. There will +// never be more goroutines spawned than there are tasks submitted. +// +// The configuration methods (With*) will panic if they are used after calling +// Go() for the first time. +// +// Pool is efficient, but not zero cost. It should not be used for very short +// tasks. Startup and teardown come with an overhead of around 1µs, and each +// task has an overhead of around 300ns. +type Pool struct { + handle conc.WaitGroup + limiter limiter + tasks chan func() + initOnce sync.Once +} + +// Go submits a task to be run in the pool. If all goroutines in the pool +// are busy, a call to Go() will block until the task can be started. +func (p *Pool) Go(f func()) { + p.init() + + if p.limiter == nil { + // No limit on the number of goroutines. + select { + case p.tasks <- f: + // A goroutine was available to handle the task. + default: + // No goroutine was available to handle the task. + // Spawn a new one and send it the task. + p.handle.Go(p.worker) + p.tasks <- f + } + } else { + select { + case p.limiter <- struct{}{}: + // If we are below our limit, spawn a new worker rather + // than waiting for one to become available. + p.handle.Go(p.worker) + + // We know there is at least one worker running, so wait + // for it to become available. This ensures we never spawn + // more workers than the number of tasks. + p.tasks <- f + case p.tasks <- f: + // A worker is available and has accepted the task. + return + } + } + +} + +// Wait cleans up spawned goroutines, propagating any panics that were +// raised by a tasks. +func (p *Pool) Wait() { + p.init() + + close(p.tasks) + + p.handle.Wait() +} + +// MaxGoroutines returns the maximum size of the pool. +func (p *Pool) MaxGoroutines() int { + return p.limiter.limit() +} + +// WithMaxGoroutines limits the number of goroutines in a pool. +// Defaults to unlimited. Panics if n < 1. +func (p *Pool) WithMaxGoroutines(n int) *Pool { + p.panicIfInitialized() + if n < 1 { + panic("max goroutines in a pool must be greater than zero") + } + p.limiter = make(limiter, n) + return p +} + +// init ensures that the pool is initialized before use. This makes the +// zero value of the pool usable. +func (p *Pool) init() { + p.initOnce.Do(func() { + p.tasks = make(chan func()) + }) +} + +// panicIfInitialized will trigger a panic if a configuration method is called +// after the pool has started any goroutines for the first time. In the case that +// new settings are needed, a new pool should be created. +func (p *Pool) panicIfInitialized() { + if p.tasks != nil { + panic("pool can not be reconfigured after calling Go() for the first time") + } +} + +// WithErrors converts the pool to an ErrorPool so the submitted tasks can +// return errors. +func (p *Pool) WithErrors() *ErrorPool { + p.panicIfInitialized() + return &ErrorPool{ + pool: p.deref(), + } +} + +// deref is a helper that creates a shallow copy of the pool with the same +// settings. We don't want to just dereference the pointer because that makes +// the copylock lint angry. +func (p *Pool) deref() Pool { + p.panicIfInitialized() + return Pool{ + limiter: p.limiter, + } +} + +// WithContext converts the pool to a ContextPool for tasks that should +// run under the same context, such that they each respect shared cancellation. +// For example, WithCancelOnError can be configured on the returned pool to +// signal that all goroutines should be cancelled upon the first error. +func (p *Pool) WithContext(ctx context.Context) *ContextPool { + p.panicIfInitialized() + ctx, cancel := context.WithCancel(ctx) + return &ContextPool{ + errorPool: p.WithErrors().deref(), + ctx: ctx, + cancel: cancel, + } +} + +func (p *Pool) worker() { + // The only time this matters is if the task panics. + // This makes it possible to spin up new workers in that case. + defer p.limiter.release() + + for f := range p.tasks { + f() + } +} + +type limiter chan struct{} + +func (l limiter) limit() int { + return cap(l) +} + +func (l limiter) release() { + if l != nil { + <-l + } +} diff --git a/vendor/github.com/sourcegraph/conc/pool/result_context_pool.go b/vendor/github.com/sourcegraph/conc/pool/result_context_pool.go new file mode 100644 index 0000000..55dc3bc --- /dev/null +++ b/vendor/github.com/sourcegraph/conc/pool/result_context_pool.go @@ -0,0 +1,75 @@ +package pool + +import ( + "context" +) + +// ResultContextPool is a pool that runs tasks that take a context and return a +// result. The context passed to the task will be canceled if any of the tasks +// return an error, which makes its functionality different than just capturing +// a context with the task closure. +// +// The configuration methods (With*) will panic if they are used after calling +// Go() for the first time. +type ResultContextPool[T any] struct { + contextPool ContextPool + agg resultAggregator[T] + collectErrored bool +} + +// Go submits a task to the pool. If all goroutines in the pool +// are busy, a call to Go() will block until the task can be started. +func (p *ResultContextPool[T]) Go(f func(context.Context) (T, error)) { + p.contextPool.Go(func(ctx context.Context) error { + res, err := f(ctx) + if err == nil || p.collectErrored { + p.agg.add(res) + } + return err + }) +} + +// Wait cleans up all spawned goroutines, propagates any panics, and +// returns an error if any of the tasks errored. +func (p *ResultContextPool[T]) Wait() ([]T, error) { + err := p.contextPool.Wait() + return p.agg.results, err +} + +// WithCollectErrored configures the pool to still collect the result of a task +// even if the task returned an error. By default, the result of tasks that errored +// are ignored and only the error is collected. +func (p *ResultContextPool[T]) WithCollectErrored() *ResultContextPool[T] { + p.panicIfInitialized() + p.collectErrored = true + return p +} + +// WithFirstError configures the pool to only return the first error +// returned by a task. By default, Wait() will return a combined error. +func (p *ResultContextPool[T]) WithFirstError() *ResultContextPool[T] { + p.panicIfInitialized() + p.contextPool.WithFirstError() + return p +} + +// WithCancelOnError configures the pool to cancel its context as soon as +// any task returns an error. By default, the pool's context is not +// canceled until the parent context is canceled. +func (p *ResultContextPool[T]) WithCancelOnError() *ResultContextPool[T] { + p.panicIfInitialized() + p.contextPool.WithCancelOnError() + return p +} + +// WithMaxGoroutines limits the number of goroutines in a pool. +// Defaults to unlimited. Panics if n < 1. +func (p *ResultContextPool[T]) WithMaxGoroutines(n int) *ResultContextPool[T] { + p.panicIfInitialized() + p.contextPool.WithMaxGoroutines(n) + return p +} + +func (p *ResultContextPool[T]) panicIfInitialized() { + p.contextPool.panicIfInitialized() +} diff --git a/vendor/github.com/sourcegraph/conc/pool/result_error_pool.go b/vendor/github.com/sourcegraph/conc/pool/result_error_pool.go new file mode 100644 index 0000000..4caaadc --- /dev/null +++ b/vendor/github.com/sourcegraph/conc/pool/result_error_pool.go @@ -0,0 +1,80 @@ +package pool + +import ( + "context" +) + +// ResultErrorPool is a pool that executes tasks that return a generic result +// type and an error. Tasks are executed in the pool with Go(), then the +// results of the tasks are returned by Wait(). +// +// The order of the results is not guaranteed to be the same as the order the +// tasks were submitted. If your use case requires consistent ordering, +// consider using the `stream` package or `Map` from the `iter` package. +// +// The configuration methods (With*) will panic if they are used after calling +// Go() for the first time. +type ResultErrorPool[T any] struct { + errorPool ErrorPool + agg resultAggregator[T] + collectErrored bool +} + +// Go submits a task to the pool. If all goroutines in the pool +// are busy, a call to Go() will block until the task can be started. +func (p *ResultErrorPool[T]) Go(f func() (T, error)) { + p.errorPool.Go(func() error { + res, err := f() + if err == nil || p.collectErrored { + p.agg.add(res) + } + return err + }) +} + +// Wait cleans up any spawned goroutines, propagating any panics and +// returning the results and any errors from tasks. +func (p *ResultErrorPool[T]) Wait() ([]T, error) { + err := p.errorPool.Wait() + return p.agg.results, err +} + +// WithCollectErrored configures the pool to still collect the result of a task +// even if the task returned an error. By default, the result of tasks that errored +// are ignored and only the error is collected. +func (p *ResultErrorPool[T]) WithCollectErrored() *ResultErrorPool[T] { + p.panicIfInitialized() + p.collectErrored = true + return p +} + +// WithContext converts the pool to a ResultContextPool for tasks that should +// run under the same context, such that they each respect shared cancellation. +// For example, WithCancelOnError can be configured on the returned pool to +// signal that all goroutines should be cancelled upon the first error. +func (p *ResultErrorPool[T]) WithContext(ctx context.Context) *ResultContextPool[T] { + p.panicIfInitialized() + return &ResultContextPool[T]{ + contextPool: *p.errorPool.WithContext(ctx), + } +} + +// WithFirstError configures the pool to only return the first error +// returned by a task. By default, Wait() will return a combined error. +func (p *ResultErrorPool[T]) WithFirstError() *ResultErrorPool[T] { + p.panicIfInitialized() + p.errorPool.WithFirstError() + return p +} + +// WithMaxGoroutines limits the number of goroutines in a pool. +// Defaults to unlimited. Panics if n < 1. +func (p *ResultErrorPool[T]) WithMaxGoroutines(n int) *ResultErrorPool[T] { + p.panicIfInitialized() + p.errorPool.WithMaxGoroutines(n) + return p +} + +func (p *ResultErrorPool[T]) panicIfInitialized() { + p.errorPool.panicIfInitialized() +} diff --git a/vendor/github.com/sourcegraph/conc/pool/result_pool.go b/vendor/github.com/sourcegraph/conc/pool/result_pool.go new file mode 100644 index 0000000..ea304cb --- /dev/null +++ b/vendor/github.com/sourcegraph/conc/pool/result_pool.go @@ -0,0 +1,93 @@ +package pool + +import ( + "context" + "sync" +) + +// NewWithResults creates a new ResultPool for tasks with a result of type T. +// +// The configuration methods (With*) will panic if they are used after calling +// Go() for the first time. +func NewWithResults[T any]() *ResultPool[T] { + return &ResultPool[T]{ + pool: *New(), + } +} + +// ResultPool is a pool that executes tasks that return a generic result type. +// Tasks are executed in the pool with Go(), then the results of the tasks are +// returned by Wait(). +// +// The order of the results is not guaranteed to be the same as the order the +// tasks were submitted. If your use case requires consistent ordering, +// consider using the `stream` package or `Map` from the `iter` package. +type ResultPool[T any] struct { + pool Pool + agg resultAggregator[T] +} + +// Go submits a task to the pool. If all goroutines in the pool +// are busy, a call to Go() will block until the task can be started. +func (p *ResultPool[T]) Go(f func() T) { + p.pool.Go(func() { + p.agg.add(f()) + }) +} + +// Wait cleans up all spawned goroutines, propagating any panics, and returning +// a slice of results from tasks that did not panic. +func (p *ResultPool[T]) Wait() []T { + p.pool.Wait() + return p.agg.results +} + +// MaxGoroutines returns the maximum size of the pool. +func (p *ResultPool[T]) MaxGoroutines() int { + return p.pool.MaxGoroutines() +} + +// WithErrors converts the pool to an ResultErrorPool so the submitted tasks +// can return errors. +func (p *ResultPool[T]) WithErrors() *ResultErrorPool[T] { + p.panicIfInitialized() + return &ResultErrorPool[T]{ + errorPool: *p.pool.WithErrors(), + } +} + +// WithContext converts the pool to a ResultContextPool for tasks that should +// run under the same context, such that they each respect shared cancellation. +// For example, WithCancelOnError can be configured on the returned pool to +// signal that all goroutines should be cancelled upon the first error. +func (p *ResultPool[T]) WithContext(ctx context.Context) *ResultContextPool[T] { + p.panicIfInitialized() + return &ResultContextPool[T]{ + contextPool: *p.pool.WithContext(ctx), + } +} + +// WithMaxGoroutines limits the number of goroutines in a pool. +// Defaults to unlimited. Panics if n < 1. +func (p *ResultPool[T]) WithMaxGoroutines(n int) *ResultPool[T] { + p.panicIfInitialized() + p.pool.WithMaxGoroutines(n) + return p +} + +func (p *ResultPool[T]) panicIfInitialized() { + p.pool.panicIfInitialized() +} + +// resultAggregator is a utility type that lets us safely append from multiple +// goroutines. The zero value is valid and ready to use. +type resultAggregator[T any] struct { + mu sync.Mutex + results []T +} + +func (r *resultAggregator[T]) add(res T) { + r.mu.Lock() + r.results = append(r.results, res) + r.mu.Unlock() +} diff --git a/vendor/github.com/sourcegraph/conc/waitgroup.go b/vendor/github.com/sourcegraph/conc/waitgroup.go new file mode 100644 index 0000000..47b1bc1 --- /dev/null +++ b/vendor/github.com/sourcegraph/conc/waitgroup.go @@ -0,0 +1,52 @@ +package conc + +import ( + "sync" + + "github.com/sourcegraph/conc/panics" +) + +// NewWaitGroup creates a new WaitGroup. +func NewWaitGroup() *WaitGroup { + return &WaitGroup{} +} + +// WaitGroup is the primary building block for scoped concurrency. +// Goroutines can be spawned in the WaitGroup with the Go method, +// and calling Wait() will ensure that each of those goroutines exits +// before continuing. Any panics in a child goroutine will be caught +// and propagated to the caller of Wait(). +// +// The zero value of WaitGroup is usable, just like sync.WaitGroup. +// Also like sync.WaitGroup, it must not be copied after first use. +type WaitGroup struct { + wg sync.WaitGroup + pc panics.Catcher +} + +// Go spawns a new goroutine in the WaitGroup. +func (h *WaitGroup) Go(f func()) { + h.wg.Add(1) + go func() { + defer h.wg.Done() + h.pc.Try(f) + }() +} + +// Wait will block until all goroutines spawned with Go exit and will +// propagate any panics spawned in a child goroutine. +func (h *WaitGroup) Wait() { + h.wg.Wait() + + // Propagate a panic if we caught one from a child goroutine. + h.pc.Repanic() +} + +// WaitAndRecover will block until all goroutines spawned with Go exit and +// will return a *panics.Recovered if one of the child goroutines panics. +func (h *WaitGroup) WaitAndRecover() *panics.Recovered { + h.wg.Wait() + + // Return a recovered panic if we caught one from a child goroutine. + return h.pc.Recovered() +} diff --git a/vendor/go.uber.org/atomic/.codecov.yml b/vendor/go.uber.org/atomic/.codecov.yml new file mode 100644 index 0000000..571116c --- /dev/null +++ b/vendor/go.uber.org/atomic/.codecov.yml @@ -0,0 +1,19 @@ +coverage: + range: 80..100 + round: down + precision: 2 + + status: + project: # measuring the overall project coverage + default: # context, you can create multiple ones with custom titles + enabled: yes # must be yes|true to enable this status + target: 100 # specify the target coverage for each commit status + # option: "auto" (must increase from parent commit or pull request base) + # option: "X%" a static target percentage to hit + if_not_found: success # if parent is not found report status as success, error, or failure + if_ci_failed: error # if ci fails report status as success, error, or failure + +# Also update COVER_IGNORE_PKGS in the Makefile. +ignore: + - /internal/gen-atomicint/ + - /internal/gen-valuewrapper/ diff --git a/vendor/go.uber.org/atomic/.gitignore b/vendor/go.uber.org/atomic/.gitignore new file mode 100644 index 0000000..c3fa253 --- /dev/null +++ b/vendor/go.uber.org/atomic/.gitignore @@ -0,0 +1,12 @@ +/bin +.DS_Store +/vendor +cover.html +cover.out +lint.log + +# Binaries +*.test + +# Profiling output +*.prof diff --git a/vendor/go.uber.org/atomic/.travis.yml b/vendor/go.uber.org/atomic/.travis.yml new file mode 100644 index 0000000..13d0a4f --- /dev/null +++ b/vendor/go.uber.org/atomic/.travis.yml @@ -0,0 +1,27 @@ +sudo: false +language: go +go_import_path: go.uber.org/atomic + +env: + global: + - GO111MODULE=on + +matrix: + include: + - go: oldstable + - go: stable + env: LINT=1 + +cache: + directories: + - vendor + +before_install: + - go version + +script: + - test -z "$LINT" || make lint + - make cover + +after_success: + - bash <(curl -s https://codecov.io/bash) diff --git a/vendor/go.uber.org/atomic/CHANGELOG.md b/vendor/go.uber.org/atomic/CHANGELOG.md new file mode 100644 index 0000000..24c0274 --- /dev/null +++ b/vendor/go.uber.org/atomic/CHANGELOG.md @@ -0,0 +1,76 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.7.0] - 2020-09-14 +### Added +- Support JSON serialization and deserialization of primitive atomic types. +- Support Text marshalling and unmarshalling for string atomics. + +### Changed +- Disallow incorrect comparison of atomic values in a non-atomic way. + +### Removed +- Remove dependency on `golang.org/x/{lint, tools}`. + +## [1.6.0] - 2020-02-24 +### Changed +- Drop library dependency on `golang.org/x/{lint, tools}`. + +## [1.5.1] - 2019-11-19 +- Fix bug where `Bool.CAS` and `Bool.Toggle` do work correctly together + causing `CAS` to fail even though the old value matches. + +## [1.5.0] - 2019-10-29 +### Changed +- With Go modules, only the `go.uber.org/atomic` import path is supported now. + If you need to use the old import path, please add a `replace` directive to + your `go.mod`. + +## [1.4.0] - 2019-05-01 +### Added + - Add `atomic.Error` type for atomic operations on `error` values. + +## [1.3.2] - 2018-05-02 +### Added +- Add `atomic.Duration` type for atomic operations on `time.Duration` values. + +## [1.3.1] - 2017-11-14 +### Fixed +- Revert optimization for `atomic.String.Store("")` which caused data races. + +## [1.3.0] - 2017-11-13 +### Added +- Add `atomic.Bool.CAS` for compare-and-swap semantics on bools. + +### Changed +- Optimize `atomic.String.Store("")` by avoiding an allocation. + +## [1.2.0] - 2017-04-12 +### Added +- Shadow `atomic.Value` from `sync/atomic`. + +## [1.1.0] - 2017-03-10 +### Added +- Add atomic `Float64` type. + +### Changed +- Support new `go.uber.org/atomic` import path. + +## [1.0.0] - 2016-07-18 + +- Initial release. + +[1.7.0]: https://github.com/uber-go/atomic/compare/v1.6.0...v1.7.0 +[1.6.0]: https://github.com/uber-go/atomic/compare/v1.5.1...v1.6.0 +[1.5.1]: https://github.com/uber-go/atomic/compare/v1.5.0...v1.5.1 +[1.5.0]: https://github.com/uber-go/atomic/compare/v1.4.0...v1.5.0 +[1.4.0]: https://github.com/uber-go/atomic/compare/v1.3.2...v1.4.0 +[1.3.2]: https://github.com/uber-go/atomic/compare/v1.3.1...v1.3.2 +[1.3.1]: https://github.com/uber-go/atomic/compare/v1.3.0...v1.3.1 +[1.3.0]: https://github.com/uber-go/atomic/compare/v1.2.0...v1.3.0 +[1.2.0]: https://github.com/uber-go/atomic/compare/v1.1.0...v1.2.0 +[1.1.0]: https://github.com/uber-go/atomic/compare/v1.0.0...v1.1.0 +[1.0.0]: https://github.com/uber-go/atomic/releases/tag/v1.0.0 diff --git a/vendor/go.uber.org/atomic/LICENSE.txt b/vendor/go.uber.org/atomic/LICENSE.txt new file mode 100644 index 0000000..8765c9f --- /dev/null +++ b/vendor/go.uber.org/atomic/LICENSE.txt @@ -0,0 +1,19 @@ +Copyright (c) 2016 Uber Technologies, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/go.uber.org/atomic/Makefile b/vendor/go.uber.org/atomic/Makefile new file mode 100644 index 0000000..1b1376d --- /dev/null +++ b/vendor/go.uber.org/atomic/Makefile @@ -0,0 +1,78 @@ +# Directory to place `go install`ed binaries into. +export GOBIN ?= $(shell pwd)/bin + +GOLINT = $(GOBIN)/golint +GEN_ATOMICINT = $(GOBIN)/gen-atomicint +GEN_ATOMICWRAPPER = $(GOBIN)/gen-atomicwrapper +STATICCHECK = $(GOBIN)/staticcheck + +GO_FILES ?= $(shell find . '(' -path .git -o -path vendor ')' -prune -o -name '*.go' -print) + +# Also update ignore section in .codecov.yml. +COVER_IGNORE_PKGS = \ + go.uber.org/atomic/internal/gen-atomicint \ + go.uber.org/atomic/internal/gen-atomicwrapper + +.PHONY: build +build: + go build ./... + +.PHONY: test +test: + go test -race ./... + +.PHONY: gofmt +gofmt: + $(eval FMT_LOG := $(shell mktemp -t gofmt.XXXXX)) + gofmt -e -s -l $(GO_FILES) > $(FMT_LOG) || true + @[ ! -s "$(FMT_LOG)" ] || (echo "gofmt failed:" && cat $(FMT_LOG) && false) + +$(GOLINT): + cd tools && go install golang.org/x/lint/golint + +$(STATICCHECK): + cd tools && go install honnef.co/go/tools/cmd/staticcheck + +$(GEN_ATOMICWRAPPER): $(wildcard ./internal/gen-atomicwrapper/*) + go build -o $@ ./internal/gen-atomicwrapper + +$(GEN_ATOMICINT): $(wildcard ./internal/gen-atomicint/*) + go build -o $@ ./internal/gen-atomicint + +.PHONY: golint +golint: $(GOLINT) + $(GOLINT) ./... + +.PHONY: staticcheck +staticcheck: $(STATICCHECK) + $(STATICCHECK) ./... + +.PHONY: lint +lint: gofmt golint staticcheck generatenodirty + +# comma separated list of packages to consider for code coverage. +COVER_PKG = $(shell \ + go list -find ./... | \ + grep -v $(foreach pkg,$(COVER_IGNORE_PKGS),-e "^$(pkg)$$") | \ + paste -sd, -) + +.PHONY: cover +cover: + go test -coverprofile=cover.out -coverpkg $(COVER_PKG) -v ./... + go tool cover -html=cover.out -o cover.html + +.PHONY: generate +generate: $(GEN_ATOMICINT) $(GEN_ATOMICWRAPPER) + go generate ./... + +.PHONY: generatenodirty +generatenodirty: + @[ -z "$$(git status --porcelain)" ] || ( \ + echo "Working tree is dirty. Commit your changes first."; \ + exit 1 ) + @make generate + @status=$$(git status --porcelain); \ + [ -z "$$status" ] || ( \ + echo "Working tree is dirty after `make generate`:"; \ + echo "$$status"; \ + echo "Please ensure that the generated code is up-to-date." ) diff --git a/vendor/go.uber.org/atomic/README.md b/vendor/go.uber.org/atomic/README.md new file mode 100644 index 0000000..ade0c20 --- /dev/null +++ b/vendor/go.uber.org/atomic/README.md @@ -0,0 +1,63 @@ +# atomic [![GoDoc][doc-img]][doc] [![Build Status][ci-img]][ci] [![Coverage Status][cov-img]][cov] [![Go Report Card][reportcard-img]][reportcard] + +Simple wrappers for primitive types to enforce atomic access. + +## Installation + +```shell +$ go get -u go.uber.org/atomic@v1 +``` + +### Legacy Import Path + +As of v1.5.0, the import path `go.uber.org/atomic` is the only supported way +of using this package. If you are using Go modules, this package will fail to +compile with the legacy import path path `github.com/uber-go/atomic`. + +We recommend migrating your code to the new import path but if you're unable +to do so, or if your dependencies are still using the old import path, you +will have to add a `replace` directive to your `go.mod` file downgrading the +legacy import path to an older version. + +``` +replace github.com/uber-go/atomic => github.com/uber-go/atomic v1.4.0 +``` + +You can do so automatically by running the following command. + +```shell +$ go mod edit -replace github.com/uber-go/atomic=github.com/uber-go/atomic@v1.4.0 +``` + +## Usage + +The standard library's `sync/atomic` is powerful, but it's easy to forget which +variables must be accessed atomically. `go.uber.org/atomic` preserves all the +functionality of the standard library, but wraps the primitive types to +provide a safer, more convenient API. + +```go +var atom atomic.Uint32 +atom.Store(42) +atom.Sub(2) +atom.CAS(40, 11) +``` + +See the [documentation][doc] for a complete API specification. + +## Development Status + +Stable. + +--- + +Released under the [MIT License](LICENSE.txt). + +[doc-img]: https://godoc.org/github.com/uber-go/atomic?status.svg +[doc]: https://godoc.org/go.uber.org/atomic +[ci-img]: https://travis-ci.com/uber-go/atomic.svg?branch=master +[ci]: https://travis-ci.com/uber-go/atomic +[cov-img]: https://codecov.io/gh/uber-go/atomic/branch/master/graph/badge.svg +[cov]: https://codecov.io/gh/uber-go/atomic +[reportcard-img]: https://goreportcard.com/badge/go.uber.org/atomic +[reportcard]: https://goreportcard.com/report/go.uber.org/atomic diff --git a/vendor/go.uber.org/atomic/bool.go b/vendor/go.uber.org/atomic/bool.go new file mode 100644 index 0000000..9cf1914 --- /dev/null +++ b/vendor/go.uber.org/atomic/bool.go @@ -0,0 +1,81 @@ +// @generated Code generated by gen-atomicwrapper. + +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +import ( + "encoding/json" +) + +// Bool is an atomic type-safe wrapper for bool values. +type Bool struct { + _ nocmp // disallow non-atomic comparison + + v Uint32 +} + +var _zeroBool bool + +// NewBool creates a new Bool. +func NewBool(v bool) *Bool { + x := &Bool{} + if v != _zeroBool { + x.Store(v) + } + return x +} + +// Load atomically loads the wrapped bool. +func (x *Bool) Load() bool { + return truthy(x.v.Load()) +} + +// Store atomically stores the passed bool. +func (x *Bool) Store(v bool) { + x.v.Store(boolToInt(v)) +} + +// CAS is an atomic compare-and-swap for bool values. +func (x *Bool) CAS(o, n bool) bool { + return x.v.CAS(boolToInt(o), boolToInt(n)) +} + +// Swap atomically stores the given bool and returns the old +// value. +func (x *Bool) Swap(o bool) bool { + return truthy(x.v.Swap(boolToInt(o))) +} + +// MarshalJSON encodes the wrapped bool into JSON. +func (x *Bool) MarshalJSON() ([]byte, error) { + return json.Marshal(x.Load()) +} + +// UnmarshalJSON decodes a bool from JSON. +func (x *Bool) UnmarshalJSON(b []byte) error { + var v bool + if err := json.Unmarshal(b, &v); err != nil { + return err + } + x.Store(v) + return nil +} diff --git a/vendor/go.uber.org/atomic/bool_ext.go b/vendor/go.uber.org/atomic/bool_ext.go new file mode 100644 index 0000000..c7bf7a8 --- /dev/null +++ b/vendor/go.uber.org/atomic/bool_ext.go @@ -0,0 +1,53 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +import ( + "strconv" +) + +//go:generate bin/gen-atomicwrapper -name=Bool -type=bool -wrapped=Uint32 -pack=boolToInt -unpack=truthy -cas -swap -json -file=bool.go + +func truthy(n uint32) bool { + return n == 1 +} + +func boolToInt(b bool) uint32 { + if b { + return 1 + } + return 0 +} + +// Toggle atomically negates the Boolean and returns the previous value. +func (b *Bool) Toggle() bool { + for { + old := b.Load() + if b.CAS(old, !old) { + return old + } + } +} + +// String encodes the wrapped value as a string. +func (b *Bool) String() string { + return strconv.FormatBool(b.Load()) +} diff --git a/vendor/go.uber.org/atomic/doc.go b/vendor/go.uber.org/atomic/doc.go new file mode 100644 index 0000000..ae7390e --- /dev/null +++ b/vendor/go.uber.org/atomic/doc.go @@ -0,0 +1,23 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +// Package atomic provides simple wrappers around numerics to enforce atomic +// access. +package atomic diff --git a/vendor/go.uber.org/atomic/duration.go b/vendor/go.uber.org/atomic/duration.go new file mode 100644 index 0000000..027cfcb --- /dev/null +++ b/vendor/go.uber.org/atomic/duration.go @@ -0,0 +1,82 @@ +// @generated Code generated by gen-atomicwrapper. + +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +import ( + "encoding/json" + "time" +) + +// Duration is an atomic type-safe wrapper for time.Duration values. +type Duration struct { + _ nocmp // disallow non-atomic comparison + + v Int64 +} + +var _zeroDuration time.Duration + +// NewDuration creates a new Duration. +func NewDuration(v time.Duration) *Duration { + x := &Duration{} + if v != _zeroDuration { + x.Store(v) + } + return x +} + +// Load atomically loads the wrapped time.Duration. +func (x *Duration) Load() time.Duration { + return time.Duration(x.v.Load()) +} + +// Store atomically stores the passed time.Duration. +func (x *Duration) Store(v time.Duration) { + x.v.Store(int64(v)) +} + +// CAS is an atomic compare-and-swap for time.Duration values. +func (x *Duration) CAS(o, n time.Duration) bool { + return x.v.CAS(int64(o), int64(n)) +} + +// Swap atomically stores the given time.Duration and returns the old +// value. +func (x *Duration) Swap(o time.Duration) time.Duration { + return time.Duration(x.v.Swap(int64(o))) +} + +// MarshalJSON encodes the wrapped time.Duration into JSON. +func (x *Duration) MarshalJSON() ([]byte, error) { + return json.Marshal(x.Load()) +} + +// UnmarshalJSON decodes a time.Duration from JSON. +func (x *Duration) UnmarshalJSON(b []byte) error { + var v time.Duration + if err := json.Unmarshal(b, &v); err != nil { + return err + } + x.Store(v) + return nil +} diff --git a/vendor/go.uber.org/atomic/duration_ext.go b/vendor/go.uber.org/atomic/duration_ext.go new file mode 100644 index 0000000..6273b66 --- /dev/null +++ b/vendor/go.uber.org/atomic/duration_ext.go @@ -0,0 +1,40 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +import "time" + +//go:generate bin/gen-atomicwrapper -name=Duration -type=time.Duration -wrapped=Int64 -pack=int64 -unpack=time.Duration -cas -swap -json -imports time -file=duration.go + +// Add atomically adds to the wrapped time.Duration and returns the new value. +func (d *Duration) Add(n time.Duration) time.Duration { + return time.Duration(d.v.Add(int64(n))) +} + +// Sub atomically subtracts from the wrapped time.Duration and returns the new value. +func (d *Duration) Sub(n time.Duration) time.Duration { + return time.Duration(d.v.Sub(int64(n))) +} + +// String encodes the wrapped value as a string. +func (d *Duration) String() string { + return d.Load().String() +} diff --git a/vendor/go.uber.org/atomic/error.go b/vendor/go.uber.org/atomic/error.go new file mode 100644 index 0000000..a6166fb --- /dev/null +++ b/vendor/go.uber.org/atomic/error.go @@ -0,0 +1,51 @@ +// @generated Code generated by gen-atomicwrapper. + +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +// Error is an atomic type-safe wrapper for error values. +type Error struct { + _ nocmp // disallow non-atomic comparison + + v Value +} + +var _zeroError error + +// NewError creates a new Error. +func NewError(v error) *Error { + x := &Error{} + if v != _zeroError { + x.Store(v) + } + return x +} + +// Load atomically loads the wrapped error. +func (x *Error) Load() error { + return unpackError(x.v.Load()) +} + +// Store atomically stores the passed error. +func (x *Error) Store(v error) { + x.v.Store(packError(v)) +} diff --git a/vendor/go.uber.org/atomic/error_ext.go b/vendor/go.uber.org/atomic/error_ext.go new file mode 100644 index 0000000..ffe0be2 --- /dev/null +++ b/vendor/go.uber.org/atomic/error_ext.go @@ -0,0 +1,39 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +// atomic.Value panics on nil inputs, or if the underlying type changes. +// Stabilize by always storing a custom struct that we control. + +//go:generate bin/gen-atomicwrapper -name=Error -type=error -wrapped=Value -pack=packError -unpack=unpackError -file=error.go + +type packedError struct{ Value error } + +func packError(v error) interface{} { + return packedError{v} +} + +func unpackError(v interface{}) error { + if err, ok := v.(packedError); ok { + return err.Value + } + return nil +} diff --git a/vendor/go.uber.org/atomic/float64.go b/vendor/go.uber.org/atomic/float64.go new file mode 100644 index 0000000..0719060 --- /dev/null +++ b/vendor/go.uber.org/atomic/float64.go @@ -0,0 +1,76 @@ +// @generated Code generated by gen-atomicwrapper. + +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +import ( + "encoding/json" + "math" +) + +// Float64 is an atomic type-safe wrapper for float64 values. +type Float64 struct { + _ nocmp // disallow non-atomic comparison + + v Uint64 +} + +var _zeroFloat64 float64 + +// NewFloat64 creates a new Float64. +func NewFloat64(v float64) *Float64 { + x := &Float64{} + if v != _zeroFloat64 { + x.Store(v) + } + return x +} + +// Load atomically loads the wrapped float64. +func (x *Float64) Load() float64 { + return math.Float64frombits(x.v.Load()) +} + +// Store atomically stores the passed float64. +func (x *Float64) Store(v float64) { + x.v.Store(math.Float64bits(v)) +} + +// CAS is an atomic compare-and-swap for float64 values. +func (x *Float64) CAS(o, n float64) bool { + return x.v.CAS(math.Float64bits(o), math.Float64bits(n)) +} + +// MarshalJSON encodes the wrapped float64 into JSON. +func (x *Float64) MarshalJSON() ([]byte, error) { + return json.Marshal(x.Load()) +} + +// UnmarshalJSON decodes a float64 from JSON. +func (x *Float64) UnmarshalJSON(b []byte) error { + var v float64 + if err := json.Unmarshal(b, &v); err != nil { + return err + } + x.Store(v) + return nil +} diff --git a/vendor/go.uber.org/atomic/float64_ext.go b/vendor/go.uber.org/atomic/float64_ext.go new file mode 100644 index 0000000..927b1ad --- /dev/null +++ b/vendor/go.uber.org/atomic/float64_ext.go @@ -0,0 +1,47 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +import "strconv" + +//go:generate bin/gen-atomicwrapper -name=Float64 -type=float64 -wrapped=Uint64 -pack=math.Float64bits -unpack=math.Float64frombits -cas -json -imports math -file=float64.go + +// Add atomically adds to the wrapped float64 and returns the new value. +func (f *Float64) Add(s float64) float64 { + for { + old := f.Load() + new := old + s + if f.CAS(old, new) { + return new + } + } +} + +// Sub atomically subtracts from the wrapped float64 and returns the new value. +func (f *Float64) Sub(s float64) float64 { + return f.Add(-s) +} + +// String encodes the wrapped value as a string. +func (f *Float64) String() string { + // 'g' is the behavior for floats with %v. + return strconv.FormatFloat(f.Load(), 'g', -1, 64) +} diff --git a/vendor/go.uber.org/atomic/gen.go b/vendor/go.uber.org/atomic/gen.go new file mode 100644 index 0000000..50d6b24 --- /dev/null +++ b/vendor/go.uber.org/atomic/gen.go @@ -0,0 +1,26 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +//go:generate bin/gen-atomicint -name=Int32 -wrapped=int32 -file=int32.go +//go:generate bin/gen-atomicint -name=Int64 -wrapped=int64 -file=int64.go +//go:generate bin/gen-atomicint -name=Uint32 -wrapped=uint32 -unsigned -file=uint32.go +//go:generate bin/gen-atomicint -name=Uint64 -wrapped=uint64 -unsigned -file=uint64.go diff --git a/vendor/go.uber.org/atomic/int32.go b/vendor/go.uber.org/atomic/int32.go new file mode 100644 index 0000000..18ae564 --- /dev/null +++ b/vendor/go.uber.org/atomic/int32.go @@ -0,0 +1,102 @@ +// @generated Code generated by gen-atomicint. + +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +import ( + "encoding/json" + "strconv" + "sync/atomic" +) + +// Int32 is an atomic wrapper around int32. +type Int32 struct { + _ nocmp // disallow non-atomic comparison + + v int32 +} + +// NewInt32 creates a new Int32. +func NewInt32(i int32) *Int32 { + return &Int32{v: i} +} + +// Load atomically loads the wrapped value. +func (i *Int32) Load() int32 { + return atomic.LoadInt32(&i.v) +} + +// Add atomically adds to the wrapped int32 and returns the new value. +func (i *Int32) Add(n int32) int32 { + return atomic.AddInt32(&i.v, n) +} + +// Sub atomically subtracts from the wrapped int32 and returns the new value. +func (i *Int32) Sub(n int32) int32 { + return atomic.AddInt32(&i.v, -n) +} + +// Inc atomically increments the wrapped int32 and returns the new value. +func (i *Int32) Inc() int32 { + return i.Add(1) +} + +// Dec atomically decrements the wrapped int32 and returns the new value. +func (i *Int32) Dec() int32 { + return i.Sub(1) +} + +// CAS is an atomic compare-and-swap. +func (i *Int32) CAS(old, new int32) bool { + return atomic.CompareAndSwapInt32(&i.v, old, new) +} + +// Store atomically stores the passed value. +func (i *Int32) Store(n int32) { + atomic.StoreInt32(&i.v, n) +} + +// Swap atomically swaps the wrapped int32 and returns the old value. +func (i *Int32) Swap(n int32) int32 { + return atomic.SwapInt32(&i.v, n) +} + +// MarshalJSON encodes the wrapped int32 into JSON. +func (i *Int32) MarshalJSON() ([]byte, error) { + return json.Marshal(i.Load()) +} + +// UnmarshalJSON decodes JSON into the wrapped int32. +func (i *Int32) UnmarshalJSON(b []byte) error { + var v int32 + if err := json.Unmarshal(b, &v); err != nil { + return err + } + i.Store(v) + return nil +} + +// String encodes the wrapped value as a string. +func (i *Int32) String() string { + v := i.Load() + return strconv.FormatInt(int64(v), 10) +} diff --git a/vendor/go.uber.org/atomic/int64.go b/vendor/go.uber.org/atomic/int64.go new file mode 100644 index 0000000..2bcbbfa --- /dev/null +++ b/vendor/go.uber.org/atomic/int64.go @@ -0,0 +1,102 @@ +// @generated Code generated by gen-atomicint. + +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +import ( + "encoding/json" + "strconv" + "sync/atomic" +) + +// Int64 is an atomic wrapper around int64. +type Int64 struct { + _ nocmp // disallow non-atomic comparison + + v int64 +} + +// NewInt64 creates a new Int64. +func NewInt64(i int64) *Int64 { + return &Int64{v: i} +} + +// Load atomically loads the wrapped value. +func (i *Int64) Load() int64 { + return atomic.LoadInt64(&i.v) +} + +// Add atomically adds to the wrapped int64 and returns the new value. +func (i *Int64) Add(n int64) int64 { + return atomic.AddInt64(&i.v, n) +} + +// Sub atomically subtracts from the wrapped int64 and returns the new value. +func (i *Int64) Sub(n int64) int64 { + return atomic.AddInt64(&i.v, -n) +} + +// Inc atomically increments the wrapped int64 and returns the new value. +func (i *Int64) Inc() int64 { + return i.Add(1) +} + +// Dec atomically decrements the wrapped int64 and returns the new value. +func (i *Int64) Dec() int64 { + return i.Sub(1) +} + +// CAS is an atomic compare-and-swap. +func (i *Int64) CAS(old, new int64) bool { + return atomic.CompareAndSwapInt64(&i.v, old, new) +} + +// Store atomically stores the passed value. +func (i *Int64) Store(n int64) { + atomic.StoreInt64(&i.v, n) +} + +// Swap atomically swaps the wrapped int64 and returns the old value. +func (i *Int64) Swap(n int64) int64 { + return atomic.SwapInt64(&i.v, n) +} + +// MarshalJSON encodes the wrapped int64 into JSON. +func (i *Int64) MarshalJSON() ([]byte, error) { + return json.Marshal(i.Load()) +} + +// UnmarshalJSON decodes JSON into the wrapped int64. +func (i *Int64) UnmarshalJSON(b []byte) error { + var v int64 + if err := json.Unmarshal(b, &v); err != nil { + return err + } + i.Store(v) + return nil +} + +// String encodes the wrapped value as a string. +func (i *Int64) String() string { + v := i.Load() + return strconv.FormatInt(int64(v), 10) +} diff --git a/vendor/go.uber.org/atomic/nocmp.go b/vendor/go.uber.org/atomic/nocmp.go new file mode 100644 index 0000000..a8201cb --- /dev/null +++ b/vendor/go.uber.org/atomic/nocmp.go @@ -0,0 +1,35 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +// nocmp is an uncomparable struct. Embed this inside another struct to make +// it uncomparable. +// +// type Foo struct { +// nocmp +// // ... +// } +// +// This DOES NOT: +// +// - Disallow shallow copies of structs +// - Disallow comparison of pointers to uncomparable structs +type nocmp [0]func() diff --git a/vendor/go.uber.org/atomic/string.go b/vendor/go.uber.org/atomic/string.go new file mode 100644 index 0000000..225b7a2 --- /dev/null +++ b/vendor/go.uber.org/atomic/string.go @@ -0,0 +1,54 @@ +// @generated Code generated by gen-atomicwrapper. + +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +// String is an atomic type-safe wrapper for string values. +type String struct { + _ nocmp // disallow non-atomic comparison + + v Value +} + +var _zeroString string + +// NewString creates a new String. +func NewString(v string) *String { + x := &String{} + if v != _zeroString { + x.Store(v) + } + return x +} + +// Load atomically loads the wrapped string. +func (x *String) Load() string { + if v := x.v.Load(); v != nil { + return v.(string) + } + return _zeroString +} + +// Store atomically stores the passed string. +func (x *String) Store(v string) { + x.v.Store(v) +} diff --git a/vendor/go.uber.org/atomic/string_ext.go b/vendor/go.uber.org/atomic/string_ext.go new file mode 100644 index 0000000..3a95582 --- /dev/null +++ b/vendor/go.uber.org/atomic/string_ext.go @@ -0,0 +1,43 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +//go:generate bin/gen-atomicwrapper -name=String -type=string -wrapped=Value -file=string.go + +// String returns the wrapped value. +func (s *String) String() string { + return s.Load() +} + +// MarshalText encodes the wrapped string into a textual form. +// +// This makes it encodable as JSON, YAML, XML, and more. +func (s *String) MarshalText() ([]byte, error) { + return []byte(s.Load()), nil +} + +// UnmarshalText decodes text and replaces the wrapped string with it. +// +// This makes it decodable from JSON, YAML, XML, and more. +func (s *String) UnmarshalText(b []byte) error { + s.Store(string(b)) + return nil +} diff --git a/vendor/go.uber.org/atomic/uint32.go b/vendor/go.uber.org/atomic/uint32.go new file mode 100644 index 0000000..a973aba --- /dev/null +++ b/vendor/go.uber.org/atomic/uint32.go @@ -0,0 +1,102 @@ +// @generated Code generated by gen-atomicint. + +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +import ( + "encoding/json" + "strconv" + "sync/atomic" +) + +// Uint32 is an atomic wrapper around uint32. +type Uint32 struct { + _ nocmp // disallow non-atomic comparison + + v uint32 +} + +// NewUint32 creates a new Uint32. +func NewUint32(i uint32) *Uint32 { + return &Uint32{v: i} +} + +// Load atomically loads the wrapped value. +func (i *Uint32) Load() uint32 { + return atomic.LoadUint32(&i.v) +} + +// Add atomically adds to the wrapped uint32 and returns the new value. +func (i *Uint32) Add(n uint32) uint32 { + return atomic.AddUint32(&i.v, n) +} + +// Sub atomically subtracts from the wrapped uint32 and returns the new value. +func (i *Uint32) Sub(n uint32) uint32 { + return atomic.AddUint32(&i.v, ^(n - 1)) +} + +// Inc atomically increments the wrapped uint32 and returns the new value. +func (i *Uint32) Inc() uint32 { + return i.Add(1) +} + +// Dec atomically decrements the wrapped uint32 and returns the new value. +func (i *Uint32) Dec() uint32 { + return i.Sub(1) +} + +// CAS is an atomic compare-and-swap. +func (i *Uint32) CAS(old, new uint32) bool { + return atomic.CompareAndSwapUint32(&i.v, old, new) +} + +// Store atomically stores the passed value. +func (i *Uint32) Store(n uint32) { + atomic.StoreUint32(&i.v, n) +} + +// Swap atomically swaps the wrapped uint32 and returns the old value. +func (i *Uint32) Swap(n uint32) uint32 { + return atomic.SwapUint32(&i.v, n) +} + +// MarshalJSON encodes the wrapped uint32 into JSON. +func (i *Uint32) MarshalJSON() ([]byte, error) { + return json.Marshal(i.Load()) +} + +// UnmarshalJSON decodes JSON into the wrapped uint32. +func (i *Uint32) UnmarshalJSON(b []byte) error { + var v uint32 + if err := json.Unmarshal(b, &v); err != nil { + return err + } + i.Store(v) + return nil +} + +// String encodes the wrapped value as a string. +func (i *Uint32) String() string { + v := i.Load() + return strconv.FormatUint(uint64(v), 10) +} diff --git a/vendor/go.uber.org/atomic/uint64.go b/vendor/go.uber.org/atomic/uint64.go new file mode 100644 index 0000000..3b6c71f --- /dev/null +++ b/vendor/go.uber.org/atomic/uint64.go @@ -0,0 +1,102 @@ +// @generated Code generated by gen-atomicint. + +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +import ( + "encoding/json" + "strconv" + "sync/atomic" +) + +// Uint64 is an atomic wrapper around uint64. +type Uint64 struct { + _ nocmp // disallow non-atomic comparison + + v uint64 +} + +// NewUint64 creates a new Uint64. +func NewUint64(i uint64) *Uint64 { + return &Uint64{v: i} +} + +// Load atomically loads the wrapped value. +func (i *Uint64) Load() uint64 { + return atomic.LoadUint64(&i.v) +} + +// Add atomically adds to the wrapped uint64 and returns the new value. +func (i *Uint64) Add(n uint64) uint64 { + return atomic.AddUint64(&i.v, n) +} + +// Sub atomically subtracts from the wrapped uint64 and returns the new value. +func (i *Uint64) Sub(n uint64) uint64 { + return atomic.AddUint64(&i.v, ^(n - 1)) +} + +// Inc atomically increments the wrapped uint64 and returns the new value. +func (i *Uint64) Inc() uint64 { + return i.Add(1) +} + +// Dec atomically decrements the wrapped uint64 and returns the new value. +func (i *Uint64) Dec() uint64 { + return i.Sub(1) +} + +// CAS is an atomic compare-and-swap. +func (i *Uint64) CAS(old, new uint64) bool { + return atomic.CompareAndSwapUint64(&i.v, old, new) +} + +// Store atomically stores the passed value. +func (i *Uint64) Store(n uint64) { + atomic.StoreUint64(&i.v, n) +} + +// Swap atomically swaps the wrapped uint64 and returns the old value. +func (i *Uint64) Swap(n uint64) uint64 { + return atomic.SwapUint64(&i.v, n) +} + +// MarshalJSON encodes the wrapped uint64 into JSON. +func (i *Uint64) MarshalJSON() ([]byte, error) { + return json.Marshal(i.Load()) +} + +// UnmarshalJSON decodes JSON into the wrapped uint64. +func (i *Uint64) UnmarshalJSON(b []byte) error { + var v uint64 + if err := json.Unmarshal(b, &v); err != nil { + return err + } + i.Store(v) + return nil +} + +// String encodes the wrapped value as a string. +func (i *Uint64) String() string { + v := i.Load() + return strconv.FormatUint(uint64(v), 10) +} diff --git a/vendor/go.uber.org/atomic/value.go b/vendor/go.uber.org/atomic/value.go new file mode 100644 index 0000000..671f3a3 --- /dev/null +++ b/vendor/go.uber.org/atomic/value.go @@ -0,0 +1,31 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +import "sync/atomic" + +// Value shadows the type of the same name from sync/atomic +// https://godoc.org/sync/atomic#Value +type Value struct { + atomic.Value + + _ nocmp // disallow non-atomic comparison +} diff --git a/vendor/go.uber.org/multierr/.codecov.yml b/vendor/go.uber.org/multierr/.codecov.yml new file mode 100644 index 0000000..6d4d1be --- /dev/null +++ b/vendor/go.uber.org/multierr/.codecov.yml @@ -0,0 +1,15 @@ +coverage: + range: 80..100 + round: down + precision: 2 + + status: + project: # measuring the overall project coverage + default: # context, you can create multiple ones with custom titles + enabled: yes # must be yes|true to enable this status + target: 100 # specify the target coverage for each commit status + # option: "auto" (must increase from parent commit or pull request base) + # option: "X%" a static target percentage to hit + if_not_found: success # if parent is not found report status as success, error, or failure + if_ci_failed: error # if ci fails report status as success, error, or failure + diff --git a/vendor/go.uber.org/multierr/.gitignore b/vendor/go.uber.org/multierr/.gitignore new file mode 100644 index 0000000..b9a05e3 --- /dev/null +++ b/vendor/go.uber.org/multierr/.gitignore @@ -0,0 +1,4 @@ +/vendor +cover.html +cover.out +/bin diff --git a/vendor/go.uber.org/multierr/CHANGELOG.md b/vendor/go.uber.org/multierr/CHANGELOG.md new file mode 100644 index 0000000..d2c8aad --- /dev/null +++ b/vendor/go.uber.org/multierr/CHANGELOG.md @@ -0,0 +1,80 @@ +Releases +======== + +v1.9.0 (2022-12-12) +=================== + +- Add `AppendFunc` that allow passsing functions to similar to + `AppendInvoke`. + +- Bump up yaml.v3 dependency to 3.0.1. + +v1.8.0 (2022-02-28) +=================== + +- `Combine`: perform zero allocations when there are no errors. + + +v1.7.0 (2021-05-06) +=================== + +- Add `AppendInvoke` to append into errors from `defer` blocks. + + +v1.6.0 (2020-09-14) +=================== + +- Actually drop library dependency on development-time tooling. + + +v1.5.0 (2020-02-24) +=================== + +- Drop library dependency on development-time tooling. + + +v1.4.0 (2019-11-04) +=================== + +- Add `AppendInto` function to more ergonomically build errors inside a + loop. + + +v1.3.0 (2019-10-29) +=================== + +- Switch to Go modules. + + +v1.2.0 (2019-09-26) +=================== + +- Support extracting and matching against wrapped errors with `errors.As` + and `errors.Is`. + + +v1.1.0 (2017-06-30) +=================== + +- Added an `Errors(error) []error` function to extract the underlying list of + errors for a multierr error. + + +v1.0.0 (2017-05-31) +=================== + +No changes since v0.2.0. This release is committing to making no breaking +changes to the current API in the 1.X series. + + +v0.2.0 (2017-04-11) +=================== + +- Repeatedly appending to the same error is now faster due to fewer + allocations. + + +v0.1.0 (2017-31-03) +=================== + +- Initial release diff --git a/vendor/go.uber.org/multierr/LICENSE.txt b/vendor/go.uber.org/multierr/LICENSE.txt new file mode 100644 index 0000000..413e30f --- /dev/null +++ b/vendor/go.uber.org/multierr/LICENSE.txt @@ -0,0 +1,19 @@ +Copyright (c) 2017-2021 Uber Technologies, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/go.uber.org/multierr/Makefile b/vendor/go.uber.org/multierr/Makefile new file mode 100644 index 0000000..dcb6fe7 --- /dev/null +++ b/vendor/go.uber.org/multierr/Makefile @@ -0,0 +1,38 @@ +# Directory to put `go install`ed binaries in. +export GOBIN ?= $(shell pwd)/bin + +GO_FILES := $(shell \ + find . '(' -path '*/.*' -o -path './vendor' ')' -prune \ + -o -name '*.go' -print | cut -b3-) + +.PHONY: build +build: + go build ./... + +.PHONY: test +test: + go test -race ./... + +.PHONY: gofmt +gofmt: + $(eval FMT_LOG := $(shell mktemp -t gofmt.XXXXX)) + @gofmt -e -s -l $(GO_FILES) > $(FMT_LOG) || true + @[ ! -s "$(FMT_LOG)" ] || (echo "gofmt failed:" | cat - $(FMT_LOG) && false) + +.PHONY: golint +golint: + @cd tools && go install golang.org/x/lint/golint + @$(GOBIN)/golint ./... + +.PHONY: staticcheck +staticcheck: + @cd tools && go install honnef.co/go/tools/cmd/staticcheck + @$(GOBIN)/staticcheck ./... + +.PHONY: lint +lint: gofmt golint staticcheck + +.PHONY: cover +cover: + go test -race -coverprofile=cover.out -coverpkg=./... -v ./... + go tool cover -html=cover.out -o cover.html diff --git a/vendor/go.uber.org/multierr/README.md b/vendor/go.uber.org/multierr/README.md new file mode 100644 index 0000000..70aacec --- /dev/null +++ b/vendor/go.uber.org/multierr/README.md @@ -0,0 +1,23 @@ +# multierr [![GoDoc][doc-img]][doc] [![Build Status][ci-img]][ci] [![Coverage Status][cov-img]][cov] + +`multierr` allows combining one or more Go `error`s together. + +## Installation + + go get -u go.uber.org/multierr + +## Status + +Stable: No breaking changes will be made before 2.0. + +------------------------------------------------------------------------------- + +Released under the [MIT License]. + +[MIT License]: LICENSE.txt +[doc-img]: https://pkg.go.dev/badge/go.uber.org/multierr +[doc]: https://pkg.go.dev/go.uber.org/multierr +[ci-img]: https://github.com/uber-go/multierr/actions/workflows/go.yml/badge.svg +[cov-img]: https://codecov.io/gh/uber-go/multierr/branch/master/graph/badge.svg +[ci]: https://github.com/uber-go/multierr/actions/workflows/go.yml +[cov]: https://codecov.io/gh/uber-go/multierr diff --git a/vendor/go.uber.org/multierr/error.go b/vendor/go.uber.org/multierr/error.go new file mode 100644 index 0000000..cdd91ae --- /dev/null +++ b/vendor/go.uber.org/multierr/error.go @@ -0,0 +1,681 @@ +// Copyright (c) 2017-2021 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +// Package multierr allows combining one or more errors together. +// +// # Overview +// +// Errors can be combined with the use of the Combine function. +// +// multierr.Combine( +// reader.Close(), +// writer.Close(), +// conn.Close(), +// ) +// +// If only two errors are being combined, the Append function may be used +// instead. +// +// err = multierr.Append(reader.Close(), writer.Close()) +// +// The underlying list of errors for a returned error object may be retrieved +// with the Errors function. +// +// errors := multierr.Errors(err) +// if len(errors) > 0 { +// fmt.Println("The following errors occurred:", errors) +// } +// +// # Appending from a loop +// +// You sometimes need to append into an error from a loop. +// +// var err error +// for _, item := range items { +// err = multierr.Append(err, process(item)) +// } +// +// Cases like this may require knowledge of whether an individual instance +// failed. This usually requires introduction of a new variable. +// +// var err error +// for _, item := range items { +// if perr := process(item); perr != nil { +// log.Warn("skipping item", item) +// err = multierr.Append(err, perr) +// } +// } +// +// multierr includes AppendInto to simplify cases like this. +// +// var err error +// for _, item := range items { +// if multierr.AppendInto(&err, process(item)) { +// log.Warn("skipping item", item) +// } +// } +// +// This will append the error into the err variable, and return true if that +// individual error was non-nil. +// +// See [AppendInto] for more information. +// +// # Deferred Functions +// +// Go makes it possible to modify the return value of a function in a defer +// block if the function was using named returns. This makes it possible to +// record resource cleanup failures from deferred blocks. +// +// func sendRequest(req Request) (err error) { +// conn, err := openConnection() +// if err != nil { +// return err +// } +// defer func() { +// err = multierr.Append(err, conn.Close()) +// }() +// // ... +// } +// +// multierr provides the Invoker type and AppendInvoke function to make cases +// like the above simpler and obviate the need for a closure. The following is +// roughly equivalent to the example above. +// +// func sendRequest(req Request) (err error) { +// conn, err := openConnection() +// if err != nil { +// return err +// } +// defer multierr.AppendInvoke(&err, multierr.Close(conn)) +// // ... +// } +// +// See [AppendInvoke] and [Invoker] for more information. +// +// NOTE: If you're modifying an error from inside a defer, you MUST use a named +// return value for that function. +// +// # Advanced Usage +// +// Errors returned by Combine and Append MAY implement the following +// interface. +// +// type errorGroup interface { +// // Returns a slice containing the underlying list of errors. +// // +// // This slice MUST NOT be modified by the caller. +// Errors() []error +// } +// +// Note that if you need access to list of errors behind a multierr error, you +// should prefer using the Errors function. That said, if you need cheap +// read-only access to the underlying errors slice, you can attempt to cast +// the error to this interface. You MUST handle the failure case gracefully +// because errors returned by Combine and Append are not guaranteed to +// implement this interface. +// +// var errors []error +// group, ok := err.(errorGroup) +// if ok { +// errors = group.Errors() +// } else { +// errors = []error{err} +// } +package multierr // import "go.uber.org/multierr" + +import ( + "bytes" + "errors" + "fmt" + "io" + "strings" + "sync" + + "go.uber.org/atomic" +) + +var ( + // Separator for single-line error messages. + _singlelineSeparator = []byte("; ") + + // Prefix for multi-line messages + _multilinePrefix = []byte("the following errors occurred:") + + // Prefix for the first and following lines of an item in a list of + // multi-line error messages. + // + // For example, if a single item is: + // + // foo + // bar + // + // It will become, + // + // - foo + // bar + _multilineSeparator = []byte("\n - ") + _multilineIndent = []byte(" ") +) + +// _bufferPool is a pool of bytes.Buffers. +var _bufferPool = sync.Pool{ + New: func() interface{} { + return &bytes.Buffer{} + }, +} + +type errorGroup interface { + Errors() []error +} + +// Errors returns a slice containing zero or more errors that the supplied +// error is composed of. If the error is nil, a nil slice is returned. +// +// err := multierr.Append(r.Close(), w.Close()) +// errors := multierr.Errors(err) +// +// If the error is not composed of other errors, the returned slice contains +// just the error that was passed in. +// +// Callers of this function are free to modify the returned slice. +func Errors(err error) []error { + if err == nil { + return nil + } + + // Note that we're casting to multiError, not errorGroup. Our contract is + // that returned errors MAY implement errorGroup. Errors, however, only + // has special behavior for multierr-specific error objects. + // + // This behavior can be expanded in the future but I think it's prudent to + // start with as little as possible in terms of contract and possibility + // of misuse. + eg, ok := err.(*multiError) + if !ok { + return []error{err} + } + + return append(([]error)(nil), eg.Errors()...) +} + +// multiError is an error that holds one or more errors. +// +// An instance of this is guaranteed to be non-empty and flattened. That is, +// none of the errors inside multiError are other multiErrors. +// +// multiError formats to a semi-colon delimited list of error messages with +// %v and with a more readable multi-line format with %+v. +type multiError struct { + copyNeeded atomic.Bool + errors []error +} + +var _ errorGroup = (*multiError)(nil) + +// Errors returns the list of underlying errors. +// +// This slice MUST NOT be modified. +func (merr *multiError) Errors() []error { + if merr == nil { + return nil + } + return merr.errors +} + +// As attempts to find the first error in the error list that matches the type +// of the value that target points to. +// +// This function allows errors.As to traverse the values stored on the +// multierr error. +func (merr *multiError) As(target interface{}) bool { + for _, err := range merr.Errors() { + if errors.As(err, target) { + return true + } + } + return false +} + +// Is attempts to match the provided error against errors in the error list. +// +// This function allows errors.Is to traverse the values stored on the +// multierr error. +func (merr *multiError) Is(target error) bool { + for _, err := range merr.Errors() { + if errors.Is(err, target) { + return true + } + } + return false +} + +func (merr *multiError) Error() string { + if merr == nil { + return "" + } + + buff := _bufferPool.Get().(*bytes.Buffer) + buff.Reset() + + merr.writeSingleline(buff) + + result := buff.String() + _bufferPool.Put(buff) + return result +} + +func (merr *multiError) Format(f fmt.State, c rune) { + if c == 'v' && f.Flag('+') { + merr.writeMultiline(f) + } else { + merr.writeSingleline(f) + } +} + +func (merr *multiError) writeSingleline(w io.Writer) { + first := true + for _, item := range merr.errors { + if first { + first = false + } else { + w.Write(_singlelineSeparator) + } + io.WriteString(w, item.Error()) + } +} + +func (merr *multiError) writeMultiline(w io.Writer) { + w.Write(_multilinePrefix) + for _, item := range merr.errors { + w.Write(_multilineSeparator) + writePrefixLine(w, _multilineIndent, fmt.Sprintf("%+v", item)) + } +} + +// Writes s to the writer with the given prefix added before each line after +// the first. +func writePrefixLine(w io.Writer, prefix []byte, s string) { + first := true + for len(s) > 0 { + if first { + first = false + } else { + w.Write(prefix) + } + + idx := strings.IndexByte(s, '\n') + if idx < 0 { + idx = len(s) - 1 + } + + io.WriteString(w, s[:idx+1]) + s = s[idx+1:] + } +} + +type inspectResult struct { + // Number of top-level non-nil errors + Count int + + // Total number of errors including multiErrors + Capacity int + + // Index of the first non-nil error in the list. Value is meaningless if + // Count is zero. + FirstErrorIdx int + + // Whether the list contains at least one multiError + ContainsMultiError bool +} + +// Inspects the given slice of errors so that we can efficiently allocate +// space for it. +func inspect(errors []error) (res inspectResult) { + first := true + for i, err := range errors { + if err == nil { + continue + } + + res.Count++ + if first { + first = false + res.FirstErrorIdx = i + } + + if merr, ok := err.(*multiError); ok { + res.Capacity += len(merr.errors) + res.ContainsMultiError = true + } else { + res.Capacity++ + } + } + return +} + +// fromSlice converts the given list of errors into a single error. +func fromSlice(errors []error) error { + // Don't pay to inspect small slices. + switch len(errors) { + case 0: + return nil + case 1: + return errors[0] + } + + res := inspect(errors) + switch res.Count { + case 0: + return nil + case 1: + // only one non-nil entry + return errors[res.FirstErrorIdx] + case len(errors): + if !res.ContainsMultiError { + // Error list is flat. Make a copy of it + // Otherwise "errors" escapes to the heap + // unconditionally for all other cases. + // This lets us optimize for the "no errors" case. + out := append(([]error)(nil), errors...) + return &multiError{errors: out} + } + } + + nonNilErrs := make([]error, 0, res.Capacity) + for _, err := range errors[res.FirstErrorIdx:] { + if err == nil { + continue + } + + if nested, ok := err.(*multiError); ok { + nonNilErrs = append(nonNilErrs, nested.errors...) + } else { + nonNilErrs = append(nonNilErrs, err) + } + } + + return &multiError{errors: nonNilErrs} +} + +// Combine combines the passed errors into a single error. +// +// If zero arguments were passed or if all items are nil, a nil error is +// returned. +// +// Combine(nil, nil) // == nil +// +// If only a single error was passed, it is returned as-is. +// +// Combine(err) // == err +// +// Combine skips over nil arguments so this function may be used to combine +// together errors from operations that fail independently of each other. +// +// multierr.Combine( +// reader.Close(), +// writer.Close(), +// pipe.Close(), +// ) +// +// If any of the passed errors is a multierr error, it will be flattened along +// with the other errors. +// +// multierr.Combine(multierr.Combine(err1, err2), err3) +// // is the same as +// multierr.Combine(err1, err2, err3) +// +// The returned error formats into a readable multi-line error message if +// formatted with %+v. +// +// fmt.Sprintf("%+v", multierr.Combine(err1, err2)) +func Combine(errors ...error) error { + return fromSlice(errors) +} + +// Append appends the given errors together. Either value may be nil. +// +// This function is a specialization of Combine for the common case where +// there are only two errors. +// +// err = multierr.Append(reader.Close(), writer.Close()) +// +// The following pattern may also be used to record failure of deferred +// operations without losing information about the original error. +// +// func doSomething(..) (err error) { +// f := acquireResource() +// defer func() { +// err = multierr.Append(err, f.Close()) +// }() +// +// Note that the variable MUST be a named return to append an error to it from +// the defer statement. See also [AppendInvoke]. +func Append(left error, right error) error { + switch { + case left == nil: + return right + case right == nil: + return left + } + + if _, ok := right.(*multiError); !ok { + if l, ok := left.(*multiError); ok && !l.copyNeeded.Swap(true) { + // Common case where the error on the left is constantly being + // appended to. + errs := append(l.errors, right) + return &multiError{errors: errs} + } else if !ok { + // Both errors are single errors. + return &multiError{errors: []error{left, right}} + } + } + + // Either right or both, left and right, are multiErrors. Rely on usual + // expensive logic. + errors := [2]error{left, right} + return fromSlice(errors[0:]) +} + +// AppendInto appends an error into the destination of an error pointer and +// returns whether the error being appended was non-nil. +// +// var err error +// multierr.AppendInto(&err, r.Close()) +// multierr.AppendInto(&err, w.Close()) +// +// The above is equivalent to, +// +// err := multierr.Append(r.Close(), w.Close()) +// +// As AppendInto reports whether the provided error was non-nil, it may be +// used to build a multierr error in a loop more ergonomically. For example: +// +// var err error +// for line := range lines { +// var item Item +// if multierr.AppendInto(&err, parse(line, &item)) { +// continue +// } +// items = append(items, item) +// } +// +// Compare this with a version that relies solely on Append: +// +// var err error +// for line := range lines { +// var item Item +// if parseErr := parse(line, &item); parseErr != nil { +// err = multierr.Append(err, parseErr) +// continue +// } +// items = append(items, item) +// } +func AppendInto(into *error, err error) (errored bool) { + if into == nil { + // We panic if 'into' is nil. This is not documented above + // because suggesting that the pointer must be non-nil may + // confuse users into thinking that the error that it points + // to must be non-nil. + panic("misuse of multierr.AppendInto: into pointer must not be nil") + } + + if err == nil { + return false + } + *into = Append(*into, err) + return true +} + +// Invoker is an operation that may fail with an error. Use it with +// AppendInvoke to append the result of calling the function into an error. +// This allows you to conveniently defer capture of failing operations. +// +// See also, [Close] and [Invoke]. +type Invoker interface { + Invoke() error +} + +// Invoke wraps a function which may fail with an error to match the Invoker +// interface. Use it to supply functions matching this signature to +// AppendInvoke. +// +// For example, +// +// func processReader(r io.Reader) (err error) { +// scanner := bufio.NewScanner(r) +// defer multierr.AppendInvoke(&err, multierr.Invoke(scanner.Err)) +// for scanner.Scan() { +// // ... +// } +// // ... +// } +// +// In this example, the following line will construct the Invoker right away, +// but defer the invocation of scanner.Err() until the function returns. +// +// defer multierr.AppendInvoke(&err, multierr.Invoke(scanner.Err)) +// +// Note that the error you're appending to from the defer statement MUST be a +// named return. +type Invoke func() error + +// Invoke calls the supplied function and returns its result. +func (i Invoke) Invoke() error { return i() } + +// Close builds an Invoker that closes the provided io.Closer. Use it with +// AppendInvoke to close io.Closers and append their results into an error. +// +// For example, +// +// func processFile(path string) (err error) { +// f, err := os.Open(path) +// if err != nil { +// return err +// } +// defer multierr.AppendInvoke(&err, multierr.Close(f)) +// return processReader(f) +// } +// +// In this example, multierr.Close will construct the Invoker right away, but +// defer the invocation of f.Close until the function returns. +// +// defer multierr.AppendInvoke(&err, multierr.Close(f)) +// +// Note that the error you're appending to from the defer statement MUST be a +// named return. +func Close(closer io.Closer) Invoker { + return Invoke(closer.Close) +} + +// AppendInvoke appends the result of calling the given Invoker into the +// provided error pointer. Use it with named returns to safely defer +// invocation of fallible operations until a function returns, and capture the +// resulting errors. +// +// func doSomething(...) (err error) { +// // ... +// f, err := openFile(..) +// if err != nil { +// return err +// } +// +// // multierr will call f.Close() when this function returns and +// // if the operation fails, its append its error into the +// // returned error. +// defer multierr.AppendInvoke(&err, multierr.Close(f)) +// +// scanner := bufio.NewScanner(f) +// // Similarly, this scheduled scanner.Err to be called and +// // inspected when the function returns and append its error +// // into the returned error. +// defer multierr.AppendInvoke(&err, multierr.Invoke(scanner.Err)) +// +// // ... +// } +// +// NOTE: If used with a defer, the error variable MUST be a named return. +// +// Without defer, AppendInvoke behaves exactly like AppendInto. +// +// err := // ... +// multierr.AppendInvoke(&err, mutltierr.Invoke(foo)) +// +// // ...is roughly equivalent to... +// +// err := // ... +// multierr.AppendInto(&err, foo()) +// +// The advantage of the indirection introduced by Invoker is to make it easy +// to defer the invocation of a function. Without this indirection, the +// invoked function will be evaluated at the time of the defer block rather +// than when the function returns. +// +// // BAD: This is likely not what the caller intended. This will evaluate +// // foo() right away and append its result into the error when the +// // function returns. +// defer multierr.AppendInto(&err, foo()) +// +// // GOOD: This will defer invocation of foo unutil the function returns. +// defer multierr.AppendInvoke(&err, multierr.Invoke(foo)) +// +// multierr provides a few Invoker implementations out of the box for +// convenience. See [Invoker] for more information. +func AppendInvoke(into *error, invoker Invoker) { + AppendInto(into, invoker.Invoke()) +} + +// AppendFunc is a shorthand for [AppendInvoke]. +// It allows using function or method value directly +// without having to wrap it into an [Invoker] interface. +// +// func doSomething(...) (err error) { +// w, err := startWorker(...) +// if err != nil { +// return err +// } +// +// // multierr will call w.Stop() when this function returns and +// // if the operation fails, it appends its error into the +// // returned error. +// defer multierr.AppendFunc(&err, w.Stop) +// } +func AppendFunc(into *error, fn func() error) { + AppendInvoke(into, Invoke(fn)) +} diff --git a/vendor/go.uber.org/multierr/glide.yaml b/vendor/go.uber.org/multierr/glide.yaml new file mode 100644 index 0000000..6ef084e --- /dev/null +++ b/vendor/go.uber.org/multierr/glide.yaml @@ -0,0 +1,8 @@ +package: go.uber.org/multierr +import: +- package: go.uber.org/atomic + version: ^1 +testImport: +- package: github.com/stretchr/testify + subpackages: + - assert diff --git a/vendor/gopkg.in/yaml.v2/go.mod b/vendor/gopkg.in/yaml.v2/go.mod deleted file mode 100644 index 2cbb85a..0000000 --- a/vendor/gopkg.in/yaml.v2/go.mod +++ /dev/null @@ -1,5 +0,0 @@ -module gopkg.in/yaml.v2 - -go 1.15 - -require gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 diff --git a/vendor/modules.txt b/vendor/modules.txt index 884d1d2..cc462bb 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,21 +1,41 @@ # github.com/emersion/go-imap v1.2.1 +## explicit; go 1.13 github.com/emersion/go-imap github.com/emersion/go-imap/client github.com/emersion/go-imap/commands github.com/emersion/go-imap/responses github.com/emersion/go-imap/utf7 # github.com/emersion/go-message v0.18.0 +## explicit; go 1.14 github.com/emersion/go-message github.com/emersion/go-message/charset github.com/emersion/go-message/mail github.com/emersion/go-message/textproto # github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 +## explicit; go 1.12 github.com/emersion/go-sasl # github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 +## explicit github.com/emersion/go-textwrapper # github.com/hashicorp/logutils v1.0.0 +## explicit github.com/hashicorp/logutils +# github.com/kr/text v0.2.0 +## explicit +# github.com/sourcegraph/conc v0.3.0 +## explicit; go 1.19 +github.com/sourcegraph/conc +github.com/sourcegraph/conc/internal/multierror +github.com/sourcegraph/conc/panics +github.com/sourcegraph/conc/pool +# go.uber.org/atomic v1.7.0 +## explicit; go 1.13 +go.uber.org/atomic +# go.uber.org/multierr v1.9.0 +## explicit; go 1.19 +go.uber.org/multierr # golang.org/x/text v0.14.0 +## explicit; go 1.18 golang.org/x/text/encoding golang.org/x/text/encoding/charmap golang.org/x/text/encoding/htmlindex @@ -35,4 +55,5 @@ golang.org/x/text/language golang.org/x/text/runes golang.org/x/text/transform # gopkg.in/yaml.v2 v2.4.0 +## explicit; go 1.15 gopkg.in/yaml.v2 From d7b1deb1acb85e4c22debedde6b348646e11d3aa Mon Sep 17 00:00:00 2001 From: Nik Soggia Date: Fri, 12 Apr 2024 09:56:12 +0200 Subject: [PATCH 03/19] add support for starttls --- config/config.dist.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/config.dist.yaml b/config/config.dist.yaml index 6b7eaa4..9223e1d 100644 --- a/config/config.dist.yaml +++ b/config/config.dist.yaml @@ -10,6 +10,8 @@ input: # debug: no # delete emails from server after fetch? # delete: no + # connection security should be: tls (default), starttls, plaintext + # security: "tls" output: # output file From bbae3cc2675df59b9e5bf20e92d7fac5f6074687 Mon Sep 17 00:00:00 2001 From: Nik Soggia Date: Fri, 12 Apr 2024 09:57:12 +0200 Subject: [PATCH 04/19] add support for starttls --- cmd/dmarc-report-converter/config.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/dmarc-report-converter/config.go b/cmd/dmarc-report-converter/config.go index 4bec34d..d387b5f 100644 --- a/cmd/dmarc-report-converter/config.go +++ b/cmd/dmarc-report-converter/config.go @@ -33,6 +33,7 @@ type IMAP struct { Mailbox string `yaml:"mailbox"` Debug bool `yaml:"debug"` Delete bool `yaml:"delete"` + Security string `yaml:"security"` } // IsConfigured return true if IMAP is configured From 63ea7eb6cf305671f27383154ac27c19a5ec6d34 Mon Sep 17 00:00:00 2001 From: Nik Soggia Date: Fri, 12 Apr 2024 10:00:05 +0200 Subject: [PATCH 05/19] add support for starttls I tried to be as paranoid as possible to avoid falling back to plaintext without notice, so I made a lot of checks. On the bright side, go-imap v2 will provide DialStartTLS() and all this code will be replaced with just one line. --- cmd/dmarc-report-converter/imap.go | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/cmd/dmarc-report-converter/imap.go b/cmd/dmarc-report-converter/imap.go index f753396..e45fa7e 100644 --- a/cmd/dmarc-report-converter/imap.go +++ b/cmd/dmarc-report-converter/imap.go @@ -1,6 +1,7 @@ package main import ( + "errors" "fmt" "io" "log" @@ -16,7 +17,31 @@ func fetchIMAPAttachments(cfg *config) error { log.Printf("[INFO] imap: connecting to server %v", cfg.Input.IMAP.Server) // connect to server - c, err := client.DialTLS(cfg.Input.IMAP.Server, nil) + var c *client.Client + var err error + if cfg.Input.IMAP.Security == "plaintext" { + c, err = client.Dial(cfg.Input.IMAP.Server) + } else if cfg.Input.IMAP.Security == "starttls" { + // go-imap v2 will replace all the following lines with + // c, err = client.DialStartTLS(cfg.Input.IMAP.Server, nil) + // and there will be no need to import "errors" + c, err = client.Dial(cfg.Input.IMAP.Server) + if err == nil { + sstRet, sstErr := c.SupportStartTLS(); + if sstErr != nil { + err = sstErr + } else if !sstRet { + err = errors.New("server doesn't support starttls") + } else { + err = c.StartTLS(nil); + } + } + if err != nil { + c.Logout() + } + } else { + c, err = client.DialTLS(cfg.Input.IMAP.Server, nil) + } if err != nil { return err } From fc98f29385e99b9eddf65523fd74cf5175c97d33 Mon Sep 17 00:00:00 2001 From: Nik Soggia Date: Fri, 12 Apr 2024 10:25:10 +0200 Subject: [PATCH 06/19] add support for starttls discourage users from using plaintext --- cmd/dmarc-report-converter/imap.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/dmarc-report-converter/imap.go b/cmd/dmarc-report-converter/imap.go index e45fa7e..17288fe 100644 --- a/cmd/dmarc-report-converter/imap.go +++ b/cmd/dmarc-report-converter/imap.go @@ -20,6 +20,7 @@ func fetchIMAPAttachments(cfg *config) error { var c *client.Client var err error if cfg.Input.IMAP.Security == "plaintext" { + log.Printf("[WARN] Without encryption your credentials may be stolen. Be careful!") c, err = client.Dial(cfg.Input.IMAP.Server) } else if cfg.Input.IMAP.Security == "starttls" { // go-imap v2 will replace all the following lines with From 92e7dbd3ef7a199f1437db56f63c324371da78b4 Mon Sep 17 00:00:00 2001 From: Nik Soggia Date: Fri, 12 Apr 2024 10:55:01 +0200 Subject: [PATCH 07/19] add support for starttls --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 750d077..54659a5 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,9 @@ Copy config/config.dist.yaml to config.yaml and change parameters: * **delete** (bool): delete email messages from IMAP server if reports are fetched successfully * **debug** (bool): print debug messages during IMAP session? + + * **security** (str): select encryption among "tls" (default), "starttls" or "plaintext" + **output** section: From d7d876c72ab72b3e7cd01d9c47274a904d810098 Mon Sep 17 00:00:00 2001 From: Nik Soggia Date: Fri, 12 Apr 2024 11:00:17 +0200 Subject: [PATCH 08/19] add support for starttls ...and learn proper english --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 54659a5..be84aa5 100644 --- a/README.md +++ b/README.md @@ -112,8 +112,7 @@ Copy config/config.dist.yaml to config.yaml and change parameters: * **debug** (bool): print debug messages during IMAP session? - * **security** (str): select encryption among "tls" (default), "starttls" or "plaintext" - + * **security** (str): select encryption between "tls" (default), "starttls" or "plaintext" **output** section: From de668198b8cecae39a41965cd3fb9c1cf78cd407 Mon Sep 17 00:00:00 2001 From: Pavel Podkorytov Date: Sat, 13 Apr 2024 20:28:49 +0500 Subject: [PATCH 09/19] go fmt on imap.go --- cmd/dmarc-report-converter/imap.go | 46 +++++++++++++++--------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/cmd/dmarc-report-converter/imap.go b/cmd/dmarc-report-converter/imap.go index 17288fe..dde7c94 100644 --- a/cmd/dmarc-report-converter/imap.go +++ b/cmd/dmarc-report-converter/imap.go @@ -18,31 +18,31 @@ func fetchIMAPAttachments(cfg *config) error { // connect to server var c *client.Client - var err error - if cfg.Input.IMAP.Security == "plaintext" { + var err error + if cfg.Input.IMAP.Security == "plaintext" { log.Printf("[WARN] Without encryption your credentials may be stolen. Be careful!") - c, err = client.Dial(cfg.Input.IMAP.Server) - } else if cfg.Input.IMAP.Security == "starttls" { - // go-imap v2 will replace all the following lines with - // c, err = client.DialStartTLS(cfg.Input.IMAP.Server, nil) + c, err = client.Dial(cfg.Input.IMAP.Server) + } else if cfg.Input.IMAP.Security == "starttls" { + // go-imap v2 will replace all the following lines with + // c, err = client.DialStartTLS(cfg.Input.IMAP.Server, nil) // and there will be no need to import "errors" - c, err = client.Dial(cfg.Input.IMAP.Server) - if err == nil { - sstRet, sstErr := c.SupportStartTLS(); - if sstErr != nil { - err = sstErr - } else if !sstRet { - err = errors.New("server doesn't support starttls") - } else { - err = c.StartTLS(nil); - } - } - if err != nil { - c.Logout() - } - } else { - c, err = client.DialTLS(cfg.Input.IMAP.Server, nil) - } + c, err = client.Dial(cfg.Input.IMAP.Server) + if err == nil { + sstRet, sstErr := c.SupportStartTLS() + if sstErr != nil { + err = sstErr + } else if !sstRet { + err = errors.New("server doesn't support starttls") + } else { + err = c.StartTLS(nil) + } + } + if err != nil { + c.Logout() + } + } else { + c, err = client.DialTLS(cfg.Input.IMAP.Server, nil) + } if err != nil { return err } From 258378eb44ec274686284818033d57941962341d Mon Sep 17 00:00:00 2001 From: Pavel Podkorytov Date: Sat, 13 Apr 2024 20:29:24 +0500 Subject: [PATCH 10/19] Fail when loading config if input.imap.security is unsupported --- cmd/dmarc-report-converter/config.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cmd/dmarc-report-converter/config.go b/cmd/dmarc-report-converter/config.go index d387b5f..750edf2 100644 --- a/cmd/dmarc-report-converter/config.go +++ b/cmd/dmarc-report-converter/config.go @@ -77,6 +77,14 @@ func loadConfig(path string) (*config, error) { return nil, fmt.Errorf("input.dir is not configured") } + if c.Input.IMAP.Security == "" { + c.Input.IMAP.Security = "tls" + } + + if c.Input.IMAP.Security != "tls" && c.Input.IMAP.Security != "starttls" && c.Input.IMAP.Security != "plaintext" { + return nil, fmt.Errorf("'input.imap.security' must be one of: tls, starttls, plaintext") + } + // Determine which template is used based upon Output.Format. t := txtTmpl switch c.Output.Format { From 170b2464ece181a1ab50cce40323fa35c7131b97 Mon Sep 17 00:00:00 2001 From: Cameron Moore Date: Sat, 13 Apr 2024 13:02:15 -0500 Subject: [PATCH 11/19] Move DNS concurrency support into pkg/dmarc --- cmd/dmarc-report-converter/convert.go | 16 +++---- cmd/dmarc-report-converter/files.go | 36 +--------------- pkg/dmarc/dmarc.go | 50 ++++++++++++++++++++-- pkg/dmarc/dmarc_test.go | 10 ++--- pkg/dmarc/merge_test.go | 6 +-- pkg/dmarc/read.go | 60 +++++++++++++++++++-------- 6 files changed, 104 insertions(+), 74 deletions(-) diff --git a/cmd/dmarc-report-converter/convert.go b/cmd/dmarc-report-converter/convert.go index c5537c2..cac62db 100644 --- a/cmd/dmarc-report-converter/convert.go +++ b/cmd/dmarc-report-converter/convert.go @@ -7,15 +7,11 @@ import ( "github.com/tierpod/dmarc-report-converter/pkg/dmarc" ) -func readParse(r io.Reader, fname string, lookupAddr bool) (dmarc.Report, error) { - var report dmarc.Report - var err error - +// readParse is a helper function that passes r, lookupAddr, and lookupLimit to +// dmarc.ReadParse. +// +// fname is the file name associated with r and is only used for debug logging. +func readParse(r io.Reader, fname string, lookupAddr bool, lookupLimit int) (dmarc.Report, error) { log.Printf("[DEBUG] parse: %v", fname) - - report, err = dmarc.ReadParse(r, lookupAddr) - if err != nil { - return dmarc.Report{}, err - } - return report, nil + return dmarc.ReadParse(r, lookupAddr, lookupLimit) } diff --git a/cmd/dmarc-report-converter/files.go b/cmd/dmarc-report-converter/files.go index f2068d4..612f2a4 100644 --- a/cmd/dmarc-report-converter/files.go +++ b/cmd/dmarc-report-converter/files.go @@ -2,12 +2,9 @@ package main import ( "log" - "net" "os" "path/filepath" - "time" - "github.com/sourcegraph/conc/pool" "github.com/tierpod/dmarc-report-converter/pkg/dmarc" ) @@ -37,12 +34,6 @@ func (c *filesConverter) ConvertWrite() error { c.convert() - if c.cfg.LookupAddr { - start := time.Now() - c.doDNSLookups() - log.Printf("[INFO] DNS lookups completed in %v", time.Since(start)) - } - if c.cfg.MergeReports { err = c.merge() if err != nil { @@ -62,31 +53,6 @@ func (c *filesConverter) ConvertWrite() error { return nil } -// doDNSLookups uses a limited goroutine pool to do concurrent DNS lookups. -func (c *filesConverter) doDNSLookups() { - p := pool.New().WithMaxGoroutines(c.cfg.LookupLimit) - - for _, r := range c.reports { - r := r - start := time.Now() - - for i, record := range r.Records { - i := i - record := record - - p.Go(func() { - hostnames, err := net.LookupAddr(record.Row.SourceIP) - if err == nil { - r.Records[i].Row.SourceHostname = hostnames[0] - } - }) - } - log.Printf("[DEBUG] files: %d DNS lookups for %s in %v", len(r.Records), r.ReportMetadata.OrgName, time.Since(start)) - } - - p.Wait() -} - func (c *filesConverter) find() error { emlFiles, err := filepath.Glob(filepath.Join(c.cfg.Input.Dir, "*.eml")) if err != nil { @@ -140,7 +106,7 @@ func (c *filesConverter) convert() { continue } - report, err := readParse(file, f, c.cfg.LookupAddr) + report, err := readParse(file, f, c.cfg.LookupAddr, c.cfg.LookupLimit) if err != nil { file.Close() log.Printf("[ERROR] files: %v in file %v, skip", err, f) diff --git a/pkg/dmarc/dmarc.go b/pkg/dmarc/dmarc.go index ed57b10..fe9eb34 100644 --- a/pkg/dmarc/dmarc.go +++ b/pkg/dmarc/dmarc.go @@ -5,9 +5,13 @@ import ( "encoding/json" "encoding/xml" "fmt" + "log" "math" + "net" "sort" "time" + + "github.com/sourcegraph/conc/pool" ) // ReportIDDateTime is the DateTime format for Report.ID @@ -214,17 +218,57 @@ type SPFAuthResult struct { Scope string `xml:"scope" json:"scope"` } -// Parse parses input xml data b to Report struct. If lookupAddr is true, performs a reverse -// lookups for feedback>record>row>source_ip -func Parse(b []byte, lookupAddr bool) (Report, error) { +// Parse parses input xml data b to Report struct. +// +// If lookupAddr is true, performs reverse DNS lookups for all +// feedback>record>row>source_ip entries. +// +// lookupLimit is the maximum pool size for doing concurrent DNS lookups. Any +// lookupLimit value less than 1 will disable concurrency by setting the pool +// size to 1. +func Parse(b []byte, lookupAddr bool, lookupLimit int) (Report, error) { var r Report err := xml.Unmarshal(b, &r) if err != nil { return Report{}, err } + if lookupAddr { + doPTRLookups(&r, lookupLimit) + } + r.SortRecords() r.CalculateStats() return r, nil } + +// doPTRLookups uses a limited goroutine pool to do concurrent DNS lookups for +// all record>row>source_ip entries in r. +// +// lookupLimit is the goroutine pool size. Any lookupLimit value less than 1 +// will essentially disable concurrency by setting the pool size to 1. +func doPTRLookups(r *Report, lookupLimit int) { + if lookupLimit < 1 { + lookupLimit = 1 + } + + p := pool.New().WithMaxGoroutines(lookupLimit) + + start := time.Now() + + for i, record := range r.Records { + i := i + record := record + + p.Go(func() { + hostnames, err := net.LookupAddr(record.Row.SourceIP) + if err == nil { + r.Records[i].Row.SourceHostname = hostnames[0] + } + }) + } + log.Printf("[INFO] Parse: completed %d DNS lookups in %v for report %s", len(r.Records), time.Since(start), r.ReportMetadata.ReportID) + + p.Wait() +} diff --git a/pkg/dmarc/dmarc_test.go b/pkg/dmarc/dmarc_test.go index 0f82352..c923321 100644 --- a/pkg/dmarc/dmarc_test.go +++ b/pkg/dmarc/dmarc_test.go @@ -137,7 +137,7 @@ func TestReadParseXML(t *testing.T) { } defer f.Close() - out, err := ReadParseXML(f, false) + out, err := ReadParseXML(f, false, 1) if err != nil { t.Fatalf("ReadParseXML: %v", err) } @@ -154,7 +154,7 @@ func TestReadParseGZIP(t *testing.T) { } defer f.Close() - out, err := ReadParseGZIP(f, false) + out, err := ReadParseGZIP(f, false, 1) if err != nil { t.Fatalf("ReadParseGZIP: %v", err) } @@ -171,7 +171,7 @@ func TestReadParseZIP(t *testing.T) { } defer f.Close() - out, err := ReadParseZIP(f, false) + out, err := ReadParseZIP(f, false, 1) if err != nil { t.Fatalf("ReadParseZIP: %v", err) } @@ -190,7 +190,7 @@ func TestReadParse(t *testing.T) { } defer f.Close() - out, err := ReadParse(f, false) + out, err := ReadParse(f, false, 1) if err != nil { t.Fatalf("ReadParse(%v): %v", testFile, err) } @@ -253,7 +253,7 @@ func TestReadParse_Empty(t *testing.T) { t.Fatalf("ReadParse(%v): %v", testFile, err) } defer f.Close() - out, err := ReadParse(f, false) + out, err := ReadParse(f, false, 1) if err != nil { t.Fatalf("ReadParse(%v): %v", testFile, err) } diff --git a/pkg/dmarc/merge_test.go b/pkg/dmarc/merge_test.go index 1b33875..b946c09 100644 --- a/pkg/dmarc/merge_test.go +++ b/pkg/dmarc/merge_test.go @@ -10,7 +10,7 @@ func TestReport_MergeRecord(t *testing.T) { // we have tested this errors already f, _ := os.Open("testdata/test.xml") defer f.Close() - report, _ := ReadParseXML(f, false) + report, _ := ReadParseXML(f, false, 1) // this record must be merged with the xmlRecord2 r1 := xmlRecord2 @@ -79,7 +79,7 @@ func TestReport_MergeReport(t *testing.T) { } defer f.Close() - rep1, err := ReadParseXML(f, false) + rep1, err := ReadParseXML(f, false, 1) if err != nil { t.Fatalf("Report_MergeReport: %v", err) } @@ -91,7 +91,7 @@ func TestReport_MergeReport(t *testing.T) { } defer f.Close() - rep2, err := ReadParseXML(f, false) + rep2, err := ReadParseXML(f, false, 1) if err != nil { t.Fatalf("Report_MergeReport: %v", err) } diff --git a/pkg/dmarc/read.go b/pkg/dmarc/read.go index 051b492..18a85f4 100644 --- a/pkg/dmarc/read.go +++ b/pkg/dmarc/read.go @@ -21,20 +21,32 @@ const ( MimeTypeXML = "text/xml" ) -// ReadParseXML reads xml data from r and parses it to Report struct. If lookupAddr is -// true, performs a reverse lookups for feedback>record>row>source_ip -func ReadParseXML(r io.Reader, lookupAddr bool) (Report, error) { +// ReadParseXML reads xml data from r and parses it to Report struct. +// +// If lookupAddr is true, performs reverse DNS lookups for all +// feedback>record>row>source_ip entries. +// +// lookupLimit is the maximum pool size for doing concurrent DNS lookups. Any +// lookupLimit value less than 1 will disable concurrency by setting the pool +// size to 1. +func ReadParseXML(r io.Reader, lookupAddr bool, lookupLimit int) (Report, error) { data, err := io.ReadAll(r) if err != nil { return Report{}, err } - return Parse(data, lookupAddr) + return Parse(data, lookupAddr, lookupLimit) } -// ReadParseGZIP reads gzipped xml data from r and parses it to Report struct. If lookupAddr is -// true, performs a reverse lookups for feedback>record>row>source_ip -func ReadParseGZIP(r io.Reader, lookupAddr bool) (Report, error) { +// ReadParseGZIP reads gzipped xml data from r and parses it to Report struct. +// +// If lookupAddr is true, performs reverse DNS lookups for all +// feedback>record>row>source_ip entries. +// +// lookupLimit is the maximum pool size for doing concurrent DNS lookups. Any +// lookupLimit value less than 1 will disable concurrency by setting the pool +// size to 1. +func ReadParseGZIP(r io.Reader, lookupAddr bool, lookupLimit int) (Report, error) { gr, err := gzip.NewReader(r) if err != nil { return Report{}, err @@ -53,17 +65,23 @@ func ReadParseGZIP(r io.Reader, lookupAddr bool) (Report, error) { mtype := http.DetectContentType(data) if mtype == MimeTypeGZIP { log.Printf("[DEBUG] ReadParseGZIP: detected nested %v mimetype", mtype) - return ReadParseGZIP(buf, lookupAddr) + return ReadParseGZIP(buf, lookupAddr, lookupLimit) } else if strings.HasPrefix(mtype, MimeTypeXML) { - return ReadParseXML(buf, lookupAddr) + return ReadParseXML(buf, lookupAddr, lookupLimit) } return Report{}, fmt.Errorf("ReadParseGZIP: supported mimetypes not found") } -// ReadParseZIP reads zipped xml data from r and parses it to Report struct. If lookupAddr is -// true, performs a reverse lookups for feedback>record>row>source_ip -func ReadParseZIP(r io.Reader, lookupAddr bool) (Report, error) { +// ReadParseZIP reads zipped xml data from r and parses it to Report struct. +// +// If lookupAddr is true, performs reverse DNS lookups for all +// feedback>record>row>source_ip entries. +// +// lookupLimit is the maximum pool size for doing concurrent DNS lookups. Any +// lookupLimit value less than 1 will disable concurrency by setting the pool +// size to 1. +func ReadParseZIP(r io.Reader, lookupAddr bool, lookupLimit int) (Report, error) { zipBytes, err := io.ReadAll(r) if err != nil { return Report{}, err @@ -92,7 +110,7 @@ func ReadParseZIP(r io.Reader, lookupAddr bool) (Report, error) { } defer rr.Close() - return ReadParseXML(rr, lookupAddr) + return ReadParseXML(rr, lookupAddr, lookupLimit) } return Report{}, err @@ -100,8 +118,14 @@ func ReadParseZIP(r io.Reader, lookupAddr bool) (Report, error) { // ReadParse reads any data from reader r, detects mimetype, and parses it to // Report struct (if mimetype is supported). -// If lookupAddr is true, performs reverse lookups for feedback>record>row>source_ip -func ReadParse(r io.Reader, lookupAddr bool) (Report, error) { +// +// If lookupAddr is true, performs reverse DNS lookups for all +// feedback>record>row>source_ip entries. +// +// lookupLimit is the maximum pool size for doing concurrent DNS lookups. Any +// lookupLimit value less than 1 will disable concurrency by setting the pool +// size to 1. +func ReadParse(r io.Reader, lookupAddr bool, lookupLimit int) (Report, error) { data, err := io.ReadAll(r) if err != nil { return Report{}, err @@ -112,11 +136,11 @@ func ReadParse(r io.Reader, lookupAddr bool) (Report, error) { br := bytes.NewReader(data) if mtype == MimeTypeGZIP { - return ReadParseGZIP(br, lookupAddr) + return ReadParseGZIP(br, lookupAddr, lookupLimit) } else if mtype == MimeTypeZIP { - return ReadParseZIP(br, lookupAddr) + return ReadParseZIP(br, lookupAddr, lookupLimit) } else if strings.HasPrefix(mtype, MimeTypeXML) { - return ReadParseXML(br, lookupAddr) + return ReadParseXML(br, lookupAddr, lookupLimit) } return Report{}, fmt.Errorf("mimetype %v not supported", mtype) From 7f3a0a565be09faf0ef40b3a24a3d91fda4ece6a Mon Sep 17 00:00:00 2001 From: Cameron Moore Date: Sat, 13 Apr 2024 15:07:46 -0500 Subject: [PATCH 12/19] Fix incorrect parsing of auth_results The DMARC spec allows for zero or more DKIM results and one or more SPF results. Prior to this commit, we only allowed for a single result for each type. Update the dmarc.AuthResults struct to use a slice for each type. For reference, see the definition of AuthResultType in Appendix C of RFC 7489. This change in the data model required updates to the Go templates. The two HTML templates are almost exactly the same, so pull the common body out into a separate const variable to ease maintenance. Fixes #45 --- cmd/dmarc-report-converter/consts.go | 239 ++++++++------------------- pkg/dmarc/dmarc.go | 4 +- pkg/dmarc/dmarc_test.go | 63 ++++--- pkg/dmarc/merge_test.go | 20 ++- pkg/dmarc/testdata/test.xml | 9 +- pkg/dmarc/testdata/test.xml.gz | Bin 515 -> 536 bytes pkg/dmarc/testdata/test.xml.gz.gz | Bin 538 -> 559 bytes pkg/dmarc/testdata/test.xml.zip | Bin 654 -> 675 bytes pkg/dmarc/testdata/test2.xml | 9 +- 9 files changed, 136 insertions(+), 208 deletions(-) diff --git a/cmd/dmarc-report-converter/consts.go b/cmd/dmarc-report-converter/consts.go index 456e025..c1e6ceb 100644 --- a/cmd/dmarc-report-converter/consts.go +++ b/cmd/dmarc-report-converter/consts.go @@ -1,39 +1,6 @@ package main -const htmlTmpl = ` - - - - - - - - - - - - - - - - - - +const htmlMain = `

@@ -121,25 +88,40 @@ table.table.bottomless {{.Row.PolicyEvaluated.SPF}} {{- end }} - {{.AuthResults.DKIM.Domain}} - - {{- if eq .AuthResults.DKIM.Result "pass"}} - {{.AuthResults.DKIM.Result}} - {{- else if eq .AuthResults.DKIM.Result "fail"}} - {{.AuthResults.DKIM.Result}} - {{- else}} - {{.AuthResults.DKIM.Result}} - {{- end}} + + {{- $len := len .AuthResults.DKIM }} + {{- range $j, $foo := .AuthResults.DKIM }} + {{- .Domain }} + {{- if lt $j $len }}
{{ end }} + {{- end -}} - {{.AuthResults.SPF.Domain}} - - {{- if eq .AuthResults.SPF.Result "pass"}} - {{.AuthResults.SPF.Result}} - {{- else if eq .AuthResults.SPF.Result "fail"}} - {{.AuthResults.SPF.Result}} - {{- else}} - {{.AuthResults.SPF.Result}} - {{- end}} + + {{- range $j, $d := .AuthResults.DKIM }} + {{.Result}} + {{- if lt $j $len }}
{{ end }} + {{- end -}} + + + {{- $len = len .AuthResults.SPF }} + {{- range $j, $foo := .AuthResults.SPF }} + {{- .Domain }} + {{- if lt $j $len }}
{{ end }} + {{- end -}} + + {{- range $j, $d := .AuthResults.SPF }} + {{.Result}} + {{- if lt $j $len }}
{{ end }} + {{- end -}} {{- end }} @@ -148,6 +130,42 @@ table.table.bottomless
+` + +const htmlTmpl = ` + + + + + + + + + + + + + + + + + +` + htmlMain + ` ` @@ -179,122 +197,7 @@ table.table.bottomless - -

-
-
-
-
-
- DMARC Report, id {{.Report.ReportMetadata.ReportID}} -
-
- - - - - - - - - - - - - - - -
Organization{{.Report.ReportMetadata.OrgName}} ({{.Report.ReportMetadata.Email}})
Date rangesince {{.Report.ReportMetadata.DateRange.Begin.UTC}} until {{.Report.ReportMetadata.DateRange.End.UTC}}
Policy published{{.Report.PolicyPublished.Domain}}: p={{.Report.PolicyPublished.Policy}} sp={{.Report.PolicyPublished.SPolicy}} pct={{.Report.PolicyPublished.Pct}} adkim={{.Report.PolicyPublished.ADKIM}} aspf={{.Report.PolicyPublished.ASPF}}
-
-
-
-
-

-
-
-
-
passed {{ .Report.MessagesStats.PassedPercent }}%
-
-
-
- passed {{ .Report.MessagesStats.Passed }} failed {{ .Report.MessagesStats.Failed }} total {{ .Report.MessagesStats.All }} -
-
-

-
-
- - - - - - - - - - - - - - - - - - - - - - {{- range .Report.Records }} - {{- if .IsPassed }} - - {{- else }} - - {{- end }} - - - - - - - - - - - - {{- end }} - -
policy evaluatedauth results
iphostnamemsgsdispositionDKIMSPFDKIM domainresultSPF domainresult
{{.Row.SourceIP}}{{.Row.SourceHostname}}{{.Row.Count}}{{.Row.PolicyEvaluated.Disposition}} - {{- if eq .Row.PolicyEvaluated.DKIM "fail" }} - {{.Row.PolicyEvaluated.DKIM}} - {{- else }} - {{.Row.PolicyEvaluated.DKIM}} - {{- end}} - - {{- if eq .Row.PolicyEvaluated.SPF "fail" }} - {{.Row.PolicyEvaluated.SPF}} - {{- else }} - {{.Row.PolicyEvaluated.SPF}} - {{- end }} - {{.AuthResults.DKIM.Domain}} - {{- if eq .AuthResults.DKIM.Result "pass"}} - {{.AuthResults.DKIM.Result}} - {{- else if eq .AuthResults.DKIM.Result "fail"}} - {{.AuthResults.DKIM.Result}} - {{- else}} - {{.AuthResults.DKIM.Result}} - {{- end}} - {{.AuthResults.SPF.Domain}} - {{- if eq .AuthResults.SPF.Result "pass"}} - {{.AuthResults.SPF.Result}} - {{- else if eq .AuthResults.SPF.Result "fail"}} - {{.AuthResults.SPF.Result}} - {{- else}} - {{.AuthResults.SPF.Result}} - {{- end}} -
-
-
-
+` + htmlMain + ` ` diff --git a/pkg/dmarc/dmarc.go b/pkg/dmarc/dmarc.go index d106962..a2f4c27 100644 --- a/pkg/dmarc/dmarc.go +++ b/pkg/dmarc/dmarc.go @@ -197,8 +197,8 @@ type Identifiers struct { // AuthResults represents feedback>record>auth_results section type AuthResults struct { - DKIM DKIMAuthResult `xml:"dkim" json:"dkim"` - SPF SPFAuthResult `xml:"spf" json:"spf"` + DKIM []DKIMAuthResult `xml:"dkim" json:"dkim"` + SPF []SPFAuthResult `xml:"spf" json:"spf"` } // DKIMAuthResult represnets feedback>record>auth_results>dkim sections diff --git a/pkg/dmarc/dmarc_test.go b/pkg/dmarc/dmarc_test.go index 0f82352..e0c00a3 100644 --- a/pkg/dmarc/dmarc_test.go +++ b/pkg/dmarc/dmarc_test.go @@ -43,15 +43,24 @@ var xmlRecord1 = Record{ EnvelopeFrom: "", }, AuthResults: AuthResults{ - DKIM: DKIMAuthResult{ - Domain: "test.net", - Result: "pass", - Selector: "selector", + DKIM: []DKIMAuthResult{ + { + Domain: "test1.net", + Result: "fail", + Selector: "selector1", + }, + { + Domain: "test2.net", + Result: "pass", + Selector: "selector2", + }, }, - SPF: SPFAuthResult{ - Domain: "test.net", - Result: "pass", - Scope: "mfrom", + SPF: []SPFAuthResult{ + { + Domain: "test.net", + Result: "pass", + Scope: "mfrom", + }, }, }, } @@ -71,15 +80,19 @@ var xmlRecord2 = Record{ EnvelopeFrom: "", }, AuthResults: AuthResults{ - DKIM: DKIMAuthResult{ - Domain: "test2.net", - Result: "fail", - Selector: "selector", + DKIM: []DKIMAuthResult{ + { + Domain: "test2.net", + Result: "fail", + Selector: "selector", + }, }, - SPF: SPFAuthResult{ - Domain: "test2.net", - Result: "softfail", - Scope: "mfrom", + SPF: []SPFAuthResult{ + { + Domain: "test2.net", + Result: "softfail", + Scope: "mfrom", + }, }, }, } @@ -225,15 +238,13 @@ var xmlEmptyReport = Report{ EnvelopeFrom: "", }, AuthResults: AuthResults{ - DKIM: DKIMAuthResult{ - Domain: "", - Result: "", - Selector: "", - }, - SPF: SPFAuthResult{ - Domain: "", - Result: "", - Scope: "", + DKIM: nil, + SPF: []SPFAuthResult{ + { + Domain: "", + Result: "", + Scope: "", + }, }, }, }, @@ -259,6 +270,6 @@ func TestReadParse_Empty(t *testing.T) { } if !reflect.DeepEqual(out, xmlEmptyReport) { - t.Errorf("ReadParse(%v): parsed structs are invalid: %+v", testFile, out) + t.Errorf("ReadParse(%v): parsed structs are invalid:\nGOT:\n%#v\nWANT:\n%#v", testFile, out, xmlEmptyReport) } } diff --git a/pkg/dmarc/merge_test.go b/pkg/dmarc/merge_test.go index 1b33875..0cc28eb 100644 --- a/pkg/dmarc/merge_test.go +++ b/pkg/dmarc/merge_test.go @@ -46,15 +46,19 @@ func TestReport_MergeRecord(t *testing.T) { EnvelopeFrom: "", }, AuthResults: AuthResults{ - DKIM: DKIMAuthResult{ - Domain: "test3.net", - Result: "fail", - Selector: "selector", + DKIM: []DKIMAuthResult{ + { + Domain: "test3.net", + Result: "fail", + Selector: "selector", + }, }, - SPF: SPFAuthResult{ - Domain: "test3.net", - Result: "softfail", - Scope: "mfrom", + SPF: []SPFAuthResult{ + { + Domain: "test3.net", + Result: "softfail", + Scope: "mfrom", + }, }, }, } diff --git a/pkg/dmarc/testdata/test.xml b/pkg/dmarc/testdata/test.xml index 3de6798..e590988 100644 --- a/pkg/dmarc/testdata/test.xml +++ b/pkg/dmarc/testdata/test.xml @@ -32,9 +32,14 @@ - test.net + test1.net + fail + selector1 + + + test2.net pass - selector + selector2 test.net diff --git a/pkg/dmarc/testdata/test.xml.gz b/pkg/dmarc/testdata/test.xml.gz index fdd5cb10af9e97ec7ed73da692b3c46bd8062a58..891794877382f1f9aa8fc63c9a83c187facf8627 100644 GIT binary patch literal 536 zcmV+z0_Xi7iwFpQst{%X19W9`bS`*pYyh=Y!E)Om5WVLsH2DBwG)ZcAD9JVV-ZdCj z783~=Kxz8-1tBEWPHj&*J{jIFyYKBjkI3^!-xcp*oz&{bJuWx*&-9Ko0AkPh8@($E zV!>$Z>mEE4%(I9ERNF(XSP%3kIA6RfUJ^88DFu2aJ8HD^J#!xHiw~&;B{-!_wWXlA zl%m`av>;QF4{uq`wepMyST&lq-ka|T#cHU9&V-VuN$GVU47FwIFmVdd9u87b{8&BU zZS}ATEfbRCpu`xhp0?k1yCM`6+)Q>o8e()O`S02c`%bzR@>`*UY(*pKQb8EQ z1f(bvZ?dPBpmdl(XPOau_X$1tCeCM9xs39IkN}sh4W9HgsCRYEN#X3D$4z)BC z@2V0%ZA)BYf)+R@ay=+d9|=l_3{TzQopnQ)jmUv0Sh}!!>4UK*(raXu7X=&UTu7L; zCvrh{3d{poDLm>KmXUQ4DL{GI$YAUFSqn^ntsATR)e_d|ROaL{VS{gL3vTHAQtycC zYQ6Y$zmwNv1=!1}-OA@_`Jeba1#aN;>UKuI;IeCt{&kaG aAmDW4|3%_|okq~|jr;>`SdCpp2LJ#aDgXxn literal 515 zcmV+e0{s0SiwFo3ulivC19W9`bS`*pYyh=Y!E)Om5WVLsH2DBwX_D0LP||Dey=yS6 zEG7~#fRgm@yBLsFCv`K^`eb;!?7p}A#-fjJ(^$QL^UCONw@tmdePlPZ2atO%p4m-R zQ3uwz&`l7y-vg#nlHin*Yb(i` zTFH7t$qC6t-h$(uFgkF7u44vcpkU1f=hlA3rxo_{9 zZF{%DmI*0w(DE2I)_`Nk zK#FqttS06tNrw!2+aIy`n;^@y(LjkUcM(_1vFMBy+&4fQLyFSW5S|6 zkwe)junb_O@T?~;qv)baf)1)zXzS(K09=CWdS|B964q!g^X@U_a~L`YejdZA-Wk`6 z>&;e=1+t)gS!sn4L@q#|`FL5{p6o}?M-eL(YkkUP zn`p307Hh9%u^#_*KD1a0T)|@P^*q*IUUHIjO_TkFfH!^k6N!Iy8YQRC(=X`LtT4a^ F007cp04e|g diff --git a/pkg/dmarc/testdata/test.xml.gz.gz b/pkg/dmarc/testdata/test.xml.gz.gz index 2f8ffe88f20ab3f23283508ee8932bb450955905..864c44f6b731e10c45b2edbadaf0f881f09f2765 100644 GIT binary patch literal 559 zcmV+~0?_>*iwFP!000000|6KU=lvgx2nc?v5M}@abY*jNE_iKh0JT-Ya@!yfz2_@5 z`2b-wNosc}$u;-hH5gVF6A2hVY5MmCAtcpKZBIHr8Qw0t@9jR1$n!_v74Kl3)au7Q zE;skj^o}$DV$b*+y(2)9s zwPorsaSG5L4pLG4SUuov^{@#o6O!Yg#2Brfw%>NUA{1Ad3;Mg{=(64vNc z=HxM9gKujKZs`0{?}+Paz4&y$lhV)kO-c#a#0EH3YaeK>N6j(0RPfZI}N5I^IXDl>L2`)i=@Q=X_q>%I9hMpZGil xZs7Ckc1FM8vTKe0b(38n;B@2vMdE**M$qz&`~z)Rja@|t007v_7!DW$008#j1fBo@ literal 538 zcmV+#0_FW5iwFP!000000|5g9{QVz`2nZ6d`e6VAbY*jNE_iKh0JT-Ya@!yfz2_@5 z`2b;QlGN@{(rfO$YcQ-VCK51!lJxJp7?4#bbu-iYWO%#mzPJ0vqK|LWSiOMr%II&m zO})8&WH+=2kb5qk*-ceZ2iCaIO%S-`fkzBc;|`tX6R;oPL-nLZP050#6qvXgnKeF4 z+y`*q1Ex}v;FOYUE6JK#$$CS{3CTs?g5#YqI&guo?hPA4uwMy@)yM#&#ggxn(rY9P zo#XnDIU%%%gVLV@)&I&wqJI;3XAK)h5TM_A-f(8wPsY}XJ_aA zsQdustu!cGvq-wufMdu&igNj^CgvzfhYWh#AF=qGAj`DTK#47P5m*y7#KV!&N9}RU z!nnf3nb$?EH?tGaDa)E&TQ?8ex~UsVPH;&SX4Zk-Q<4q^&fVaJk2B6jmOvD&Jg!~^ zG*%|PMpk)Iu;t!k!lFHqL)j^?3}B`3tS2s`=%Px34yspZ>*d)1T!QO*XQtH>)@UyC z?lI+a7&-@j9>b~L8P|*J%~p>EvY>ofX@wC)FpiZ2iZ0NieBG0-X7WG!3uA#zaS+(~ zcv;$>>_^T=5i1pIeadB(Fu(=?01j>_83O_U0Bg4cpa1{> diff --git a/pkg/dmarc/testdata/test.xml.zip b/pkg/dmarc/testdata/test.xml.zip index 4ec40c21adfe8e79329b52e72ed429545393d3f3..6f4e96ab62b1d217b2a18bdc276e83f47eb745b2 100644 GIT binary patch literal 675 zcmWIWW@Zs#U|`^2h+ftjksHw)`+lhO~2nWl?W3P=9F;A{PmqxQRS*% zuD_L?pZM&4U-W&#+$*n++v=HrKmVoAgdyqt59z(yTuBErUcZ*J5k9lI zgEi!~^V$ghFX|TYd$i(&Op-NOtJJKswtTiNmV499zmi4OVyVy8r3-dUS#(EWi$d^3 z{`|RDow}>MMgnt_yN}L4kiFY#-!)-N30U zJn8X*BNA)pZM$E#TS+8*Cik!1cYMlhrIJe2f4;xP^V@Hi*r(`)%{i{hFM}K<4zn!L z5YFGUeAAML9RG`CIvupWRAR3;RFosFiI5-&PZm z_p5~LbG>7dn#3&Bi)=Ogk(-c_|ZmCkdLdX>s`)s<`Y z(U?y^Oj&lAIsD$yd!cgAncV6B8LaNPubJ|qh?%i3NV!nlaLbcw!o4%oe?ALZ&@D{X{pGJ9a1w*?R@UA7u;uF7BuO>lu6mzCuc>Sta3id zdD8V>)8n}YRt1e4-tkI1ZHVXWyn2FR@zF`|Ctu1|;hFpR*91$&E$z*h*6hrGKP$I- z`@w0PnuQlWN#x3Ji!a;F)&2TV>z}wh#ocneThl@wrKdmtyW$VS)3qiV(VIPXU28}_ z;m}p|bB)?F4bOI-s@w7#f6P`mz2%}qr*64mgs|z6!xJyLzb!m-Xvd)=Gq1a@G2bkF zLG$#i*l2V9+q+gud-)Z3ilkj@yc;};kM-Q$2d(|a=iH__h^(<$7sqN6rnc8DtTJ%J z(|gH>7THd8zNxu^Ct>M_HR^hcUo7%!;#(D-@%rgahM1<;Rq4yF3SG<4`~1%EX|aSA zd!5|jmG>mCcfURx*3Y_WK}}?4tDuWP-&E!uw6_K~* zy>UP1E~qV - test.net + test1.net + fail + selector1 + + + test2.net pass - selector + selector2 test.net From 9a8b613469bc7b9bfabe54823e7d6e0a806b6c18 Mon Sep 17 00:00:00 2001 From: Cameron Moore Date: Sat, 13 Apr 2024 16:16:40 -0500 Subject: [PATCH 13/19] Update text template to support multiple DKIM results --- cmd/dmarc-report-converter/consts.go | 36 ++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/cmd/dmarc-report-converter/consts.go b/cmd/dmarc-report-converter/consts.go index c1e6ceb..bf333f4 100644 --- a/cmd/dmarc-report-converter/consts.go +++ b/cmd/dmarc-report-converter/consts.go @@ -201,22 +201,38 @@ table.table.bottomless ` +// NOTE(moorereason): This template assumes only one SPF result will be present even though the DMARC spec allows for multiple. const txtTmpl = ` DMARC report with id {{.ReportMetadata.ReportID}} Organization: {{.ReportMetadata.ExtraContactInfo}} ({{.ReportMetadata.Email}})) Date range: since {{.ReportMetadata.DateRange.Begin.UTC}} until {{.ReportMetadata.DateRange.End.UTC}} Policy published: {{.PolicyPublished.Domain}}: p={{.PolicyPublished.Policy}} sp={{.PolicyPublished.SPolicy}} pct={{.PolicyPublished.Pct}} adkim={{.PolicyPublished.ADKIM}} aspf={{.PolicyPublished.ASPF}} ------------------------------------------------------------------------------------------------------------------------- -{{printf "%23v | %32v | %57v |" "" "policy evaluated" "auth results" }} ------------------------------------------------------------------------------------------------------------------------- -{{printf "%16v | %4v | %10v | %8v | %8v | %16v | %8v | %16v | %8v | %v" "ip" "msgs" "disp" "dkim" "spf" "dkim domain" "dkim res" "spf domain" "spf res" "hostname" }} ------------------------------------------------------------------------------------------------------------------------- +-------------------------------------------------------------------------------------------------------------------------- +{{printf "%24v | %32v | %58v |" "" "policy evaluated" "auth results" }} +-------------------------------------------------------------------------------------------------------------------------- +{{printf "%17v | %4v | %10v | %8v | %8v | %16v | %9v | %16v | %8v | %v" "ip" "msgs" "disp" "dkim" "spf" "dkim domain" "dkim res" "spf domain" "spf res" "hostname" }} +-------------------------------------------------------------------------------------------------------------------------- {{- range .Records }} -{{- if .IsPassed }} -{{ printf "* %14v | %4v | %10v | %8v | %8v | %16v | %8v | %16v | %8v | %v" .Row.SourceIP .Row.Count .Row.PolicyEvaluated.Disposition .Row.PolicyEvaluated.DKIM .Row.PolicyEvaluated.SPF .AuthResults.DKIM.Domain .AuthResults.DKIM.Result .AuthResults.SPF.Domain .AuthResults.SPF.Result .Row.SourceHostname}} -{{- else }} -{{ printf "%16v | %4v | %10v | %8v | %8v | %16v | %8v | %16v | %8v | %v" .Row.SourceIP .Row.Count .Row.PolicyEvaluated.Disposition .Row.PolicyEvaluated.DKIM .Row.PolicyEvaluated.SPF .AuthResults.DKIM.Domain .AuthResults.DKIM.Result .AuthResults.SPF.Domain .AuthResults.SPF.Result .Row.SourceHostname}} -{{- end}} + {{- $prefix := " " }}{{ if .IsPassed }}{{ $prefix = "* " }}{{ end }} + {{- $dkimLen := len .AuthResults.DKIM }} + {{- if eq $dkimLen 0 }} +{{ printf "%2v%15v | %4v | %10v | %8v | %8v | %16v | %9v | %16v | %8v | %v" $prefix .Row.SourceIP .Row.Count .Row.PolicyEvaluated.Disposition .Row.PolicyEvaluated.DKIM .Row.PolicyEvaluated.SPF "" "" (index .AuthResults.SPF 0).Domain (index .AuthResults.SPF 0).Result .Row.SourceHostname }} + {{- else if eq $dkimLen 1 }} + {{- $dkimDomain := (index .AuthResults.DKIM 0).Domain }} + {{- $dkimResult := (index .AuthResults.DKIM 0).Result }} +{{ printf "%2v%15v | %4v | %10v | %8v | %8v | %16v | %9v | %16v | %8v | %v" $prefix .Row.SourceIP .Row.Count .Row.PolicyEvaluated.Disposition .Row.PolicyEvaluated.DKIM .Row.PolicyEvaluated.SPF $dkimDomain $dkimResult (index .AuthResults.SPF 0).Domain (index .AuthResults.SPF 0).Result .Row.SourceHostname }} + {{- else }} + {{- $rec := . }} + {{- range $i, $res := .AuthResults.DKIM }} + {{- $dkimDomain := $res.Domain }} + {{- $dkimResult := $res.Result }} + {{- if eq $i 0 }} +{{ printf "%2v%15v | %4v | %10v | %8v | %8v | %16v | %9v | %16v | %8v | %v" $prefix $rec.Row.SourceIP $rec.Row.Count $rec.Row.PolicyEvaluated.Disposition $rec.Row.PolicyEvaluated.DKIM $rec.Row.PolicyEvaluated.SPF $dkimDomain $dkimResult (index $rec.AuthResults.SPF 0).Domain (index $rec.AuthResults.SPF 0).Result $rec.Row.SourceHostname }} + {{- else }} +{{ printf "%2v%15v | %4v | %10v | %8v | %8v | %16v | %9v | %16v | %8v | %v" "" "" "" "" "" "" $dkimDomain $dkimResult "" "" "" }} + {{- end }} + {{- end }} + {{- end }} {{- end }} {{ printf "Total: %v, passed %v, failed %v" .MessagesStats.All .MessagesStats.Passed .MessagesStats.Failed }} From 8cb08f0183847944e3996d95ecf97d9cc8dee872 Mon Sep 17 00:00:00 2001 From: Cameron Moore Date: Sat, 13 Apr 2024 18:26:19 -0500 Subject: [PATCH 14/19] Add tests for template variables --- cmd/dmarc-report-converter/output_test.go | 130 ++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 cmd/dmarc-report-converter/output_test.go diff --git a/cmd/dmarc-report-converter/output_test.go b/cmd/dmarc-report-converter/output_test.go new file mode 100644 index 0000000..27b9d04 --- /dev/null +++ b/cmd/dmarc-report-converter/output_test.go @@ -0,0 +1,130 @@ +package main + +import ( + "bytes" + "html/template" + "testing" + "time" + + "github.com/tierpod/dmarc-report-converter/pkg/dmarc" +) + +func TestExternalTemplate(t *testing.T) { + r := ` + + + Org 1 + foo@bar.baz + 1712279633.907274 + + 1712188800 + 1712275199 + + + + report.test + r + r +

none

+ 100 +
+ + + 1.2.3.4 + 1 + + none + pass + fail + + + + headerfrom.test + + + + auth.test + 1000073432 + pass + + + cust.test + 2020263919 + pass + + + spf.test + pass + + + +
+` + + report, err := dmarc.Parse([]byte(r), false) + if err != nil { + t.Errorf("unexpected error parsing XML: %s", err) + } + + tmpl := `AssetsPath: {{ .AssetsPath }} +# Report +XMLName: {{ .Report.XMLName.Local }} +ReportMetadata: {{ .Report.ReportMetadata }} +PolicyPublished: {{ .Report.PolicyPublished }} +## Records +{{- range .Report.Records }} +- {{ . }} +{{ end -}} +## MessagesStats +{{ .Report.MessagesStats }} + +// Deprecated +XMLName: {{ .XMLName.Local }} +ReportMetadata: {{ .ReportMetadata }} +PolicyPublished: {{ .PolicyPublished }} +{{ .MessagesStats }} +` + + conf := config{ + Output: Output{ + AssetsPath: "/foo", + template: template.Must(template.New("report").Funcs( + template.FuncMap{ + "now": func(fmt string) string { + return time.Now().Format(fmt) + }, + }, + ).Parse(tmpl)), + }, + } + + var buf bytes.Buffer + out := newOutput(&conf) + out.w = &buf + + err = out.template(report) + if err != nil { + t.Errorf("unexpected error building template: %s", err) + } + + expect := `AssetsPath: /foo +# Report +XMLName: feedback +ReportMetadata: {Org 1 foo@bar.baz 1712279633.907274 {2024-04-03 19:00:00 -0500 CDT 2024-04-04 18:59:59 -0500 CDT}} +PolicyPublished: {report.test r r none 100} +## Records +- {{1.2.3.4 1 {none pass fail} } {headerfrom.test } {{cust.test pass 2020263919} {spf.test pass }}} +## MessagesStats +{1 0 1 100} + +// Deprecated +XMLName: feedback +ReportMetadata: {Org 1 foo@bar.baz 1712279633.907274 {2024-04-03 19:00:00 -0500 CDT 2024-04-04 18:59:59 -0500 CDT}} +PolicyPublished: {report.test r r none 100} +{1 0 1 100} +` + + if buf.String() != expect { + t.Errorf("Oops!\nWANT:\n%s\nGOT:\n%s", expect, buf.String()) + } +} From 3c09e05dbd2bb029c4a13156d23b3ceffaae33fb Mon Sep 17 00:00:00 2001 From: Cameron Moore Date: Sun, 14 Apr 2024 13:51:41 -0500 Subject: [PATCH 15/19] Update tests Update tests since the data model changed after PR #48 was merged. --- cmd/dmarc-report-converter/output_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/dmarc-report-converter/output_test.go b/cmd/dmarc-report-converter/output_test.go index 27b9d04..eae26f2 100644 --- a/cmd/dmarc-report-converter/output_test.go +++ b/cmd/dmarc-report-converter/output_test.go @@ -61,7 +61,7 @@ func TestExternalTemplate(t *testing.T) { ` - report, err := dmarc.Parse([]byte(r), false) + report, err := dmarc.Parse([]byte(r), false, 1) if err != nil { t.Errorf("unexpected error parsing XML: %s", err) } @@ -113,7 +113,7 @@ XMLName: feedback ReportMetadata: {Org 1 foo@bar.baz 1712279633.907274 {2024-04-03 19:00:00 -0500 CDT 2024-04-04 18:59:59 -0500 CDT}} PolicyPublished: {report.test r r none 100} ## Records -- {{1.2.3.4 1 {none pass fail} } {headerfrom.test } {{cust.test pass 2020263919} {spf.test pass }}} +- {{1.2.3.4 1 {none pass fail} } {headerfrom.test } {[{auth.test pass 1000073432} {cust.test pass 2020263919}] [{spf.test pass }]}} ## MessagesStats {1 0 1 100} From 287e7371c83b0f10bc4ec9fa5b3b28a2d0029af1 Mon Sep 17 00:00:00 2001 From: Cameron Moore Date: Sun, 14 Apr 2024 21:37:05 -0500 Subject: [PATCH 16/19] Ignore eml files when processing extracted reports Fixes #54 --- cmd/dmarc-report-converter/files.go | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/cmd/dmarc-report-converter/files.go b/cmd/dmarc-report-converter/files.go index 612f2a4..920f528 100644 --- a/cmd/dmarc-report-converter/files.go +++ b/cmd/dmarc-report-converter/files.go @@ -1,6 +1,7 @@ package main import ( + "io/fs" "log" "os" "path/filepath" @@ -85,7 +86,24 @@ func (c *filesConverter) find() error { } } - files, err := filepath.Glob(filepath.Join(c.cfg.Input.Dir, "*.*")) + // Walk Input.Dir for a list of files to process, skipping eml files. + var files []string + err = filepath.Walk(c.cfg.Input.Dir, func(path string, info fs.FileInfo, err error) error { + if err != nil { + return err + } + + if info.IsDir() { + // Process the root dir but don't recurse + if path != c.cfg.Input.Dir { + return filepath.SkipDir + } + } else if filepath.Ext(path) != ".eml" { + files = append(files, path) + } + + return nil + }) if err != nil { return err } From bb0d628366fd2a9438afd17c34d5fa6383bbe463 Mon Sep 17 00:00:00 2001 From: Pavel Podkorytov Date: Mon, 15 Apr 2024 20:58:31 +0500 Subject: [PATCH 17/19] Run workflow on PR --- .github/workflows/build.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f82e21b..822bcd8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,5 +1,9 @@ name: Build -on: [push] +on: + push: + pull_request: + paths-ignore: + - '**.md' jobs: From fd080149d56363db21c15073db5d89924e336201 Mon Sep 17 00:00:00 2001 From: Pavel Podkorytov Date: Mon, 15 Apr 2024 21:12:42 +0500 Subject: [PATCH 18/19] Prevent workflow jobs run twice --- .github/workflows/build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 822bcd8..9d71eca 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,6 +10,10 @@ jobs: build: name: Build runs-on: ubuntu-latest + # https://github.com/orgs/community/discussions/57827 + if: github.event_name != 'pull_request' || + github.event.pull_request.head.repo.full_name != + github.event.pull_request.base.repo.full_name steps: From 068948cc015be814ef7d8c340f8b90c7efe452a4 Mon Sep 17 00:00:00 2001 From: Cameron Moore Date: Mon, 15 Apr 2024 20:40:31 -0500 Subject: [PATCH 19/19] Refactor output tests and fix timezone bug --- cmd/dmarc-report-converter/output_test.go | 138 +++++++++++++--------- 1 file changed, 83 insertions(+), 55 deletions(-) diff --git a/cmd/dmarc-report-converter/output_test.go b/cmd/dmarc-report-converter/output_test.go index eae26f2..66cb5c4 100644 --- a/cmd/dmarc-report-converter/output_test.go +++ b/cmd/dmarc-report-converter/output_test.go @@ -61,70 +61,98 @@ func TestExternalTemplate(t *testing.T) { ` - report, err := dmarc.Parse([]byte(r), false, 1) - if err != nil { - t.Errorf("unexpected error parsing XML: %s", err) - } - - tmpl := `AssetsPath: {{ .AssetsPath }} -# Report -XMLName: {{ .Report.XMLName.Local }} -ReportMetadata: {{ .Report.ReportMetadata }} -PolicyPublished: {{ .Report.PolicyPublished }} -## Records -{{- range .Report.Records }} -- {{ . }} -{{ end -}} -## MessagesStats -{{ .Report.MessagesStats }} - -// Deprecated -XMLName: {{ .XMLName.Local }} -ReportMetadata: {{ .ReportMetadata }} -PolicyPublished: {{ .PolicyPublished }} -{{ .MessagesStats }} -` + tests := map[string]struct { + tmpl string + expect string + }{ + ".AssetsPath": { + `{{ .AssetsPath }}`, + `/foo`, + }, + ".Report.XMLName.Local": { + `{{ .Report.XMLName.Local }}`, + `feedback`, + }, + ".Report.ReportMetadata": { + `{{ .Report.ReportMetadata }}`, + `{Org 1 foo@bar.baz 1712279633.907274 {2024-04-04 00:00:00 +0000 UTC 2024-04-04 23:59:59 +0000 UTC}}`, + }, + ".Report.PolicyPublished": { + `{{ .Report.PolicyPublished }}`, + `{report.test r r none 100}`, + }, + ".Report.Records": { + "{{ range .Report.Records }}\n- {{ . }}\n{{ end -}}", + "\n- {{1.2.3.4 1 {none pass fail} } {headerfrom.test } {[{auth.test pass 1000073432} {cust.test pass 2020263919}] [{spf.test pass }]}}\n", + }, + ".Report.MessagesStats": { + `{{ .Report.MessagesStats }}`, + `{1 0 1 100}`, + }, - conf := config{ - Output: Output{ - AssetsPath: "/foo", - template: template.Must(template.New("report").Funcs( - template.FuncMap{ - "now": func(fmt string) string { - return time.Now().Format(fmt) - }, - }, - ).Parse(tmpl)), + // Deprecated + ".XMLName.Local": { + `{{ .XMLName.Local }}`, + `feedback`, + }, + ".ReportMetadata": { + `{{ .ReportMetadata }}`, + `{Org 1 foo@bar.baz 1712279633.907274 {2024-04-04 00:00:00 +0000 UTC 2024-04-04 23:59:59 +0000 UTC}}`, + }, + ".PolicyPublished": { + `{{ .PolicyPublished }}`, + `{report.test r r none 100}`, + }, + ".MessagesStats": { + `{{ .MessagesStats }}`, + `{1 0 1 100}`, }, } - var buf bytes.Buffer - out := newOutput(&conf) - out.w = &buf + // Set the timezone so that timestamps match regardless of local system time + origLocal := time.Local + + loc, err := time.LoadLocation("UTC") + if err != nil { + t.Errorf("unable to load UTC timezone: %s", err) + } + time.Local = loc + defer func() { + // Reset timezone + time.Local = origLocal + }() - err = out.template(report) + report, err := dmarc.Parse([]byte(r), false, 1) if err != nil { - t.Errorf("unexpected error building template: %s", err) + t.Fatalf("unexpected error parsing XML: %s", err) } - expect := `AssetsPath: /foo -# Report -XMLName: feedback -ReportMetadata: {Org 1 foo@bar.baz 1712279633.907274 {2024-04-03 19:00:00 -0500 CDT 2024-04-04 18:59:59 -0500 CDT}} -PolicyPublished: {report.test r r none 100} -## Records -- {{1.2.3.4 1 {none pass fail} } {headerfrom.test } {[{auth.test pass 1000073432} {cust.test pass 2020263919}] [{spf.test pass }]}} -## MessagesStats -{1 0 1 100} + for name, test := range tests { + conf := config{ + Output: Output{ + AssetsPath: "/foo", + template: template.Must(template.New("report").Funcs( + template.FuncMap{ + "now": func(fmt string) string { + return time.Now().Format(fmt) + }, + }, + ).Parse(test.tmpl)), + }, + } + + var buf bytes.Buffer + out := newOutput(&conf) + out.w = &buf -// Deprecated -XMLName: feedback -ReportMetadata: {Org 1 foo@bar.baz 1712279633.907274 {2024-04-03 19:00:00 -0500 CDT 2024-04-04 18:59:59 -0500 CDT}} -PolicyPublished: {report.test r r none 100} -{1 0 1 100} -` + err = out.template(report) + if err != nil { + t.Fatalf("%s: unexpected error building template: %s", name, err) + } + + if buf.String() != test.expect { + t.Errorf("%s\nWANT:\n%s\nGOT:\n%s", name, test.expect, buf.String()) + } - if buf.String() != expect { - t.Errorf("Oops!\nWANT:\n%s\nGOT:\n%s", expect, buf.String()) } }