diff --git a/go.mod b/go.mod index 6538464..0bfba04 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,8 @@ module github.com/ln80/struct-sensitive go 1.22.0 -require github.com/sanity-io/litter v1.5.5 // indirect +require ( + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/sanity-io/litter v1.5.5 // indirect +) diff --git a/go.sum b/go.sum index b5ef41c..91b9d88 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,8 @@ github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= diff --git a/mask.go b/mask.go index 4bc7e44..eedd9ca 100644 --- a/mask.go +++ b/mask.go @@ -1,6 +1,15 @@ package sensitive -import "github.com/ln80/struct-sensitive/mask" +import ( + "encoding/json" + "errors" + "fmt" + "log/slog" + + "github.com/ln80/struct-sensitive/internal/option" + "github.com/ln80/struct-sensitive/mask" + "github.com/mitchellh/copystructure" +) // WithRegisteredMasks returns an option that force redaction using the registered masks, // including the predefined one e.g. `email`, `ipv4_addr`, `credit_card`. @@ -25,5 +34,78 @@ func WithRegisteredMasks(rc *RedactConfig) { // // Use [mask.Register] to register additional masks. func Mask(structPtr any, opts ...func(*RedactConfig)) error { - return Redact(structPtr, append(opts, WithRegisteredMasks)...) + return Redact(structPtr, append([]func(*RedactConfig){WithRegisteredMasks}, opts...)...) +} + +var ( + ErrFailedToMaskCopy = errors.New("failed to mask copy") +) + +// Masked is a wrapper that contains both the original value and masked copy +type Masked[T any] struct { + original T + value T +} + +type MaskedCopyConfig struct { + DeepCopy bool // default false +} + +// NewMaskedCopy returns a new masked copy of the given value. +// It fails if it can't copy the value or the mask config is invalid. +func NewMaskedCopy[T any](v T, opts ...func(*MaskedCopyConfig)) (*Masked[T], error) { + cfg := MaskedCopyConfig{ + DeepCopy: false, + } + option.Apply(&cfg, opts) + + var copy = v + if cfg.DeepCopy { + c, err := copystructure.Copy(v) + if err != nil { + return nil, errors.Join(ErrFailedToMaskCopy, err) + } + copy = c.(T) + } + + if err := Mask(©); err != nil { + return nil, errors.Join(ErrFailedToMaskCopy, err) + } + + return &Masked[T]{value: copy, original: v}, nil +} + +// MaskedCopy returns a masked copy of the given value. +// It panics in case of failure. +func MaskedCopy[T any](v T, opts ...func(*MaskedCopyConfig)) *Masked[T] { + copy, err := NewMaskedCopy(v, opts...) + if err != nil { + panic(err) + } + return copy +} + +// Reveal reveals the original value without applying masks +func (r Masked[T]) Reveal() T { + return r.original +} + +// Value returns the masked copy value +func (r Masked[T]) Value() T { + return r.value +} + +// LogValue implements slog.LogValuer +func (r Masked[T]) LogValue() slog.Value { + return slog.AnyValue(r.value) +} + +// String implements fmt.Stringer +func (r Masked[T]) String() string { + return fmt.Sprintf("%+v", r.value) +} + +// MarshalJSON implements json.Marshaler +func (r Masked[T]) MarshalJSON() ([]byte, error) { + return json.Marshal(r.value) } diff --git a/mask_test.go b/mask_test.go index 877230c..b3a4ed3 100644 --- a/mask_test.go +++ b/mask_test.go @@ -106,3 +106,29 @@ func TestMask(t *testing.T) { }) } } + +func TestMaskedCopy(t *testing.T) { + profile := Profile{ + Email: "email@example.com", + Fullname: "Guadalupe Kemmer DDS", + } + + copy, err := NewMaskedCopy(profile) + if err != nil { + t.Fatal("expect err be nil, got", err) + } + + maskedCopy := copy.Value() + + if reflect.DeepEqual(profile, maskedCopy) { + t.Fatalf("expect not be equals %v, %v", profile, maskedCopy) + } + + if err := Mask(&profile); err != nil { + t.Fatal("expect err be nil, got", err) + } + + if !reflect.DeepEqual(profile, maskedCopy) { + t.Fatalf("expect be equals %v, %v", profile, maskedCopy) + } +} diff --git a/tag.go b/tag.go index 08d0256..d4709e5 100644 --- a/tag.go +++ b/tag.go @@ -42,6 +42,10 @@ func (p TagPayload) Marshal() string { return marshalTag(p) } +func (p TagPayload) String() string { + return p.Marshal() +} + // ParseTag searches for a sensitive tag in the given field's raw tag, // parses it, and returns a representational payload. // It returns nil if the tag is not found or is misconfigured.