Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ For more usage and examples see the [Godoc](http://godoc.org/github.com/ln80/str
### Predefined masks:
- `email`
- `ipv4_addr`
- `credit_card`

## Limitations
1. Only fields of types convertible to `string` or `*string` are supported, although nesting structs directly or through collections (slices and maps) is also supported.
Expand Down
87 changes: 87 additions & 0 deletions mask/credit_card.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package mask

import (
"errors"
"strings"

"github.com/ln80/struct-sensitive/internal/option"
)

type CreditCardConfig struct {
MaskBankIdentifier bool // default false
}

func CreditCard(cardNumber string, opts ...func(*Config[CreditCardConfig])) (string, error) {
cfg := DefaultConfig(CreditCardConfig{
MaskBankIdentifier: false,
})
option.Apply(&cfg, opts)

formatCardNumber := func(cardNumber string, groupings []int) string {
totalLength := len(cardNumber)
formattedLength := totalLength + len(groupings) - 1

result := make([]rune, formattedLength)
pos := 0
index := 0

for _, group := range groupings {
if pos >= totalLength {
break
}

if index > 0 {
result[index] = ' '
index++
}

end := pos + group
if end > totalLength {
end = totalLength
}

for i := pos; i < end; i++ {
result[index] = rune(cardNumber[i])
index++
}

pos += group
}

return string(result)
}

cardNumber = strings.ReplaceAll(cardNumber, " ", "")
length := len(cardNumber)

if length < 15 || length > 16 {
return "", errors.New("unsupported credit card number format")
}

var groupings []int
if length == 15 {
groupings = []int{4, 6, 5} // Typical for American Express
} else {
groupings = []int{4, 4, 4, 4} // Default for standard cards
}

if cfg.Kind.MaskBankIdentifier {
maskUntil := length - groupings[len(groupings)-1]
masked := strings.Repeat(string([]rune{cfg.Symbol}), maskUntil) + cardNumber[maskUntil:]
return formatCardNumber(masked, groupings), nil
}

visibleStart := groupings[0]
visibleEnd := groupings[len(groupings)-1]
if length <= visibleStart+visibleEnd {
return cardNumber, nil
}
masked := cardNumber[:visibleStart] +
strings.Repeat(string([]rune{cfg.Symbol}), length-visibleStart-visibleEnd) +
cardNumber[length-visibleEnd:]
return formatCardNumber(masked, groupings), nil
}

func init() {
Register("credit_card", DefaultMasker(CreditCard))
}
67 changes: 67 additions & 0 deletions mask/credit_card_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package mask_test

import (
"testing"

"github.com/ln80/struct-sensitive/mask"
"github.com/ln80/struct-sensitive/masktest"
)

func TestCreditCard(t *testing.T) {
masktest.Run(t, mask.CreditCard, []masktest.Tc[mask.CreditCardConfig]{
{
Value: "3012",
OK: false,
},
{
Value: "4111 3145 4001 1111",
Want: "4111 **** **** 1111",
OK: true,
},
{
Value: "4111 3145 4001 1111",
Want: "**** **** **** 1111",
OK: true,
Option: func(c *mask.Config[mask.CreditCardConfig]) {
c.Kind.MaskBankIdentifier = true
},
},
{
Value: "3714 496353 98431",
Want: "3714 ****** 98431",
OK: true,
},
{
Value: "371449635398431",
Want: "3714 ****** 98431",
OK: true,
},
{
Value: "3714 496353 98431",
Want: "**** ****** 98431",
OK: true,
Option: func(c *mask.Config[mask.CreditCardConfig]) {
c.Kind.MaskBankIdentifier = true
},
},
{
Value: "3714 496353 98431",
Want: "$$$$ $$$$$$ 98431",
OK: true,
Option: func(c *mask.Config[mask.CreditCardConfig]) {
c.Kind.MaskBankIdentifier = true
c.Symbol = '$'
},
},
})
}

func BenchmarkCreditCard(b *testing.B) {
for i := 0; i < b.N; i++ {
if _, err := mask.CreditCard("4111 3145 4001 1111", func(mc *mask.Config[mask.CreditCardConfig]) {
mc.Kind.MaskBankIdentifier = true
}); err != nil {
b.Fatal(err)
}
}
}
Loading