// Copyright (c) 2026 Micha Hoiting package serial import ( "crypto/rand" "encoding/binary" "errors" "fmt" ) const ( MaxIndex = block MaxRandomAttempts = 1024 letters = 21 // A-Z excluding F, I, O, Q, U numbers = 900 // 100–999 block = letters * letters * letters * numbers * letters * letters // 26^5 * 900 ) var ( ErrInvalidFormat = errors.New("invalid format, expected LLL-NNN-LLL") ErrInvalidFormatNoChecksum = errors.New("invalid format, expected LLL-NNN-LL") ErrInvalidLetter = 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(uint) bool RandomIndex func(max uint) (uint, error) } func Decode(code string) (uint, error) { // Expected format: L1L2L3-NNN-L4L5C => length 10, positions: 0,1,2,4,5,6,8,9,10 if len(code) != 11 || !isUpperASCIIAlpha(code[10]) { return 0, ErrInvalidFormat } idx, err := decodeWithoutChecksum(code[0:10]) if err != nil { return 0, err } checksum := checksumLetter(idx) if code[10] != checksum { return 0, ErrInvalidChecksum } return idx, nil } func Encode(idx uint) (string, error) { if idx >= MaxIndex { return "", ErrIndexOutOfRange } x := int(idx) // L1 = // xLL-NNN-LL L1 := x / (letters * letters * numbers * letters * letters) r1 := x % (letters * letters * numbers * letters * letters) // L2 = // xL-NNN-LL L2 := r1 / (letters * numbers * letters * letters) r2 := r1 % (letters * numbers * letters * letters) // L3 = // x-NNN-LL L3 := r2 / (numbers * letters * letters) r3 := r2 % (numbers * letters * letters) // N = // x-LL N := r3 / (letters * letters) r4 := r3 % (letters * letters) // L4 = // xL L4 := r4 / letters r5 := r4 % letters // L5 = // x L5 := r5 l1 := encodeLetter(L1) l2 := encodeLetter(L2) l3 := encodeLetter(L3) l4 := encodeLetter(L4) l5 := encodeLetter(L5) if IsForbiddenTriplet(l1, l2, l3) { return "", ErrForbiddenLetterTriplet } c := checksumLetter(idx) code := fmt.Sprintf("%c%c%c-%03d-%c%c%c", l1, l2, l3, N+100, l4, l5, c) return code, nil } func CompleteCode(codeWithoutChecksum string) (string, uint, error) { if len(codeWithoutChecksum) != 10 { return "", 0, ErrInvalidFormatNoChecksum } idx, err := decodeWithoutChecksum(codeWithoutChecksum) if err != nil { return "", 0, err } checksum := checksumLetter(idx) return codeWithoutChecksum + string(checksum), idx, nil } func RandomCode(isInUse ...func(uint) bool) (string, uint, error) { options := RandomCodeOptions{} if len(isInUse) > 0 { options.IsInUse = isInUse[0] } return RandomCodeWithOptions(options) } func RandomCodeWithOptions(options RandomCodeOptions) (string, uint, error) { maxAttempts := options.MaxAttempts if maxAttempts <= 0 { maxAttempts = MaxRandomAttempts } inUse := options.IsInUse randomIndex := options.RandomIndex if randomIndex == nil { randomIndex = randomuintn } 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 := Encode(idx) if err == nil { return code, idx, nil } if errors.Is(err, ErrForbiddenLetterTriplet) { continue } return "", 0, err } return "", 0, ErrNoAvailableIndex } 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) } func isUpperASCIIAlpha(b byte) bool { 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 { return b >= '0' && b <= '9' } 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 { return 0, ErrIndexOutOfRange } maxuint := ^uint(0) limit := maxuint - (maxuint % n) for { var buf [4]byte if _, err := rand.Read(buf[:]); err != nil { return 0, err } v := uint(binary.LittleEndian.Uint32(buf[:])) if v < limit { return v % n, nil } } }