Various improvements to the serial package, including better error handling for out-of-range indices and improved test coverage for edge cases.

This commit is contained in:
2026-02-21 00:15:42 +01:00
parent 82679048e0
commit 4e933df49a
2 changed files with 250 additions and 44 deletions

View File

@@ -1,86 +1,170 @@
package serial
import (
"crypto/rand"
"encoding/binary"
"errors"
"fmt"
"log"
)
const (
MaxIndex = block
letters = 26
numbers = 900 // 100999
block = letters * letters * letters * numbers // 26^3 * 900
numbers = 900 // 100999
block = letters * letters * letters * letters * numbers // 26^4 * 900
)
var (
ErrInvalidFormat = errors.New("invalid format, expected LLL-NNN-LC")
ErrInvalidLetters = errors.New("invalid letters")
ErrForbiddenLetterTriplet = errors.New("forbidden letter combination")
ErrInvalidNumber = errors.New("invalid number")
ErrNumberOutOfRange = errors.New("number out of range")
ErrInvalidChecksum = errors.New("invalid checksum")
ErrIndexOutOfRange = errors.New("index out of range")
ErrRandomGenerationFailed = errors.New("random code generation failed")
)
func Encode(code string) (uint32, error) {
log.Printf("code: %v", code)
// Verwacht formaat: L1L2-NNN-L3-C => lengte 10, posities: 0,1,3,4,5,7,9
if len(code) != 10 || code[2] != '-' || code[6] != '-' || code[8] != '-' {
return 0, errors.New("invalid format, expected LL-NNN-L-C")
// Expected format: L1L2L3-NNN-L4C => length 10, positions: 0,1,2,4,5,6,8,9
if len(code) != 10 || code[3] != '-' || code[7] != '-' {
return 0, ErrInvalidFormat
}
l1, l2, l3 := code[0], code[1], code[7]
n1, n2, n3 := code[3], code[4], code[5]
l1, l2, l3, l4 := code[0], code[1], code[2], code[8]
n1, n2, n3 := code[4], code[5], code[6]
// letters checken
// forbidden alleen op l1,l2
// nummer parsen uit code[3:6]
// 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) {
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')
num := uint32(n1*100 + n2*10 + n3)
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) * numbers) + N)
idx := uint32((((((L1*letters+L2)*letters+L3)*letters + L4) * numbers) + N))
if code[9] != checksumLetter(idx) {
return 0, errors.New("invalid checksum")
return 0, ErrInvalidChecksum
}
log.Printf("idx: %v", idx)
return idx, nil
}
func Decode(idx uint32) (string, error) {
log.Printf("idx: %v", idx)
if idx >= block {
return "", errors.New("index out of range")
if idx >= MaxIndex {
return "", ErrIndexOutOfRange
}
x := int(idx)
L1 := x / (letters * letters * numbers)
r1 := x % (letters * letters * numbers)
L1 := x / (letters * letters * letters * numbers)
r1 := x % (letters * letters * letters * numbers)
L2 := r1 / (letters * numbers)
r2 := r1 % (letters * numbers)
L2 := r1 / (letters * letters * numbers)
r2 := r1 % (letters * letters * numbers)
L3 := r2 / numbers
N := r2 % numbers
L3 := r2 / (letters * numbers)
r3 := r2 % (letters * numbers)
L4 := r3 / numbers
N := r3 % numbers
l1 := byte('A' + L1)
l2 := byte('A' + L2)
l3 := byte('A' + L3)
l4 := byte('A' + L4)
if IsForbiddenPair(l1, l2) {
return "", errors.New("forbidden letter combination")
if IsForbiddenTriplet(l1, l2, l3) {
return "", ErrForbiddenLetterTriplet
}
c := checksumLetter(idx)
code := fmt.Sprintf("%c%c-%03d-%c-%c", l1, l2, N+100, l3, c)
code := fmt.Sprintf("%c%c%c-%03d-%c%c", l1, l2, l3, N+100, l4, c)
log.Printf("code: %v", code)
return code, nil
}
func RandomCode(isInUse ...func(uint32) bool) (string, uint32, error) {
var inUse func(uint32) bool
if len(isInUse) > 0 {
inUse = isInUse[0]
}
for attempts := 0; attempts < 1024; attempts++ {
idx, err := randomUint32n(MaxIndex)
if err != nil {
return "", 0, err
}
if inUse != nil && inUse(idx) {
continue
}
code, err := Decode(idx)
if err == nil {
return code, idx, nil
}
if errors.Is(err, ErrForbiddenLetterTriplet) {
continue
}
return "", 0, err
}
return "", 0, ErrRandomGenerationFailed
}
func checksumLetter(idx uint32) byte {
return byte('A' + idx%26)
}
func IsForbiddenPair(l1, l2 byte) bool {
func isUpperAlphaASCII(b byte) bool {
return (b >= 'A' && b <= 'Z')
}
func isNumber(b byte) bool {
return b >= '0' && b <= '9'
}
func randomUint32n(n uint32) (uint32, error) {
if n == 0 {
return 0, ErrIndexOutOfRange
}
maxUint32 := ^uint32(0)
limit := maxUint32 - (maxUint32 % n)
for {
var buf [4]byte
if _, err := rand.Read(buf[:]); err != nil {
return 0, err
}
v := binary.LittleEndian.Uint32(buf[:])
if v < limit {
return v % n, nil
}
}
}
func IsForbiddenTriplet(l1, l2, l3 byte) bool {
return false
}