diff --git a/caddytest/integration/caddyfile_adapt/encode_options.caddyfiletest b/caddytest/integration/caddyfile_adapt/encode_options.caddyfiletest index ea9038ef8..89e897631 100644 --- a/caddytest/integration/caddyfile_adapt/encode_options.caddyfiletest +++ b/caddytest/integration/caddyfile_adapt/encode_options.caddyfiletest @@ -18,7 +18,9 @@ encode gzip zstd { # Long way with a block for each encoding encode { - zstd + zstd { + disable_checksum + } gzip 5 } @@ -71,7 +73,9 @@ encode "gzip": { "level": 5 }, - "zstd": {} + "zstd": { + "checksum": false + } }, "handler": "encode", "prefer": [ diff --git a/modules/caddyhttp/encode/caddyfile.go b/modules/caddyhttp/encode/caddyfile.go index 8b8657080..04b1a3c77 100644 --- a/modules/caddyhttp/encode/caddyfile.go +++ b/modules/caddyhttp/encode/caddyfile.go @@ -41,7 +41,10 @@ func parseCaddyfile(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) // // encode [] { // gzip [] -// zstd +// zstd [] { +// level +// disable_checksum +// } // minimum_length // # response matcher block // match { diff --git a/modules/caddyhttp/encode/zstd/zstd.go b/modules/caddyhttp/encode/zstd/zstd.go index 1706de89d..ece935974 100644 --- a/modules/caddyhttp/encode/zstd/zstd.go +++ b/modules/caddyhttp/encode/zstd/zstd.go @@ -33,6 +33,10 @@ type Zstd struct { // The compression level. Accepted values: fastest, better, best, default. Level string `json:"level,omitempty"` + // Whether to include the optional 4-byte zstd frame checksum trailer. + // If unset, the upstream zstd library default is preserved. + Checksum *bool `json:"checksum,omitempty"` + // Compression level refer to type constants value from zstd.SpeedFastest to zstd.SpeedBestCompression level zstd.EncoderLevel } @@ -48,19 +52,48 @@ func (Zstd) CaddyModule() caddy.ModuleInfo { // UnmarshalCaddyfile sets up the handler from Caddyfile tokens. func (z *Zstd) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { d.Next() // consume option name - if !d.NextArg() { - return nil + args := d.RemainingArgs() + switch len(args) { + case 0: + case 1: + if _, err := parseEncoderLevel(args[0]); err != nil { + return d.Err(err.Error()) + } + z.Level = args[0] + default: + return d.ArgErr() } - levelStr := d.Val() - if ok, _ := zstd.EncoderLevelFromString(levelStr); !ok { - return d.Errf("unexpected compression level, use one of '%s', '%s', '%s', '%s'", - zstd.SpeedFastest, - zstd.SpeedBetterCompression, - zstd.SpeedBestCompression, - zstd.SpeedDefault, - ) + + for nesting := d.Nesting(); d.NextBlock(nesting); { + switch d.Val() { + case "level": + args := d.RemainingArgs() + if len(args) != 1 { + return d.ArgErr() + } + if z.Level != "" { + return d.Err("compression level already specified") + } + if _, err := parseEncoderLevel(args[0]); err != nil { + return d.Err(err.Error()) + } + z.Level = args[0] + + case "disable_checksum": + if d.NextArg() { + return d.ArgErr() + } + if z.Checksum != nil { + return d.Err("checksum already specified") + } + disabled := false + z.Checksum = &disabled + + default: + return d.Errf("unknown subdirective '%s'", d.Val()) + } } - z.Level = levelStr + return nil } @@ -69,15 +102,11 @@ func (z *Zstd) Provision(ctx caddy.Context) error { if z.Level == "" { z.Level = zstd.SpeedDefault.String() } - var ok bool - if ok, z.level = zstd.EncoderLevelFromString(z.Level); !ok { - return fmt.Errorf("unexpected compression level, use one of '%s', '%s', '%s', '%s'", - zstd.SpeedFastest, - zstd.SpeedDefault, - zstd.SpeedBetterCompression, - zstd.SpeedBestCompression, - ) + level, err := parseEncoderLevel(z.Level) + if err != nil { + return err } + z.level = level return nil } @@ -90,14 +119,45 @@ func (z Zstd) NewEncoder() encode.Encoder { // The default of 8MB for the window is // too large for many clients, so we limit // it to 128K to lighten their load. - writer, _ := zstd.NewWriter( - nil, - zstd.WithWindowSize(128<<10), + writer, _ := zstd.NewWriter(nil, z.writerOptions(128<<10)...) + return writer +} + +func (z Zstd) writerOptions(windowSize int) []zstd.EOption { + opts := []zstd.EOption{ + zstd.WithWindowSize(windowSize), zstd.WithEncoderConcurrency(1), zstd.WithZeroFrames(true), - zstd.WithEncoderLevel(z.level), + zstd.WithEncoderLevel(z.encoderLevel()), + } + if z.Checksum != nil { + opts = append(opts, zstd.WithEncoderCRC(*z.Checksum)) + } + return opts +} + +func (z Zstd) encoderLevel() zstd.EncoderLevel { + if z.level != 0 { + return z.level + } + if z.Level != "" { + if level, err := parseEncoderLevel(z.Level); err == nil { + return level + } + } + return zstd.SpeedDefault +} + +func parseEncoderLevel(level string) (zstd.EncoderLevel, error) { + if ok, encLevel := zstd.EncoderLevelFromString(level); ok { + return encLevel, nil + } + return 0, fmt.Errorf("unexpected compression level, use one of '%s', '%s', '%s', '%s'", + zstd.SpeedFastest, + zstd.SpeedBetterCompression, + zstd.SpeedBestCompression, + zstd.SpeedDefault, ) - return writer } // Interface guards