235 lines
5.6 KiB
Go
235 lines
5.6 KiB
Go
// Copyright (c) 2026 Micha Hoiting
|
||
|
||
package serial
|
||
|
||
import (
|
||
"crypto/rand"
|
||
"encoding/binary"
|
||
"errors"
|
||
"fmt"
|
||
)
|
||
|
||
const (
|
||
MaxIndex = block
|
||
MaxRandomAttempts = 1024
|
||
|
||
letters = 26
|
||
numbers = 900 // 100–999
|
||
block = letters * letters * letters * letters * numbers // 26^4 * 900
|
||
)
|
||
|
||
var (
|
||
ErrInvalidFormat = errors.New("invalid format, expected LLL-NNN-LC")
|
||
ErrInvalidFormatNoChecksum = errors.New("invalid format, expected LLL-NNN-L")
|
||
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")
|
||
ErrRandomSourceFailed = errors.New("random source failed")
|
||
ErrNoAvailableIndex = errors.New("no available index found")
|
||
)
|
||
|
||
type RandomCodeOptions struct {
|
||
MaxAttempts int
|
||
IsInUse func(uint32) bool
|
||
RandomIndex func(max uint32) (uint32, error)
|
||
}
|
||
|
||
func Encode(code string) (uint32, error) {
|
||
// 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, l4 := code[0], code[1], code[2], code[8]
|
||
n1, n2, n3 := code[4], code[5], code[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')
|
||
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 idx, nil
|
||
}
|
||
|
||
func Decode(idx uint32) (string, error) {
|
||
if idx >= MaxIndex {
|
||
return "", ErrIndexOutOfRange
|
||
}
|
||
|
||
x := int(idx)
|
||
|
||
L1 := x / (letters * letters * letters * numbers)
|
||
r1 := x % (letters * letters * letters * numbers)
|
||
|
||
L2 := r1 / (letters * letters * numbers)
|
||
r2 := r1 % (letters * letters * 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 IsForbiddenTriplet(l1, l2, l3) {
|
||
return "", ErrForbiddenLetterTriplet
|
||
}
|
||
|
||
c := checksumLetter(idx)
|
||
|
||
code := fmt.Sprintf("%c%c%c-%03d-%c%c", l1, l2, l3, N+100, l4, c)
|
||
|
||
return code, nil
|
||
}
|
||
|
||
func CompleteCode(codeWithoutChecksum string) (string, uint32, error) {
|
||
if len(codeWithoutChecksum) != 9 || codeWithoutChecksum[3] != '-' || codeWithoutChecksum[7] != '-' {
|
||
return "", 0, ErrInvalidFormatNoChecksum
|
||
}
|
||
|
||
l1, l2, l3, l4 := codeWithoutChecksum[0], codeWithoutChecksum[1], codeWithoutChecksum[2], codeWithoutChecksum[8]
|
||
n1, n2, n3 := codeWithoutChecksum[4], codeWithoutChecksum[5], codeWithoutChecksum[6]
|
||
|
||
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)
|
||
|
||
return codeWithoutChecksum + string(checksum), idx, nil
|
||
}
|
||
|
||
func RandomCode(isInUse ...func(uint32) bool) (string, uint32, error) {
|
||
options := RandomCodeOptions{}
|
||
if len(isInUse) > 0 {
|
||
options.IsInUse = isInUse[0]
|
||
}
|
||
|
||
return RandomCodeWithOptions(options)
|
||
}
|
||
|
||
func RandomCodeWithOptions(options RandomCodeOptions) (string, uint32, error) {
|
||
maxAttempts := options.MaxAttempts
|
||
if maxAttempts <= 0 {
|
||
maxAttempts = MaxRandomAttempts
|
||
}
|
||
|
||
inUse := options.IsInUse
|
||
randomIndex := options.RandomIndex
|
||
if randomIndex == nil {
|
||
randomIndex = randomUint32n
|
||
}
|
||
|
||
for attempts := 0; attempts < maxAttempts; attempts++ {
|
||
idx, err := randomIndex(MaxIndex)
|
||
if err != nil {
|
||
return "", 0, fmt.Errorf("%w: %v", ErrRandomSourceFailed, err)
|
||
}
|
||
|
||
if idx >= MaxIndex {
|
||
return "", 0, fmt.Errorf("%w: index out of range", ErrRandomSourceFailed)
|
||
}
|
||
|
||
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, ErrNoAvailableIndex
|
||
}
|
||
|
||
func checksumLetter(idx uint32) byte {
|
||
return byte('A' + idx%26)
|
||
}
|
||
|
||
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
|
||
}
|