Various improvements and changed encoding/decoding format to LLL-NNN-LLC with reduced character set.
All checks were successful
CI / test (push) Successful in 16s
All checks were successful
CI / test (push) Successful in 16s
This commit is contained in:
@@ -37,7 +37,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Run example
|
- name: Run example
|
||||||
run: |
|
run: |
|
||||||
if ./bin/triplex-example && ./bin/triplex-example --complete ABC-123-D; then
|
if ./bin/triplex-example && ./bin/triplex-example --complete ABC-123-DE; then
|
||||||
echo "Example ran successfully"
|
echo "Example ran successfully"
|
||||||
else
|
else
|
||||||
echo "Example failed"
|
echo "Example failed"
|
||||||
|
|||||||
3
Makefile
3
Makefile
@@ -15,3 +15,6 @@ run-example:
|
|||||||
|
|
||||||
fmt:
|
fmt:
|
||||||
gofmt -w ./serial/*.go ./cmd/$(APP_EXAMPLE)/*.go
|
gofmt -w ./serial/*.go ./cmd/$(APP_EXAMPLE)/*.go
|
||||||
|
|
||||||
|
vet:
|
||||||
|
go vet ./...
|
||||||
|
|||||||
44
README.md
44
README.md
@@ -10,36 +10,36 @@ high-integrity identifiers. It maps a compact 32-bit integer space to a
|
|||||||
human-readable serial format:
|
human-readable serial format:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
LLL-NNN-LC
|
LLL-NNN-LLC
|
||||||
```
|
```
|
||||||
|
|
||||||
Where:
|
Where:
|
||||||
- `L` = data letter (`A`–`Z`)
|
- `L` = data letter (`A` – `Z` excluding `F`, `I`, `O`, `Q`, `U`)
|
||||||
- `N` = number (`100`–`999`)
|
- `N` = number (`100`–`999`)
|
||||||
- `C` = checksum letter (`A` – `Z`)
|
- `C` = checksum letter (`A` – `Z`)
|
||||||
|
|
||||||
For instance
|
For instance
|
||||||
|
|
||||||
```text
|
```text
|
||||||
ABC-123-DT
|
ABC-123-DEO
|
||||||
```
|
```
|
||||||
|
|
||||||
## What it provides
|
## What it provides
|
||||||
|
|
||||||
- 4 data letters
|
- 5 data letters
|
||||||
- 3 numeric digits (`100`–`999`)
|
- 3 numeric digits (`100`–`999`)
|
||||||
- 1 checksum letter
|
- 1 checksum letter
|
||||||
- Optional forbidden-triplet filtering on the first three letters
|
- Optional forbidden-triplet letter combination filtering on the first three letters
|
||||||
- Reversible integer encoding/decoding
|
- Reversible integer encoding/decoding
|
||||||
- Deterministic checksum generation
|
- Deterministic checksum generation
|
||||||
|
|
||||||
Total index capacity: **386,942,400** (`26^4 * 900`)
|
Total index capacity: **3,675,690,900** (`21^5 * 900`)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Reversible mapping: `Encode(code) ↔ Decode(index)`
|
- Reversible mapping: `Decode(code) ↔ Encode(index)`
|
||||||
- Deterministic checksum: `C = 'A' + (index % 26)`
|
- Deterministic checksum: `C = 'A' + (index % 26)`
|
||||||
- Compact index space: fits in `uint32`
|
- Compact index space: fits in `uint`
|
||||||
- Human-readable serial format
|
- Human-readable serial format
|
||||||
- Useful for manufacturing, logistics, SaaS identifiers, and audit-safe systems
|
- Useful for manufacturing, logistics, SaaS identifiers, and audit-safe systems
|
||||||
|
|
||||||
@@ -55,14 +55,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
idx := uint32(123456)
|
idx := uint(123456)
|
||||||
|
|
||||||
code, err := serial.Decode(idx)
|
code, err := serial.Encode(idx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
idx2, err := serial.Encode(code)
|
idx2, err := serial.Decode(code)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -74,21 +74,21 @@ func main() {
|
|||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- The current serial format is `LLL-NNN-LC`.
|
- The current serial format is `LLL-NNN-LLC`.
|
||||||
- Forbidden-triplet logic is centralized in `serial.IsForbiddenTriplet` and can be tailored to project rules.
|
- Forbidden-triplet logic is centralized in `serial.IsForbiddenTriplet` and can be tailored to project rules.
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
- `serial.Encode(code string) (uint32, error)`
|
- `serial.Decode(code string) (uint, error)`
|
||||||
- Parses and validates `LLL-NNN-LC`, checks checksum, and returns the deterministic index.
|
- Parses and validates `LLL-NNN-LLC`, checks checksum, and returns the deterministic index.
|
||||||
- `serial.Decode(idx uint32) (string, error)`
|
- `serial.Encode(idx uint) (string, error)`
|
||||||
- Converts a valid index back to `LLL-NNN-LC`.
|
- Converts a valid index back to `LLL-NNN-LLC`.
|
||||||
- `serial.CompleteCode(codeWithoutChecksum string) (string, uint32, error)`
|
- `serial.CompleteCode(codeWithoutChecksum string) (string, uint, error)`
|
||||||
- Takes `LLL-NNN-L` and returns the completed `LLL-NNN-LC` plus index.
|
- Takes `LLL-NNN-L` and returns the completed `LLL-NNN-LLC` plus index.
|
||||||
- `serial.RandomCode(isInUse ...func(uint32) bool) (string, uint32, error)`
|
- `serial.RandomCode(isInUse ...func(uint) bool) (string, uint, error)`
|
||||||
- Generates a random valid `LLL-NNN-LC` code and its corresponding index.
|
- Generates a random valid `LLL-NNN-LLC` code and its corresponding index.
|
||||||
- If provided, the callback is used to skip indices already in use by the client.
|
- If provided, the callback is used to skip indices already in use by the client.
|
||||||
- `serial.RandomCodeWithOptions(options serial.RandomCodeOptions) (string, uint32, error)`
|
- `serial.RandomCodeWithOptions(options serial.RandomCodeOptions) (string, uint, error)`
|
||||||
- Configurable variant with `MaxAttempts`, `IsInUse`, and custom `RandomIndex` source.
|
- Configurable variant with `MaxAttempts`, `IsInUse`, and custom `RandomIndex` source.
|
||||||
|
|
||||||
Example with options:
|
Example with options:
|
||||||
@@ -117,7 +117,7 @@ Exported errors:
|
|||||||
|
|
||||||
- `serial.ErrInvalidFormat`
|
- `serial.ErrInvalidFormat`
|
||||||
- `serial.ErrInvalidFormatNoChecksum`
|
- `serial.ErrInvalidFormatNoChecksum`
|
||||||
- `serial.ErrInvalidLetters`
|
- `serial.ErrInvalidLetter`
|
||||||
- `serial.ErrForbiddenLetterTriplet`
|
- `serial.ErrForbiddenLetterTriplet`
|
||||||
- `serial.ErrInvalidNumber`
|
- `serial.ErrInvalidNumber`
|
||||||
- `serial.ErrNumberOutOfRange`
|
- `serial.ErrNumberOutOfRange`
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ import (
|
|||||||
"git.hoiting.org/micha/triplex/serial"
|
"git.hoiting.org/micha/triplex/serial"
|
||||||
)
|
)
|
||||||
|
|
||||||
func alreadyUsedByClient(idx uint32) bool {
|
func alreadyUsedByClient(idx uint) bool {
|
||||||
return idx%2 == 0
|
return idx%2 == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func myRandomIndex(max uint32) (uint32, error) {
|
func myRandomIndex(max uint) (uint, error) {
|
||||||
if max == 0 {
|
if max == 0 {
|
||||||
return 0, fmt.Errorf("max must be > 0")
|
return 0, fmt.Errorf("max must be > 0")
|
||||||
}
|
}
|
||||||
@@ -27,7 +27,7 @@ func myRandomIndex(max uint32) (uint32, error) {
|
|||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return binary.LittleEndian.Uint32(buf[:]) % max, nil
|
return uint(binary.LittleEndian.Uint32(buf[:])) % max, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -48,10 +48,10 @@ func main() {
|
|||||||
|
|
||||||
opts := serial.RandomCodeOptions{
|
opts := serial.RandomCodeOptions{
|
||||||
MaxAttempts: 500,
|
MaxAttempts: 500,
|
||||||
IsInUse: func(idx uint32) bool {
|
IsInUse: func(idx uint) bool {
|
||||||
return alreadyUsedByClient(idx)
|
return alreadyUsedByClient(idx)
|
||||||
},
|
},
|
||||||
RandomIndex: func(max uint32) (uint32, error) {
|
RandomIndex: func(max uint) (uint, error) {
|
||||||
return myRandomIndex(max)
|
return myRandomIndex(max)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
245
serial/serial.go
245
serial/serial.go
@@ -13,15 +13,15 @@ const (
|
|||||||
MaxIndex = block
|
MaxIndex = block
|
||||||
MaxRandomAttempts = 1024
|
MaxRandomAttempts = 1024
|
||||||
|
|
||||||
letters = 26
|
letters = 21 // A-Z excluding F, I, O, Q, U
|
||||||
numbers = 900 // 100–999
|
numbers = 900 // 100–999
|
||||||
block = letters * letters * letters * letters * numbers // 26^4 * 900
|
block = letters * letters * letters * numbers * letters * letters // 26^5 * 900
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrInvalidFormat = errors.New("invalid format, expected LLL-NNN-LC")
|
ErrInvalidFormat = errors.New("invalid format, expected LLL-NNN-LLL")
|
||||||
ErrInvalidFormatNoChecksum = errors.New("invalid format, expected LLL-NNN-L")
|
ErrInvalidFormatNoChecksum = errors.New("invalid format, expected LLL-NNN-LL")
|
||||||
ErrInvalidLetters = errors.New("invalid letters")
|
ErrInvalidLetter = errors.New("invalid letters")
|
||||||
ErrForbiddenLetterTriplet = errors.New("forbidden letter combination")
|
ErrForbiddenLetterTriplet = errors.New("forbidden letter combination")
|
||||||
ErrInvalidNumber = errors.New("invalid number")
|
ErrInvalidNumber = errors.New("invalid number")
|
||||||
ErrNumberOutOfRange = errors.New("number out of range")
|
ErrNumberOutOfRange = errors.New("number out of range")
|
||||||
@@ -33,188 +33,225 @@ var (
|
|||||||
|
|
||||||
type RandomCodeOptions struct {
|
type RandomCodeOptions struct {
|
||||||
MaxAttempts int
|
MaxAttempts int
|
||||||
IsInUse func(uint32) bool
|
IsInUse func(uint) bool
|
||||||
RandomIndex func(max uint32) (uint32, error)
|
RandomIndex func(max uint) (uint, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Encode(code string) (uint32, error) {
|
func Decode(code string) (uint, error) {
|
||||||
// Expected format: L1L2L3-NNN-L4C => length 10, positions: 0,1,2,4,5,6,8,9
|
// Expected format: L1L2L3-NNN-L4L5C => length 10, positions: 0,1,2,4,5,6,8,9,10
|
||||||
if len(code) != 10 || code[3] != '-' || code[7] != '-' {
|
if len(code) != 11 ||
|
||||||
|
!isUpperASCIIAlpha(code[10]) {
|
||||||
return 0, ErrInvalidFormat
|
return 0, ErrInvalidFormat
|
||||||
}
|
}
|
||||||
|
idx, err := decodeWithoutChecksum(code[0:10])
|
||||||
l1, l2, l3, l4 := code[0], code[1], code[2], code[8]
|
if err != nil {
|
||||||
n1, n2, n3 := code[4], code[5], code[6]
|
return 0, err
|
||||||
|
|
||||||
// Validate letters
|
|
||||||
// Forbidden rule applies to l1,l2,l3
|
|
||||||
// Parse number from code[4:7]
|
|
||||||
if !isUpperAlphaASCII(l1) || !isUpperAlphaASCII(l2) || !isUpperAlphaASCII(l3) || !isUpperAlphaASCII(l4) {
|
|
||||||
return 0, ErrInvalidLetters
|
|
||||||
}
|
}
|
||||||
if IsForbiddenTriplet(l1, l2, l3) {
|
checksum := checksumLetter(idx)
|
||||||
return 0, ErrForbiddenLetterTriplet
|
if code[10] != checksum {
|
||||||
}
|
|
||||||
if !isNumber(n1) || !isNumber(n2) || !isNumber(n3) {
|
|
||||||
return 0, ErrInvalidNumber
|
|
||||||
}
|
|
||||||
|
|
||||||
L1 := uint32(l1 - 'A')
|
|
||||||
L2 := uint32(l2 - 'A')
|
|
||||||
L3 := uint32(l3 - 'A')
|
|
||||||
L4 := uint32(l4 - 'A')
|
|
||||||
num := uint32(n1-'0')*100 + uint32(n2-'0')*10 + uint32(n3-'0')
|
|
||||||
if num < 100 || num > 999 {
|
|
||||||
return 0, ErrNumberOutOfRange
|
|
||||||
}
|
|
||||||
N := num - 100
|
|
||||||
|
|
||||||
idx := uint32((((((L1*letters+L2)*letters+L3)*letters + L4) * numbers) + N))
|
|
||||||
|
|
||||||
if code[9] != checksumLetter(idx) {
|
|
||||||
return 0, ErrInvalidChecksum
|
return 0, ErrInvalidChecksum
|
||||||
}
|
}
|
||||||
|
|
||||||
return idx, nil
|
return idx, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Decode(idx uint32) (string, error) {
|
func Encode(idx uint) (string, error) {
|
||||||
if idx >= MaxIndex {
|
if idx >= MaxIndex {
|
||||||
return "", ErrIndexOutOfRange
|
return "", ErrIndexOutOfRange
|
||||||
}
|
}
|
||||||
|
|
||||||
x := int(idx)
|
x := int(idx)
|
||||||
|
|
||||||
L1 := x / (letters * letters * letters * numbers)
|
// L1 = // xLL-NNN-LL
|
||||||
r1 := x % (letters * letters * letters * numbers)
|
L1 := x / (letters * letters * numbers * letters * letters)
|
||||||
|
r1 := x % (letters * letters * numbers * letters * letters)
|
||||||
|
|
||||||
L2 := r1 / (letters * letters * numbers)
|
// L2 = // xL-NNN-LL
|
||||||
r2 := r1 % (letters * letters * numbers)
|
L2 := r1 / (letters * numbers * letters * letters)
|
||||||
|
r2 := r1 % (letters * numbers * letters * letters)
|
||||||
|
|
||||||
L3 := r2 / (letters * numbers)
|
// L3 = // x-NNN-LL
|
||||||
r3 := r2 % (letters * numbers)
|
L3 := r2 / (numbers * letters * letters)
|
||||||
|
r3 := r2 % (numbers * letters * letters)
|
||||||
|
|
||||||
L4 := r3 / numbers
|
// N = // x-LL
|
||||||
N := r3 % numbers
|
N := r3 / (letters * letters)
|
||||||
|
r4 := r3 % (letters * letters)
|
||||||
|
|
||||||
l1 := byte('A' + L1)
|
// L4 = // xL
|
||||||
l2 := byte('A' + L2)
|
L4 := r4 / letters
|
||||||
l3 := byte('A' + L3)
|
r5 := r4 % letters
|
||||||
l4 := byte('A' + L4)
|
|
||||||
|
// L5 = // x
|
||||||
|
L5 := r5
|
||||||
|
|
||||||
|
l1 := encodeLetter(L1)
|
||||||
|
l2 := encodeLetter(L2)
|
||||||
|
l3 := encodeLetter(L3)
|
||||||
|
l4 := encodeLetter(L4)
|
||||||
|
l5 := encodeLetter(L5)
|
||||||
|
|
||||||
if IsForbiddenTriplet(l1, l2, l3) {
|
if IsForbiddenTriplet(l1, l2, l3) {
|
||||||
return "", ErrForbiddenLetterTriplet
|
return "", ErrForbiddenLetterTriplet
|
||||||
}
|
}
|
||||||
|
|
||||||
c := checksumLetter(idx)
|
c := checksumLetter(idx)
|
||||||
|
code := fmt.Sprintf("%c%c%c-%03d-%c%c%c", l1, l2, l3, N+100, l4, l5, c)
|
||||||
code := fmt.Sprintf("%c%c%c-%03d-%c%c", l1, l2, l3, N+100, l4, c)
|
|
||||||
|
|
||||||
return code, nil
|
return code, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func CompleteCode(codeWithoutChecksum string) (string, uint32, error) {
|
func CompleteCode(codeWithoutChecksum string) (string, uint, error) {
|
||||||
if len(codeWithoutChecksum) != 9 || codeWithoutChecksum[3] != '-' || codeWithoutChecksum[7] != '-' {
|
if len(codeWithoutChecksum) != 10 {
|
||||||
return "", 0, ErrInvalidFormatNoChecksum
|
return "", 0, ErrInvalidFormatNoChecksum
|
||||||
}
|
}
|
||||||
|
idx, err := decodeWithoutChecksum(codeWithoutChecksum)
|
||||||
l1, l2, l3, l4 := codeWithoutChecksum[0], codeWithoutChecksum[1], codeWithoutChecksum[2], codeWithoutChecksum[8]
|
if err != nil {
|
||||||
n1, n2, n3 := codeWithoutChecksum[4], codeWithoutChecksum[5], codeWithoutChecksum[6]
|
return "", 0, err
|
||||||
|
|
||||||
if !isUpperAlphaASCII(l1) || !isUpperAlphaASCII(l2) || !isUpperAlphaASCII(l3) || !isUpperAlphaASCII(l4) {
|
|
||||||
return "", 0, ErrInvalidLetters
|
|
||||||
}
|
}
|
||||||
if IsForbiddenTriplet(l1, l2, l3) {
|
|
||||||
return "", 0, ErrForbiddenLetterTriplet
|
|
||||||
}
|
|
||||||
if !isNumber(n1) || !isNumber(n2) || !isNumber(n3) {
|
|
||||||
return "", 0, ErrInvalidNumber
|
|
||||||
}
|
|
||||||
|
|
||||||
L1 := uint32(l1 - 'A')
|
|
||||||
L2 := uint32(l2 - 'A')
|
|
||||||
L3 := uint32(l3 - 'A')
|
|
||||||
L4 := uint32(l4 - 'A')
|
|
||||||
num := uint32(n1-'0')*100 + uint32(n2-'0')*10 + uint32(n3-'0')
|
|
||||||
if num < 100 || num > 999 {
|
|
||||||
return "", 0, ErrNumberOutOfRange
|
|
||||||
}
|
|
||||||
N := num - 100
|
|
||||||
|
|
||||||
idx := uint32((((((L1*letters+L2)*letters+L3)*letters + L4) * numbers) + N))
|
|
||||||
checksum := checksumLetter(idx)
|
checksum := checksumLetter(idx)
|
||||||
|
|
||||||
return codeWithoutChecksum + string(checksum), idx, nil
|
return codeWithoutChecksum + string(checksum), idx, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func RandomCode(isInUse ...func(uint32) bool) (string, uint32, error) {
|
func RandomCode(isInUse ...func(uint) bool) (string, uint, error) {
|
||||||
options := RandomCodeOptions{}
|
options := RandomCodeOptions{}
|
||||||
if len(isInUse) > 0 {
|
if len(isInUse) > 0 {
|
||||||
options.IsInUse = isInUse[0]
|
options.IsInUse = isInUse[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
return RandomCodeWithOptions(options)
|
return RandomCodeWithOptions(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
func RandomCodeWithOptions(options RandomCodeOptions) (string, uint32, error) {
|
func RandomCodeWithOptions(options RandomCodeOptions) (string, uint, error) {
|
||||||
maxAttempts := options.MaxAttempts
|
maxAttempts := options.MaxAttempts
|
||||||
if maxAttempts <= 0 {
|
if maxAttempts <= 0 {
|
||||||
maxAttempts = MaxRandomAttempts
|
maxAttempts = MaxRandomAttempts
|
||||||
}
|
}
|
||||||
|
|
||||||
inUse := options.IsInUse
|
inUse := options.IsInUse
|
||||||
randomIndex := options.RandomIndex
|
randomIndex := options.RandomIndex
|
||||||
if randomIndex == nil {
|
if randomIndex == nil {
|
||||||
randomIndex = randomUint32n
|
randomIndex = randomuintn
|
||||||
}
|
}
|
||||||
|
|
||||||
for attempts := 0; attempts < maxAttempts; attempts++ {
|
for attempts := 0; attempts < maxAttempts; attempts++ {
|
||||||
idx, err := randomIndex(MaxIndex)
|
idx, err := randomIndex(MaxIndex)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", 0, fmt.Errorf("%w: %v", ErrRandomSourceFailed, err)
|
return "", 0, fmt.Errorf("%w: %v", ErrRandomSourceFailed, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if idx >= MaxIndex {
|
if idx >= MaxIndex {
|
||||||
return "", 0, fmt.Errorf("%w: index out of range", ErrRandomSourceFailed)
|
return "", 0, fmt.Errorf("%w: index out of range", ErrRandomSourceFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
if inUse != nil && inUse(idx) {
|
if inUse != nil && inUse(idx) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
code, err := Encode(idx)
|
||||||
code, err := Decode(idx)
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return code, idx, nil
|
return code, idx, nil
|
||||||
}
|
}
|
||||||
if errors.Is(err, ErrForbiddenLetterTriplet) {
|
if errors.Is(err, ErrForbiddenLetterTriplet) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", 0, err
|
return "", 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", 0, ErrNoAvailableIndex
|
return "", 0, ErrNoAvailableIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
func checksumLetter(idx uint32) byte {
|
func IsForbiddenTriplet(l1, l2, l3 byte) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeWithoutChecksum(code string) (uint, error) {
|
||||||
|
if code[3] != '-' ||
|
||||||
|
code[7] != '-' {
|
||||||
|
return 0, ErrInvalidFormat
|
||||||
|
}
|
||||||
|
l1, l2, l3 := code[0], code[1], code[2]
|
||||||
|
n1, n2, n3 := code[4], code[5], code[6]
|
||||||
|
l4, l5 := code[8], code[9]
|
||||||
|
|
||||||
|
// Validate letters
|
||||||
|
// Parse number from code[4:7]
|
||||||
|
if !isTriplexAlpha(l1) ||
|
||||||
|
!isTriplexAlpha(l2) ||
|
||||||
|
!isTriplexAlpha(l3) ||
|
||||||
|
!isTriplexAlpha(l4) ||
|
||||||
|
!isTriplexAlpha(l5) {
|
||||||
|
return 0, ErrInvalidLetter
|
||||||
|
}
|
||||||
|
if !isNumber(n1) ||
|
||||||
|
!isNumber(n2) ||
|
||||||
|
!isNumber(n3) {
|
||||||
|
return 0, ErrInvalidNumber
|
||||||
|
}
|
||||||
|
N, err := iton(n1, n2, n3)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
L1 := decodeLetter(l1)
|
||||||
|
L2 := decodeLetter(l2)
|
||||||
|
L3 := decodeLetter(l3)
|
||||||
|
L4 := decodeLetter(l4)
|
||||||
|
L5 := decodeLetter(l5)
|
||||||
|
idx := uint((((((L1*letters+L2)*letters+L3)*numbers+N)*letters+L4)*letters + L5))
|
||||||
|
return idx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checksumLetter(idx uint) byte {
|
||||||
return byte('A' + idx%26)
|
return byte('A' + idx%26)
|
||||||
}
|
}
|
||||||
|
|
||||||
func isUpperAlphaASCII(b byte) bool {
|
func isUpperASCIIAlpha(b byte) bool {
|
||||||
return (b >= 'A' && b <= 'Z')
|
return (b >= 'A' && b <= 'Z')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isTriplexAlpha(b byte) bool {
|
||||||
|
s := [...]bool{
|
||||||
|
'A': true, 'B': true, 'C': true, 'D': true, 'E': true, 'F': false,
|
||||||
|
'G': true, 'H': true, 'I': false, 'J': true, 'K': true, 'L': true,
|
||||||
|
'M': true, 'N': true, 'O': false, 'P': true, 'Q': false, 'R': true,
|
||||||
|
'S': true, 'T': true, 'U': true, 'V': true, 'W': true, 'X': true,
|
||||||
|
'Y': false, 'Z': true}
|
||||||
|
return (b >= 'A' && b <= 'Z' && s[b])
|
||||||
|
}
|
||||||
|
|
||||||
func isNumber(b byte) bool {
|
func isNumber(b byte) bool {
|
||||||
return b >= '0' && b <= '9'
|
return b >= '0' && b <= '9'
|
||||||
}
|
}
|
||||||
|
|
||||||
func randomUint32n(n uint32) (uint32, error) {
|
func encodeLetter(index int) byte {
|
||||||
|
s := [...]byte{
|
||||||
|
0: 'A', 1: 'B', 2: 'C', 3: 'D', 4: 'E', 5: 'G',
|
||||||
|
6: 'H', 7: 'J', 8: 'K', 9: 'L', 10: 'M',
|
||||||
|
11: 'N', 12: 'P', 13: 'R', 14: 'S', 15: 'T',
|
||||||
|
16: 'U', 17: 'V', 18: 'W', 19: 'X', 20: 'Z'}
|
||||||
|
if index < 0 || index >= len(s) {
|
||||||
|
panic("index out of range")
|
||||||
|
}
|
||||||
|
return s[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeLetter(letter byte) int {
|
||||||
|
s := [...]int{
|
||||||
|
'A': 0, 'B': 1, 'C': 2, 'D': 3, 'E': 4, 'F': -1, 'G': 5,
|
||||||
|
'H': 6, 'I': -1, 'J': 7, 'K': 8, 'L': 9, 'M': 10,
|
||||||
|
'N': 11, 'O': -1, 'P': 12, 'Q': -1, 'R': 13, 'S': 14, 'T': 15,
|
||||||
|
'U': 16, 'V': 17, 'W': 18, 'X': 19, 'Y': -1, 'Z': 20}
|
||||||
|
if letter < 'A' || letter > 'Z' || s[letter] == -1 {
|
||||||
|
panic("invalid letter")
|
||||||
|
}
|
||||||
|
return s[letter]
|
||||||
|
}
|
||||||
|
|
||||||
|
func iton(n1, n2, n3 byte) (int, error) {
|
||||||
|
num := int(n1-'0')*100 + int(n2-'0')*10 + int(n3-'0')
|
||||||
|
if num < 100 || num > 999 {
|
||||||
|
return 0, ErrNumberOutOfRange
|
||||||
|
}
|
||||||
|
return num - 100, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func randomuintn(n uint) (uint, error) {
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
return 0, ErrIndexOutOfRange
|
return 0, ErrIndexOutOfRange
|
||||||
}
|
}
|
||||||
|
|
||||||
maxUint32 := ^uint32(0)
|
maxuint := ^uint(0)
|
||||||
limit := maxUint32 - (maxUint32 % n)
|
limit := maxuint - (maxuint % n)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
var buf [4]byte
|
var buf [4]byte
|
||||||
@@ -222,13 +259,9 @@ func randomUint32n(n uint32) (uint32, error) {
|
|||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
v := binary.LittleEndian.Uint32(buf[:])
|
v := uint(binary.LittleEndian.Uint32(buf[:]))
|
||||||
if v < limit {
|
if v < limit {
|
||||||
return v % n, nil
|
return v % n, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsForbiddenTriplet(l1, l2, l3 byte) bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -11,19 +11,19 @@ import (
|
|||||||
"git.hoiting.org/micha/triplex/serial"
|
"git.hoiting.org/micha/triplex/serial"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Property: Decode(Encode(code)) == code (for valid codes)
|
// Property: Encode(Decode(code)) == code (for valid codes)
|
||||||
func TestEncodeDecodeRoundtrip(t *testing.T) {
|
func TestEncodeDecodeRoundtrip(t *testing.T) {
|
||||||
f := func(idx uint32) bool {
|
f := func(idx uint) bool {
|
||||||
if idx >= serial.MaxIndex {
|
if idx >= serial.MaxIndex {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
code, err := serial.Decode(idx)
|
code, err := serial.Encode(idx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
idx2, err := serial.Encode(code)
|
idx2, err := serial.Decode(code)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -32,28 +32,28 @@ func TestEncodeDecodeRoundtrip(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := quick.Check(f, nil); err != nil {
|
if err := quick.Check(f, nil); err != nil {
|
||||||
t.Fatalf("Encode/Decode roundtrip failed: %v", err)
|
t.Fatalf("Decode/Encode roundtrip failed: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Property: Decode(Encode(Decode(i))) == Decode(i) (idempotent on valid indices)
|
// Property: Encode(Decode(Encode(i))) == Encode(i) (idempotent on valid indices)
|
||||||
func TestDecodeEncodeDecodeIdempotent(t *testing.T) {
|
func TestDecodeEncodeDecodeIdempotent(t *testing.T) {
|
||||||
f := func(idx uint32) bool {
|
f := func(idx uint) bool {
|
||||||
if idx >= serial.MaxIndex {
|
if idx >= serial.MaxIndex {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
code1, err := serial.Decode(idx)
|
code1, err := serial.Encode(idx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return true // decode may reject indices (e.g. forbidden pair)
|
return true // decode may reject indices (e.g. forbidden pair)
|
||||||
}
|
}
|
||||||
|
|
||||||
idx2, err := serial.Encode(code1)
|
idx2, err := serial.Decode(code1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
code2, err := serial.Decode(idx2)
|
code2, err := serial.Encode(idx2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -62,18 +62,18 @@ func TestDecodeEncodeDecodeIdempotent(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := quick.Check(f, nil); err != nil {
|
if err := quick.Check(f, nil); err != nil {
|
||||||
t.Fatalf("Decode/Encode/Decode idempotence failed: %v", err)
|
t.Fatalf("Encode/Decode/Encode idempotence failed: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Property: forbidden first three letters are never returned by Decode
|
// Property: forbidden first three letters are never returned by Encode
|
||||||
func TestForbiddenTripletsNeverDecoded(t *testing.T) {
|
func TestForbiddenTripletsNeverDecoded(t *testing.T) {
|
||||||
f := func(idx uint32) bool {
|
f := func(idx uint) bool {
|
||||||
if idx >= serial.MaxIndex {
|
if idx >= serial.MaxIndex {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
code, err := serial.Decode(idx)
|
code, err := serial.Encode(idx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return true // decode may reject indices
|
return true // decode may reject indices
|
||||||
}
|
}
|
||||||
@@ -86,31 +86,31 @@ func TestForbiddenTripletsNeverDecoded(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := quick.Check(f, nil); err != nil {
|
if err := quick.Check(f, nil); err != nil {
|
||||||
t.Fatalf("Forbidden triplets appeared in Decode: %v", err)
|
t.Fatalf("Forbidden triplets appeared in Encode: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optional: basic check for format LLL-NNN-LC
|
// Optional: basic check for format LLL-NNN-LLC
|
||||||
func TestDecodedFormatLooksCorrect(t *testing.T) {
|
func TestDecodedFormatLooksCorrect(t *testing.T) {
|
||||||
f := func(idx uint32) bool {
|
f := func(idx uint) bool {
|
||||||
if idx >= serial.MaxIndex {
|
if idx >= serial.MaxIndex {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
code, err := serial.Decode(idx)
|
code, err := serial.Encode(idx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(code) != 10 {
|
if len(code) != 11 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if code[3] != '-' || code[7] != '-' {
|
if code[3] != '-' || code[7] != '-' {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// letters at positions 0,1,2,8,9
|
// letters at positions 0,1,2,8,9,10
|
||||||
for _, p := range []int{0, 1, 2, 8, 9} {
|
for _, p := range []int{0, 1, 2, 8, 9, 10} {
|
||||||
if code[p] < 'A' || code[p] > 'Z' {
|
if code[p] < 'A' || code[p] > 'Z' {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -121,6 +121,9 @@ func TestDecodedFormatLooksCorrect(t *testing.T) {
|
|||||||
if code[p] < '0' || code[p] > '9' {
|
if code[p] < '0' || code[p] > '9' {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if code[0] == '0' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@@ -131,18 +134,249 @@ func TestDecodedFormatLooksCorrect(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEncodeDecodeRoundtripSpecificIndex11223344(t *testing.T) {
|
func TestEncodeSpecificIndex0(t *testing.T) {
|
||||||
baseIdx := uint32(0x11223344)
|
code := "AAA-100-AA"
|
||||||
idx := baseIdx % serial.MaxIndex
|
idx := uint(0)
|
||||||
|
|
||||||
code, err := serial.Decode(idx)
|
code2, err := serial.Encode(idx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Decode failed for idx %d (derived from 0x11223344): %v", idx, err)
|
t.Fatalf("Encode failed for idx %d: %v", idx, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
idx2, err := serial.Encode(code)
|
// Check excluding the checksum letter
|
||||||
|
if code != code2[0:10] {
|
||||||
|
t.Fatalf("Encode failed for idx %d: exptect: %v actual: %v", idx, code, code2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeSpecificIndex1(t *testing.T) {
|
||||||
|
code := "AAA-100-AB"
|
||||||
|
idx := uint(1)
|
||||||
|
|
||||||
|
code2, err := serial.Encode(idx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Encode failed for code %q: %v", code, err)
|
t.Fatalf("Encode failed for idx %d: %v", idx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check excluding the checksum letter
|
||||||
|
if code != code2[0:10] {
|
||||||
|
t.Fatalf("Encode failed for idx %d: exptect: %v actual: %v", idx, code, code2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestEncodeSpecificIndex20(t *testing.T) {
|
||||||
|
code := "AAA-100-AZ"
|
||||||
|
idx := uint(20)
|
||||||
|
|
||||||
|
code2, err := serial.Encode(idx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Encode failed for idx %d: %v", idx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check excluding the checksum letter
|
||||||
|
if code != code2[0:10] {
|
||||||
|
t.Fatalf("Encode failed for idx %d: exptect: %v actual: %v", idx, code, code2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeSpecificIndex21(t *testing.T) {
|
||||||
|
code := "AAA-100-BA"
|
||||||
|
idx := uint(21)
|
||||||
|
|
||||||
|
code2, err := serial.Encode(idx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Encode failed for idx %d: %v", idx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check excluding the checksum letter
|
||||||
|
if code != code2[0:10] {
|
||||||
|
t.Fatalf("Encode failed for idx %d: exptect: %v actual: %v", idx, code, code2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeSpecificIndex42(t *testing.T) {
|
||||||
|
code := "AAA-100-CA"
|
||||||
|
idx := uint(42)
|
||||||
|
|
||||||
|
code2, err := serial.Encode(idx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Encode failed for idx %d: %v", idx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check excluding the checksum letter
|
||||||
|
if code != code2[0:10] {
|
||||||
|
t.Fatalf("Encode failed for idx %d: exptect: %v actual: %v", idx, code, code2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeSpecificIndex441(t *testing.T) {
|
||||||
|
code := "AAA-101-AA"
|
||||||
|
idx := uint(441)
|
||||||
|
|
||||||
|
code2, err := serial.Encode(idx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Encode failed for idx %d: %v", idx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check excluding the checksum letter
|
||||||
|
if code != code2[0:10] {
|
||||||
|
t.Fatalf("Encode failed for idx %d: exptect: %v actual: %v", idx, code, code2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeSpecificIndex396900(t *testing.T) {
|
||||||
|
code := "AAB-100-AA"
|
||||||
|
idx := uint(396900)
|
||||||
|
|
||||||
|
code2, err := serial.Encode(idx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Encode failed for idx %d: %v", idx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check excluding the checksum letter
|
||||||
|
if code != code2[0:10] {
|
||||||
|
t.Fatalf("Encode failed for idx %d: exptect: %v actual: %v", idx, code, code2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeSpecificIndex8334900(t *testing.T) {
|
||||||
|
code := "ABA-100-AA"
|
||||||
|
idx := uint(8334900)
|
||||||
|
|
||||||
|
code2, err := serial.Encode(idx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Encode failed for idx %d: %v", idx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check excluding the checksum letter
|
||||||
|
if code != code2[0:10] {
|
||||||
|
t.Fatalf("Encode failed for idx %d: exptect: %v actual: %v", idx, code, code2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeSpecificIndex175032900(t *testing.T) {
|
||||||
|
code := "BAA-100-AA"
|
||||||
|
idx := uint(175032900)
|
||||||
|
|
||||||
|
code2, err := serial.Encode(idx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Encode failed for idx %d: %v", idx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check excluding the checksum letter
|
||||||
|
if code != code2[0:10] {
|
||||||
|
t.Fatalf("Encode failed for idx %d: exptect: %v actual: %v", idx, code, code2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDencodeSpecificIndex0(t *testing.T) {
|
||||||
|
code := "AAA-100-AAA"
|
||||||
|
idx := uint(0)
|
||||||
|
|
||||||
|
idx2, err := serial.Decode(code)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Decode failed for code %q: %v", code, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if idx != idx2 {
|
||||||
|
t.Fatalf("roundtrip mismatch: got %d, want %d", idx2, idx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDencodeSpecificIndex21(t *testing.T) {
|
||||||
|
code := "AAA-100-BAV"
|
||||||
|
idx := uint(21)
|
||||||
|
|
||||||
|
idx2, err := serial.Decode(code)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Decode failed for code %q: %v", code, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if idx != idx2 {
|
||||||
|
t.Fatalf("roundtrip mismatch: got %d, want %d", idx2, idx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDencodeSpecificIndex1(t *testing.T) {
|
||||||
|
code := "AAA-100-ABB"
|
||||||
|
idx := uint(1)
|
||||||
|
|
||||||
|
idx2, err := serial.Decode(code)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Decode failed for code %q: %v", code, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if idx != idx2 {
|
||||||
|
t.Fatalf("roundtrip mismatch: got %d, want %d", idx2, idx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDencodeSpecificIndex441(t *testing.T) {
|
||||||
|
code := "AAA-101-AAZ"
|
||||||
|
idx := uint(441)
|
||||||
|
|
||||||
|
idx2, err := serial.Decode(code)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Decode failed for code %q: %v", code, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if idx != idx2 {
|
||||||
|
t.Fatalf("roundtrip mismatch: got %d, want %d", idx2, idx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestDencodeSpecificIndex396900(t *testing.T) {
|
||||||
|
code := "AAB-100-AAK"
|
||||||
|
idx := uint(396900)
|
||||||
|
|
||||||
|
idx2, err := serial.Decode(code)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Decode failed for code %q: %v", code, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if idx != idx2 {
|
||||||
|
t.Fatalf("roundtrip mismatch: got %d, want %d", idx2, idx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDencodeSpecificIndex8334900(t *testing.T) {
|
||||||
|
code := "ABA-100-AAC"
|
||||||
|
idx := uint(8334900)
|
||||||
|
|
||||||
|
idx2, err := serial.Decode(code)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Decode failed for code %q: %v", code, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if idx != idx2 {
|
||||||
|
t.Fatalf("roundtrip mismatch: got %d, want %d", idx2, idx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDencodeSpecificIndex175032900(t *testing.T) {
|
||||||
|
code := "BAA-100-AAQ"
|
||||||
|
idx := uint(175032900)
|
||||||
|
|
||||||
|
idx2, err := serial.Decode(code)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Decode failed for code %q: %v", code, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if idx != idx2 {
|
||||||
|
t.Fatalf("roundtrip mismatch: got %d, want %d", idx2, idx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeDecodeRoundtripSpecificIndex11223344(t *testing.T) {
|
||||||
|
baseIdx := uint(0x11223344)
|
||||||
|
idx := baseIdx % serial.MaxIndex
|
||||||
|
|
||||||
|
code, err := serial.Encode(idx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Encode failed for idx %d (derived from 0x11223344): %v", idx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
idx2, err := serial.Decode(code)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Decode failed for code %q: %v", code, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if idx2 != idx {
|
if idx2 != idx {
|
||||||
@@ -151,51 +385,51 @@ func TestEncodeDecodeRoundtripSpecificIndex11223344(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDecodeOutOfRangeAtMaxIndex(t *testing.T) {
|
func TestDecodeOutOfRangeAtMaxIndex(t *testing.T) {
|
||||||
_, err := serial.Decode(serial.MaxIndex)
|
_, err := serial.Encode(serial.MaxIndex)
|
||||||
if !errors.Is(err, serial.ErrIndexOutOfRange) {
|
if !errors.Is(err, serial.ErrIndexOutOfRange) {
|
||||||
t.Fatalf("expected index out of range error, got: %v", err)
|
t.Fatalf("expected index out of range error, got: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDecodeOutOfRangeAtMaxIndexPlusHundred(t *testing.T) {
|
func TestDecodeOutOfRangeAtMaxIndexPlusHundred(t *testing.T) {
|
||||||
_, err := serial.Decode(serial.MaxIndex + 100)
|
_, err := serial.Encode(serial.MaxIndex + 100)
|
||||||
if !errors.Is(err, serial.ErrIndexOutOfRange) {
|
if !errors.Is(err, serial.ErrIndexOutOfRange) {
|
||||||
t.Fatalf("expected index out of range error, got: %v", err)
|
t.Fatalf("expected index out of range error, got: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEncodeRejectsOldFormatWithThirdDash(t *testing.T) {
|
func TestEncodeRejectsOldFormatWithThirdDash(t *testing.T) {
|
||||||
_, err := serial.Encode("AA-100-AA")
|
_, err := serial.Decode("AA-100-AAA")
|
||||||
if !errors.Is(err, serial.ErrInvalidFormat) {
|
if !errors.Is(err, serial.ErrInvalidFormat) {
|
||||||
t.Fatalf("expected invalid format error, got: %v", err)
|
t.Fatalf("expected invalid format error, got: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEncodeRejectsLowercaseLetters(t *testing.T) {
|
func TestEncodeRejectsLowercaseLetters(t *testing.T) {
|
||||||
_, err := serial.Encode("AAa-100-AA")
|
_, err := serial.Decode("AAa-100-AAA")
|
||||||
if !errors.Is(err, serial.ErrInvalidLetters) {
|
if !errors.Is(err, serial.ErrInvalidLetter) {
|
||||||
t.Fatalf("expected invalid letters error, got: %v", err)
|
t.Fatalf("expected invalid letters error, got: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEncodeRejectsInvalidChecksum(t *testing.T) {
|
func TestEncodeRejectsInvalidChecksum(t *testing.T) {
|
||||||
_, err := serial.Encode("AAA-100-AB")
|
_, err := serial.Decode("AAA-100-AAB")
|
||||||
if !errors.Is(err, serial.ErrInvalidChecksum) {
|
if !errors.Is(err, serial.ErrInvalidChecksum) {
|
||||||
t.Fatalf("expected invalid checksum error, got: %v", err)
|
t.Fatalf("expected invalid checksum error, got: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEncodeRejectsNonDigitInNumber(t *testing.T) {
|
func TestEncodeRejectsNonDigitInNumber(t *testing.T) {
|
||||||
_, err := serial.Encode("AAA-1A0-AA")
|
_, err := serial.Decode("AAA-1A0-AAA")
|
||||||
if !errors.Is(err, serial.ErrInvalidNumber) {
|
if !errors.Is(err, serial.ErrInvalidNumber) {
|
||||||
t.Fatalf("expected invalid number error, got: %v", err)
|
t.Fatalf("expected invalid number error, got: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCompleteCodeRoundtrip(t *testing.T) {
|
func TestCompleteCodeRoundtrip(t *testing.T) {
|
||||||
original, err := serial.Decode(123456)
|
original, err := serial.Encode(123456)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Decode failed: %v", err)
|
t.Fatalf("Encode failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
withoutChecksum := original[:len(original)-1]
|
withoutChecksum := original[:len(original)-1]
|
||||||
@@ -208,9 +442,9 @@ func TestCompleteCodeRoundtrip(t *testing.T) {
|
|||||||
t.Fatalf("completed code mismatch: got %q, want %q", full, original)
|
t.Fatalf("completed code mismatch: got %q, want %q", full, original)
|
||||||
}
|
}
|
||||||
|
|
||||||
idx2, err := serial.Encode(full)
|
idx2, err := serial.Decode(full)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Encode failed for completed code %q: %v", full, err)
|
t.Fatalf("Decode failed for completed code %q: %v", full, err)
|
||||||
}
|
}
|
||||||
if idx2 != idx {
|
if idx2 != idx {
|
||||||
t.Fatalf("idx mismatch: got %d, want %d", idx, idx2)
|
t.Fatalf("idx mismatch: got %d, want %d", idx, idx2)
|
||||||
@@ -218,7 +452,7 @@ func TestCompleteCodeRoundtrip(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCompleteCodeRejectsInvalidFormat(t *testing.T) {
|
func TestCompleteCodeRejectsInvalidFormat(t *testing.T) {
|
||||||
_, _, err := serial.CompleteCode("AAA-10-A")
|
_, _, err := serial.CompleteCode("AAA-10-AA")
|
||||||
if !errors.Is(err, serial.ErrInvalidFormatNoChecksum) {
|
if !errors.Is(err, serial.ErrInvalidFormatNoChecksum) {
|
||||||
t.Fatalf("expected invalid no-checksum format error, got: %v", err)
|
t.Fatalf("expected invalid no-checksum format error, got: %v", err)
|
||||||
}
|
}
|
||||||
@@ -231,18 +465,18 @@ func TestRandomCodeRoundtrip(t *testing.T) {
|
|||||||
t.Fatalf("RandomCode failed: %v", err)
|
t.Fatalf("RandomCode failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
idx2, err := serial.Encode(code)
|
idx2, err := serial.Decode(code)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Encode failed for random code %q: %v", code, err)
|
t.Fatalf("Decode failed for random code %q: %v", code, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if idx2 != idx {
|
if idx2 != idx {
|
||||||
t.Fatalf("returned idx mismatch: got %d, want %d", idx, idx2)
|
t.Fatalf("returned idx mismatch: got %d, want %d", idx, idx2)
|
||||||
}
|
}
|
||||||
|
|
||||||
decoded, err := serial.Decode(idx)
|
decoded, err := serial.Encode(idx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Decode failed for idx %d: %v", idx, err)
|
t.Fatalf("Encode failed for idx %d: %v", idx, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if decoded != code {
|
if decoded != code {
|
||||||
@@ -254,7 +488,7 @@ func TestRandomCodeRoundtrip(t *testing.T) {
|
|||||||
func TestRandomCodeInvokesInUseCallback(t *testing.T) {
|
func TestRandomCodeInvokesInUseCallback(t *testing.T) {
|
||||||
called := false
|
called := false
|
||||||
|
|
||||||
code, idx, err := serial.RandomCode(func(candidate uint32) bool {
|
code, idx, err := serial.RandomCode(func(candidate uint) bool {
|
||||||
called = true
|
called = true
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
@@ -265,9 +499,9 @@ func TestRandomCodeInvokesInUseCallback(t *testing.T) {
|
|||||||
t.Fatalf("expected in-use callback to be called")
|
t.Fatalf("expected in-use callback to be called")
|
||||||
}
|
}
|
||||||
|
|
||||||
idx2, err := serial.Encode(code)
|
idx2, err := serial.Decode(code)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Encode failed for random code %q: %v", code, err)
|
t.Fatalf("Decode failed for random code %q: %v", code, err)
|
||||||
}
|
}
|
||||||
if idx2 != idx {
|
if idx2 != idx {
|
||||||
t.Fatalf("returned idx mismatch: got %d, want %d", idx, idx2)
|
t.Fatalf("returned idx mismatch: got %d, want %d", idx, idx2)
|
||||||
@@ -275,7 +509,7 @@ func TestRandomCodeInvokesInUseCallback(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRandomCodeFailsWhenAllIndicesInUse(t *testing.T) {
|
func TestRandomCodeFailsWhenAllIndicesInUse(t *testing.T) {
|
||||||
_, _, err := serial.RandomCode(func(candidate uint32) bool {
|
_, _, err := serial.RandomCode(func(candidate uint) bool {
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
if !errors.Is(err, serial.ErrNoAvailableIndex) {
|
if !errors.Is(err, serial.ErrNoAvailableIndex) {
|
||||||
@@ -286,7 +520,7 @@ func TestRandomCodeFailsWhenAllIndicesInUse(t *testing.T) {
|
|||||||
func TestRandomCodeWithOptionsMaxAttempts(t *testing.T) {
|
func TestRandomCodeWithOptionsMaxAttempts(t *testing.T) {
|
||||||
_, _, err := serial.RandomCodeWithOptions(serial.RandomCodeOptions{
|
_, _, err := serial.RandomCodeWithOptions(serial.RandomCodeOptions{
|
||||||
MaxAttempts: 1,
|
MaxAttempts: 1,
|
||||||
IsInUse: func(candidate uint32) bool {
|
IsInUse: func(candidate uint) bool {
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -297,7 +531,7 @@ func TestRandomCodeWithOptionsMaxAttempts(t *testing.T) {
|
|||||||
|
|
||||||
func TestRandomCodeWithOptionsRandomSourceFailure(t *testing.T) {
|
func TestRandomCodeWithOptionsRandomSourceFailure(t *testing.T) {
|
||||||
_, _, err := serial.RandomCodeWithOptions(serial.RandomCodeOptions{
|
_, _, err := serial.RandomCodeWithOptions(serial.RandomCodeOptions{
|
||||||
RandomIndex: func(max uint32) (uint32, error) {
|
RandomIndex: func(max uint) (uint, error) {
|
||||||
return 0, fmt.Errorf("rng down")
|
return 0, fmt.Errorf("rng down")
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user