1// Package base32 implements RFC 4648 base32 and Crockford base32 as a pure,2// reusable package.3//4// Two alphabets, for two different jobs:5//6// - RFC 4648 (A–Z, 2–7) with '=' padding — the interoperable one; use it when7// something else has to decode the result.8// - Crockford (0–9, A–Z minus I, L, O and U) — designed to be read aloud and9// typed by humans: decoding folds case, treats I/L as 1 and O as 0, and10// ignores hyphens, so a mis-heard identifier still decodes. U is excluded11// to avoid accidental obscenities.12//13// Base32 costs 60% expansion (8 characters per 5 bytes) versus base64's 33%.14// You take that hit to get an alphabet that survives case-insensitive systems15// and being read over the phone.16//17// A live demo of this package is at18// [r/moul/x/daily/base32demo](/r/moul/x/daily/base32demo/v0).19package base322021import (22 "errors"23 "strings"24)2526// Alphabets.27const (28 StdAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"29 CrockfordAlphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"30)3132// MaxLen bounds input so gas stays predictable.33const MaxLen = 40963435var (36 // ErrTooLong is returned when input exceeds MaxLen.37 ErrTooLong = errors.New("base32: input too long")38 // ErrCorrupt is returned when input is not valid base32.39 ErrCorrupt = errors.New("base32: corrupt input")40)4142// encode turns src into base32 over the given alphabet, padding when pad.43func encode(src, alphabet string, pad bool) (string, error) {44 if len(src) > MaxLen {45 return "", ErrTooLong46 }47 var b strings.Builder48 for i := 0; i < len(src); i += 5 {49 // gather up to 5 bytes into a 40-bit group50 var buf [5]byte51 n := 052 for j := 0; j < 5 && i+j < len(src); j++ {53 buf[j] = src[i+j]54 n++55 }56 // 5 bytes -> 8 characters of 5 bits57 var chars [8]byte58 chars[0] = buf[0] >> 359 chars[1] = (buf[0]&0x07)<<2 | buf[1]>>660 chars[2] = (buf[1] & 0x3E) >> 161 chars[3] = (buf[1]&0x01)<<4 | buf[2]>>462 chars[4] = (buf[2]&0x0F)<<1 | buf[3]>>763 chars[5] = (buf[3] & 0x7C) >> 264 chars[6] = (buf[3]&0x03)<<3 | buf[4]>>565 chars[7] = buf[4] & 0x1F6667 // how many characters this group actually carries68 out := [6]int{0, 2, 4, 5, 7, 8}[n]69 for j := 0; j < out; j++ {70 b.WriteByte(alphabet[chars[j]])71 }72 if pad {73 for j := out; j < 8; j++ {74 b.WriteByte('=')75 }76 }77 }78 return b.String(), nil79}8081// Encode returns the RFC 4648 base32 of src, with '=' padding.82func Encode(src string) (string, error) { return encode(src, StdAlphabet, true) }8384// EncodeUnpadded returns RFC 4648 base32 without padding.85func EncodeUnpadded(src string) (string, error) { return encode(src, StdAlphabet, false) }8687// EncodeCrockford returns Crockford base32, which is never padded.88func EncodeCrockford(src string) (string, error) { return encode(src, CrockfordAlphabet, false) }8990// stdValue maps an RFC 4648 character to its 5-bit value, or -1.91func stdValue(c byte) int {92 switch {93 case c >= 'A' && c <= 'Z':94 return int(c - 'A')95 case c >= 'a' && c <= 'z':96 return int(c - 'a') // tolerate lowercase on decode97 case c >= '2' && c <= '7':98 return int(c-'2') + 2699 }100 return -1101}102103// crockfordValue maps a Crockford character to its value, or -1.104//105// The forgiving part: case is folded, I and L read as 1, O reads as 0, and106// hyphens are skipped by the caller. This is what makes a Crockford identifier107// survive being written down and typed back in.108func crockfordValue(c byte) int {109 if c >= 'a' && c <= 'z' {110 c -= 32111 }112 switch c {113 case 'O':114 return 0115 case 'I', 'L':116 return 1117 }118 if c >= '0' && c <= '9' {119 return int(c - '0')120 }121 if c >= 'A' && c <= 'Z' {122 if i := strings.IndexByte(CrockfordAlphabet, c); i >= 0 {123 return i124 }125 }126 return -1127}128129// decode turns base32 back into bytes using the given value function.130func decode(s string, value func(byte) int, skipHyphen bool) (string, error) {131 if len(s) > MaxLen {132 return "", ErrTooLong133 }134 // strip padding and (for Crockford) hyphens135 var clean strings.Builder136 for i := 0; i < len(s); i++ {137 c := s[i]138 if c == '=' {139 continue140 }141 if skipHyphen && c == '-' {142 continue143 }144 clean.WriteByte(c)145 }146 in := clean.String()147148 var b strings.Builder149 var acc uint64150 bits := 0151 for i := 0; i < len(in); i++ {152 v := value(in[i])153 if v < 0 {154 return "", ErrCorrupt155 }156 acc = acc<<5 | uint64(v)157 bits += 5158 if bits >= 8 {159 bits -= 8160 b.WriteByte(byte(acc >> uint(bits)))161 acc &= (1 << uint(bits)) - 1162 }163 }164 // leftover bits must be zero padding, never data165 if bits >= 5 || acc != 0 {166 return "", ErrCorrupt167 }168 return b.String(), nil169}170171// Decode parses RFC 4648 base32, tolerating lowercase and missing padding.172func Decode(s string) (string, error) { return decode(s, stdValue, false) }173174// DecodeCrockford parses Crockford base32, folding case, reading I/L as 1 and175// O as 0, and ignoring hyphens.176func DecodeCrockford(s string) (string, error) { return decode(s, crockfordValue, true) }177Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.