ronantakizawa/codereview-bench huggingface dataset:
Repository: caddyserver/caddy
File: caddytest/caddytest.go (Go)

Code before:
	Client       *http.Client
	configLoaded bool
	t            testing.TB
	config       Config
}

// NewTester will create a new testing client with an attached cookie jar
func NewTester(t testing.TB) *Tester {
	jar, err := cookiejar.New(nil)
	if err != nil {
		t.Fatalf("failed to create cookiejar: %s", err)
	}

	return &Tester{
		Client: &http.Client{
			Transport: CreateTestingTransport(),
			Jar:       jar,
			Timeout:   Default.TestRequestTimeout,
		},
		configLoaded: false,
		t:            t,
		config:       Default,
	}
}

func (tc *Tester) WithDefaultOverrides(config Config) *Tester {
	if config.AdminPort != 0 {
		tc.config.AdminPort = config.AdminPort
	}
	if config.Certificates != nil {
		tc.config.Certificates = config.Certificates
	}
	if config.TestRequestTimeout != 0 {
		tc.config.TestRequestTimeout = config.TestRequestTimeout
		tc.Client.Timeout = config.TestRequestTimeout
	}
	if config.LoadRequestTimeout != 0 {
		tc.config.LoadRequestTimeout = config.LoadRequestTimeout
	}

	return tc
}

type configLoadError struct {
	Response string
}

func (e configLoadError) Error() string { return e.Response }

func timeElapsed(start time.Time, name string) {
	elapsed := time.Since(start)

Review comment (suggestion):
Can you add a godoc comment on this? And maybe rename `config Config` to `overrides Config` or something. :+1:

Code after:
	Client       *http.Client
	configLoaded bool
	t            testing.TB
	config       Config
}

// NewTester will create a new testing client with an attached cookie jar
func NewTester(t testing.TB) *Tester {
	jar, err := cookiejar.New(nil)
	if err != nil {
		t.Fatalf("failed to create cookiejar: %s", err)
	}

	return &Tester{
		Client: &http.Client{
			Transport: CreateTestingTransport(),
			Jar:       jar,
			Timeout:   Default.TestRequestTimeout,
		},
		configLoaded: false,
		t:            t,
		config:       Default,
	}
}

// WithDefaultOverrides this will override the default test configuration with the provided values.
func (tc *Tester) WithDefaultOverrides(overrides Config) *Tester {
	if overrides.AdminPort != 0 {
		tc.config.AdminPort = overrides.AdminPort
	}
	if len(overrides.Certificates) > 0 {
		tc.config.Certificates = overrides.Certificates
	}
	if overrides.TestRequestTimeout != 0 {
		tc.config.TestRequestTimeout = overrides.TestRequestTimeout
		tc.Client.Timeout = overrides.TestRequestTimeout
	}
	if overrides.LoadRequestTimeout != 0 {
		tc.config.LoadRequestTimeout = overrides.LoadRequestTimeout
	}

	return tc
}

type configLoadError struct {
	Response string
}

func (e configLoadError) Error() string { return e.Response }

func timeElapsed(start time.Time, name string) {
Repository: caddyserver/caddy
File: modules/caddytls/certselection.go (Go)

Code before:
				}
			}
			if !found {
				continue
			}
		}

		if len(p.SubjectOrganization) > 0 {
			found := slices.ContainsFunc(p.SubjectOrganization, func(s string) bool {
				return slices.Contains(cert.Leaf.Subject.Organization, s)
			})
			if !found {
				continue
			}
		}

		if p.PublicKeyAlgorithm != PublicKeyAlgorithm(x509.UnknownPublicKeyAlgorithm) &&
			PublicKeyAlgorithm(cert.Leaf.PublicKeyAlgorithm) != p.PublicKeyAlgorithm {
			continue
		}

		if len(p.AnyTag) > 0 {
			var found bool
			if slices.ContainsFunc(p.AnyTag, cert.HasTag) {
				found = true
			}
			if !found {
				continue
			}
		}

		if len(p.AllTags) > 0 {
			for _, tag := range p.AllTags {
				if !cert.HasTag(tag) {
					continue nextChoice
				}
			}
		}

		// this certificate at least meets the policy's requirements,
		// but we still have to check expiration and compatibility
		viable = append(viable, cert)
	}

	if len(viable) == 0 {
		return certmagic.Certificate{}, fmt.Errorf("no certificates matched custom selection policy")
	}

	return certmagic.DefaultCertificateSelector(hello, viable)
}


Review comment (suggestion):
```suggestion
			found = slices.ContainsFunc(p.AnyTag, cert.HasTag)
```

Code after:
				}
			}
			if !found {
				continue
			}
		}

		if len(p.SubjectOrganization) > 0 {
			found := slices.ContainsFunc(p.SubjectOrganization, func(s string) bool {
				return slices.Contains(cert.Leaf.Subject.Organization, s)
			})
			if !found {
				continue
			}
		}

		if p.PublicKeyAlgorithm != PublicKeyAlgorithm(x509.UnknownPublicKeyAlgorithm) &&
			PublicKeyAlgorithm(cert.Leaf.PublicKeyAlgorithm) != p.PublicKeyAlgorithm {
			continue
		}

		if len(p.AnyTag) > 0 {
			found := slices.ContainsFunc(p.AnyTag, cert.HasTag)
			if !found {
				continue
			}
		}

		if len(p.AllTags) > 0 {
			for _, tag := range p.AllTags {
				if !cert.HasTag(tag) {
					continue nextChoice
				}
			}
		}

		// this certificate at least meets the policy's requirements,
		// but we still have to check expiration and compatibility
		viable = append(viable, cert)
	}

	if len(viable) == 0 {
		return certmagic.Certificate{}, fmt.Errorf("no certificates matched custom selection policy")
	}

	return certmagic.DefaultCertificateSelector(hello, viable)
}

// UnmarshalCaddyfile sets up the CustomCertSelectionPolicy from Caddyfile tokens. Syntax:
//
//	cert_selection {
Repository: caddyserver/caddy
File: modules/caddyhttp/caddyauth/hashes.go (Go)

Code before:
	cost int
}

// CaddyModule returns the Caddy module information.
func (BcryptHash) CaddyModule() caddy.ModuleInfo {
	return caddy.ModuleInfo{
		ID:  "http.authentication.hashes.bcrypt",
		New: func() caddy.Module { return new(BcryptHash) },
	}
}

// Compare compares passwords.
func (BcryptHash) Compare(hashed, plaintext []byte) (bool, error) {
	err := bcrypt.CompareHashAndPassword(hashed, plaintext)
	if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
		return false, nil
	}
	if err != nil {
		return false, err
	}
	return true, nil
}

// Hash hashes plaintext using a random salt.
func (b BcryptHash) Hash(plaintext []byte) ([]byte, error) {
	return bcrypt.GenerateFromPassword(plaintext, b.cost)
}

// FakeHash returns a fake hash.
func (BcryptHash) FakeHash() []byte {
	// hashed with the following command:
	// caddy hash-password --plaintext "antitiming" --algorithm "bcrypt"
	return []byte("$2a$14$X3ulqf/iGxnf1k6oMZ.RZeJUoqI9PX2PM4rS5lkIKJXduLGXGPrt6")
}

// Interface guards
var (
	_ Comparer = (*BcryptHash)(nil)
	_ Hasher   = (*BcryptHash)(nil)
)


Review comment (suggestion):
Since `cost` of zero is never correct, this should have this:
```go
cost := b.cost
if cost == 0 {
	cost = defaultBcryptCost
}
```
and then make a `const defaultBcryptCost = 14` which is then also used in the command setup.

Code after:
// BcryptHash implements the bcrypt hash.
type BcryptHash struct {
	// cost is the bcrypt hashing difficulty factor (work factor).
	// Higher values increase computation time and security.
	cost int
}

// CaddyModule returns the Caddy module information.
func (BcryptHash) CaddyModule() caddy.ModuleInfo {
	return caddy.ModuleInfo{
		ID:  "http.authentication.hashes.bcrypt",
		New: func() caddy.Module { return new(BcryptHash) },
	}
}

// Compare compares passwords.
func (BcryptHash) Compare(hashed, plaintext []byte) (bool, error) {
	err := bcrypt.CompareHashAndPassword(hashed, plaintext)
	if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
		return false, nil
	}
	if err != nil {
		return false, err
	}
	return true, nil
}

// Hash hashes plaintext using a random salt.
func (b BcryptHash) Hash(plaintext []byte) ([]byte, error) {
	cost := b.cost
	if cost < bcrypt.MinCost || cost > bcrypt.MaxCost {
		cost = defaultBcryptCost
	}

	return bcrypt.GenerateFromPassword(plaintext, cost)
}

// FakeHash returns a fake hash.
func (BcryptHash) FakeHash() []byte {
	// hashed with the following command:
	// caddy hash-password --plaintext "antitiming" --algorithm "bcrypt"
	return []byte("$2a$14$X3ulqf/iGxnf1k6oMZ.RZeJUoqI9PX2PM4rS5lkIKJXduLGXGPrt6")
}

// Interface guards
var (
	_ Comparer = (*BcryptHash)(nil)
	_ Hasher   = (*BcryptHash)(nil)
)
Repository: caddyserver/caddy
File: modules/caddyhttp/caddyauth/hashes.go (Go)

Code before:
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package caddyauth

import (
	"errors"

	"github.com/caddyserver/caddy/v2"
	"golang.org/x/crypto/bcrypt"
)

func init() {
	caddy.RegisterModule(BcryptHash{})
}

// defaultBcryptCost cost 14 strikes a solid balance between security, usability, and hardware performance
const defaultBcryptCost = 14

// BcryptHash implements the bcrypt hash.
type BcryptHash struct {
	// cost is the bcrypt hashing difficulty factor (work factor).
	// Higher values increase computation time and security.
	cost int
}

// CaddyModule returns the Caddy module information.
func (BcryptHash) CaddyModule() caddy.ModuleInfo {
	return caddy.ModuleInfo{
		ID:  "http.authentication.hashes.bcrypt",
		New: func() caddy.Module { return new(BcryptHash) },
	}
}

// Compare compares passwords.
func (BcryptHash) Compare(hashed, plaintext []byte) (bool, error) {
	err := bcrypt.CompareHashAndPassword(hashed, plaintext)
	if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
		return false, nil
	}
	if err != nil {
		return false, err
	}
	return true, nil
}

// Hash hashes plaintext using a random salt.
func (b BcryptHash) Hash(plaintext []byte) ([]byte, error) {
	cost := b.cost
	if cost < bcrypt.MinCost || cost > bcrypt.MaxCost {

Review comment (suggestion):
Configurable parts of module should be exported. If there's assumption of default value, we usually implement those checks in `Provision` method, which I believe at this point it's needed.

Code after:
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package caddyauth

import (
	"errors"

	"golang.org/x/crypto/bcrypt"

	"github.com/caddyserver/caddy/v2"
)

func init() {
	caddy.RegisterModule(BcryptHash{})
}

// defaultBcryptCost cost 14 strikes a solid balance between security, usability, and hardware performance
const defaultBcryptCost = 14

// BcryptHash implements the bcrypt hash.
type BcryptHash struct {
	// cost is the bcrypt hashing difficulty factor (work factor).
	// Higher values increase computation time and security.
	cost int
}

// CaddyModule returns the Caddy module information.
func (BcryptHash) CaddyModule() caddy.ModuleInfo {
	return caddy.ModuleInfo{
		ID:  "http.authentication.hashes.bcrypt",
		New: func() caddy.Module { return new(BcryptHash) },
	}
}

// Compare compares passwords.
func (BcryptHash) Compare(hashed, plaintext []byte) (bool, error) {
	err := bcrypt.CompareHashAndPassword(hashed, plaintext)
	if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
		return false, nil
	}
	if err != nil {
		return false, err
	}
	return true, nil
}

// Hash hashes plaintext using a random salt.
func (b BcryptHash) Hash(plaintext []byte) ([]byte, error) {
	cost := b.cost
Repository: caddyserver/caddy
File: modules/logging/filewriter.go (Go)

Code before:
// `"0777"` and `"777"` are thus equivalent values.
type fileMode os.FileMode

// UnmarshalJSON satisfies json.Unmarshaler.
func (m *fileMode) UnmarshalJSON(b []byte) error {
	if len(b) == 0 {
		return io.EOF
	}

	var s string
	if err := json.Unmarshal(b, &s); err != nil {
		return err
	}

	mode, err := parseFileMode(s)
	if err != nil {
		return err
	}

	*m = fileMode(mode)
	return err
}

// MarshalJSON satisfies json.Marshaler.
func (m *fileMode) MarshalJSON() ([]byte, error) {
	return fmt.Appendf(nil, "\"%04o\"", *m), nil
}

// parseFileMode parses a file mode string,
// adding support for `chmod` unix command like
// 1 to 4 digital octal values.
func parseFileMode(s string) (os.FileMode, error) {
	modeStr := fmt.Sprintf("%04s", s)
	mode, err := strconv.ParseUint(modeStr, 8, 32)
	if err != nil {
		return 0, err
	}
	return os.FileMode(mode), nil
}

// FileWriter can write logs to files. By default, log files
// are rotated ("rolled") when they get large, and old log
// files get deleted, to ensure that the process does not
// exhaust disk space.
type FileWriter struct {
	// Filename is the name of the file to write.
	Filename string `json:"filename,omitempty"`

	// The file permissions mode.
	// 0600 by default.
	Mode fileMode `json:"mode,omitempty"`

Review comment (suggestion):
This one's weird, using `nil` as the byte slice to append into is _strange_. I think I prefer the old way.

Code after:
// `"0777"` and `"777"` are thus equivalent values.
type fileMode os.FileMode

// UnmarshalJSON satisfies json.Unmarshaler.
func (m *fileMode) UnmarshalJSON(b []byte) error {
	if len(b) == 0 {
		return io.EOF
	}

	var s string
	if err := json.Unmarshal(b, &s); err != nil {
		return err
	}

	mode, err := parseFileMode(s)
	if err != nil {
		return err
	}

	*m = fileMode(mode)
	return err
}

// MarshalJSON satisfies json.Marshaler.
func (m *fileMode) MarshalJSON() ([]byte, error) {
	return []byte(fmt.Sprintf("\"%04o\"", *m)), nil
}

// parseFileMode parses a file mode string,
// adding support for `chmod` unix command like
// 1 to 4 digital octal values.
func parseFileMode(s string) (os.FileMode, error) {
	modeStr := fmt.Sprintf("%04s", s)
	mode, err := strconv.ParseUint(modeStr, 8, 32)
	if err != nil {
		return 0, err
	}
	return os.FileMode(mode), nil
}

// FileWriter can write logs to files. By default, log files
// are rotated ("rolled") when they get large, and old log
// files get deleted, to ensure that the process does not
// exhaust disk space.
type FileWriter struct {
	// Filename is the name of the file to write.
	Filename string `json:"filename,omitempty"`

	// The file permissions mode.
	// 0600 by default.
	Mode fileMode `json:"mode,omitempty"`
Repository: caddyserver/caddy
File: context.go (Go)

Code before:
	if ctx.cfg == nil {
		// often the case in tests; just use a dev logger
		l, err := zap.NewDevelopment()
		if err != nil {
			panic("config missing, unable to create dev logger: " + err.Error())
		}
		return l
	}
	mod := ctx.Module()
	if len(module) > 0 {
		mod = module[0]
	}
	if mod == nil {
		return Log()
	}
	return ctx.cfg.Logging.Logger(mod)
}

// Slogger returns a slog logger that is intended for use by
// the most recent module associated with the context.
func (ctx Context) Slogger() *slog.Logger {
	if ctx.cfg == nil {
		// often the case in tests; just use a dev logger
		l, err := zap.NewDevelopment()
		if err != nil {
			panic("config missing, unable to create dev logger: " + err.Error())
		}
		return slog.New(zapslog.NewHandler(l.Core()))
	}
	mod := ctx.Module()
	if mod == nil {
		return slog.New(zapslog.NewHandler(Log().Core()))
	}
	return slog.New(zapslog.NewHandler(ctx.cfg.Logging.Logger(mod).Core(),
		zapslog.WithName(string(mod.CaddyModule().ID)),
	))
}

// Modules returns the lineage of modules that this context provisioned,
// with the most recent/current module being last in the list.
func (ctx Context) Modules() []Module {
	mods := make([]Module, len(ctx.ancestry))
	copy(mods, ctx.ancestry)
	return mods
}

// Module returns the current module, or the most recent one
// provisioned by the context.
func (ctx Context) Module() Module {
	if len(ctx.ancestry) == 0 {
		return nil

Review comment (suggestion):
I know this is an annoying request, but how would you feel if we drop the "factory" thing? 😆 I just... it feels like Java.

What about like `slogHandlerWrapper` instead? Or, maybe even just `slogHandler`? I am not 100% sure since I haven't dived into slog too much yet to be that familiar with it.

Code after:
		panic("more than 1 module passed in")
	}
	if ctx.cfg == nil {
		// often the case in tests; just use a dev logger
		l, err := zap.NewDevelopment()
		if err != nil {
			panic("config missing, unable to create dev logger: " + err.Error())
		}
		return l
	}
	mod := ctx.Module()
	if len(module) > 0 {
		mod = module[0]
	}
	if mod == nil {
		return Log()
	}
	return ctx.cfg.Logging.Logger(mod)
}

type slogHandlerFactory func(handler slog.Handler, core zapcore.Core, moduleID string) slog.Handler

var (
	slogHandlerFactories   []slogHandlerFactory
	slogHandlerFactoriesMu sync.RWMutex
)

// RegisterSlogHandlerFactory allows modules to register custom log/slog.Handler,
// for instance, to add contextual data to the logs.
func RegisterSlogHandlerFactory(factory slogHandlerFactory) {
	slogHandlerFactoriesMu.Lock()
	slogHandlerFactories = append(slogHandlerFactories, factory)
	slogHandlerFactoriesMu.Unlock()
}

// Slogger returns a slog logger that is intended for use by
// the most recent module associated with the context.
func (ctx Context) Slogger() *slog.Logger {
	var (
		handler  slog.Handler
		core     zapcore.Core
		moduleID string
	)
	if ctx.cfg == nil {
		// often the case in tests; just use a dev logger
		l, err := zap.NewDevelopment()
		if err != nil {
			panic("config missing, unable to create dev logger: " + err.Error())
		}

		core = l.Core()
Repository: caddyserver/caddy
File: modules/caddyhttp/routes.go (Go)

Code before:
		for _, matcher := range matcherSetIfaces {
			if m, ok := matcher.(RequestMatcherWithError); ok {
				matcherSet = append(matcherSet, m)
				continue
			}
			if m, ok := matcher.(RequestMatcher); ok {
				matcherSet = append(matcherSet, m)
				continue
			}
			return fmt.Errorf("decoded module is not a RequestMatcher or RequestMatcherWithError: %#v", matcher)
		}
		*ms = append(*ms, matcherSet)
	}
	return nil
}

// TODO: Is this used?
func (ms MatcherSets) String() string {
	var result strings.Builder
	result.WriteString("[")
	for _, matcherSet := range ms {
		for _, matcher := range matcherSet {
			result.WriteString(fmt.Sprintf(" %#v", matcher))
		}
	}
	return result.String() + " ]"
}

var routeGroupCtxKey = caddy.CtxKey("route_group")


Review comment (suggestion):
```suggestion
    result.WriteByte(']')

	return result.String()
```

Code after:
		var matcherSet MatcherSet
		for _, matcher := range matcherSetIfaces {
			if m, ok := matcher.(RequestMatcherWithError); ok {
				matcherSet = append(matcherSet, m)
				continue
			}
			if m, ok := matcher.(RequestMatcher); ok {
				matcherSet = append(matcherSet, m)
				continue
			}
			return fmt.Errorf("decoded module is not a RequestMatcher or RequestMatcherWithError: %#v", matcher)
		}
		*ms = append(*ms, matcherSet)
	}
	return nil
}

// TODO: Is this used?
func (ms MatcherSets) String() string {
	var result strings.Builder
	result.WriteByte('[')
	for _, matcherSet := range ms {
		for _, matcher := range matcherSet {
			result.WriteString(fmt.Sprintf(" %#v", matcher))
		}
	}
	result.WriteByte(']')
	return result.String()
}

var routeGroupCtxKey = caddy.CtxKey("route_group")
Repository: caddyserver/caddy
File: modules/logging/filters.go (Go)

Code before:
// Filter filters the input field.
func (m IPMaskFilter) Filter(in zapcore.Field) zapcore.Field {
	if array, ok := in.Interface.(caddyhttp.LoggableStringArray); ok {
		newArray := make(caddyhttp.LoggableStringArray, len(array))
		for i, s := range array {
			newArray[i] = m.mask(s)
		}
		in.Interface = newArray
	} else {
		in.String = m.mask(in.String)
	}

	return in
}

func (m IPMaskFilter) mask(s string) string {
	var output strings.Builder
	for value := range strings.SplitSeq(s, ",") {
		value = strings.TrimSpace(value)
		host, port, err := net.SplitHostPort(value)
		if err != nil {
			host = value // assume whole thing was IP address
		}
		ipAddr := net.ParseIP(host)
		if ipAddr == nil {
			output.WriteString(value + ", ")
			continue
		}
		mask := m.v4Mask
		if ipAddr.To4() == nil {
			mask = m.v6Mask
		}
		masked := ipAddr.Mask(mask)
		if port == "" {
			output.WriteString(masked.String() + ", ")
			continue
		}

		output.WriteString(net.JoinHostPort(masked.String(), port) + ", ")
	}
	return strings.TrimSuffix(output.String(), ", ")
}

type filterAction string

const (
	// Replace value(s).
	replaceAction filterAction = "replace"

	// Hash value(s).
	hashAction filterAction = "hash"

Review comment (suggestion):
```suggestion
			output.WriteString(value)
			output.WriteString(", ")
```

Code after:
// Filter filters the input field.
func (m IPMaskFilter) Filter(in zapcore.Field) zapcore.Field {
	if array, ok := in.Interface.(caddyhttp.LoggableStringArray); ok {
		newArray := make(caddyhttp.LoggableStringArray, len(array))
		for i, s := range array {
			newArray[i] = m.mask(s)
		}
		in.Interface = newArray
	} else {
		in.String = m.mask(in.String)
	}

	return in
}

func (m IPMaskFilter) mask(s string) string {
	parts := make([]string, 0)
	for value := range strings.SplitSeq(s, ",") {
		value = strings.TrimSpace(value)
		host, port, err := net.SplitHostPort(value)
		if err != nil {
			host = value // assume whole thing was IP address
		}
		ipAddr := net.ParseIP(host)
		if ipAddr == nil {
			parts = append(parts, value)
			continue
		}
		mask := m.v4Mask
		if ipAddr.To4() == nil {
			mask = m.v6Mask
		}
		masked := ipAddr.Mask(mask)
		if port == "" {
			parts = append(parts, masked.String())
			continue
		}

		parts = append(parts, net.JoinHostPort(masked.String(), port))
	}
	return strings.Join(parts, ", ")
}

type filterAction string

const (
	// Replace value(s).
	replaceAction filterAction = "replace"

	// Hash value(s).
	hashAction filterAction = "hash"
Repository: caddyserver/caddy
File: modules/logging/filters.go (Go)

Code before:
		in.String = m.mask(in.String)
	}

	return in
}

func (m IPMaskFilter) mask(s string) string {
	var output strings.Builder
	for value := range strings.SplitSeq(s, ",") {
		value = strings.TrimSpace(value)
		host, port, err := net.SplitHostPort(value)
		if err != nil {
			host = value // assume whole thing was IP address
		}
		ipAddr := net.ParseIP(host)
		if ipAddr == nil {
			output.WriteString(value + ", ")
			continue
		}
		mask := m.v4Mask
		if ipAddr.To4() == nil {
			mask = m.v6Mask
		}
		masked := ipAddr.Mask(mask)
		if port == "" {
			output.WriteString(masked.String() + ", ")
			continue
		}

		output.WriteString(net.JoinHostPort(masked.String(), port) + ", ")
	}
	return strings.TrimSuffix(output.String(), ", ")
}

type filterAction string

const (
	// Replace value(s).
	replaceAction filterAction = "replace"

	// Hash value(s).
	hashAction filterAction = "hash"

	// Delete.
	deleteAction filterAction = "delete"
)

func (a filterAction) IsValid() error {
	switch a {
	case replaceAction, deleteAction, hashAction:
		return nil

Review comment (suggestion):
```suggestion
			output.WriteString(masked.String())
			output.WriteString(", ")

```

Code after:
		in.String = m.mask(in.String)
	}

	return in
}

func (m IPMaskFilter) mask(s string) string {
	parts := make([]string, 0)
	for value := range strings.SplitSeq(s, ",") {
		value = strings.TrimSpace(value)
		host, port, err := net.SplitHostPort(value)
		if err != nil {
			host = value // assume whole thing was IP address
		}
		ipAddr := net.ParseIP(host)
		if ipAddr == nil {
			parts = append(parts, value)
			continue
		}
		mask := m.v4Mask
		if ipAddr.To4() == nil {
			mask = m.v6Mask
		}
		masked := ipAddr.Mask(mask)
		if port == "" {
			parts = append(parts, masked.String())
			continue
		}

		parts = append(parts, net.JoinHostPort(masked.String(), port))
	}
	return strings.Join(parts, ", ")
}

type filterAction string

const (
	// Replace value(s).
	replaceAction filterAction = "replace"

	// Hash value(s).
	hashAction filterAction = "hash"

	// Delete.
	deleteAction filterAction = "delete"
)

func (a filterAction) IsValid() error {
	switch a {
	case replaceAction, deleteAction, hashAction:
		return nil