base32.gno
4.80 Kb · 176 lines
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 when
7// something else has to decode the result.
8// - Crockford (0–9, A–Z minus I, L, O and U) — designed to be read aloud and
9// typed by humans: decoding folds case, treats I/L as 1 and O as 0, and
10// ignores hyphens, so a mis-heard identifier still decodes. U is excluded
11// 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 systems
15// and being read over the phone.
16//
17// A live demo of this package is at
18// [r/moul/x/daily/base32demo](/r/moul/x/daily/base32demo/v0).
19package base32
20
21import (
22 "errors"
23 "strings"
24)
25
26// Alphabets.
27const (
28 StdAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
29 CrockfordAlphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
30)
31
32// MaxLen bounds input so gas stays predictable.
33const MaxLen = 4096
34
35var (
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)
41
42// 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 "", ErrTooLong
46 }
47 var b strings.Builder
48 for i := 0; i < len(src); i += 5 {
49 // gather up to 5 bytes into a 40-bit group
50 var buf [5]byte
51 n := 0
52 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 bits
57 var chars [8]byte
58 chars[0] = buf[0] >> 3
59 chars[1] = (buf[0]&0x07)<<2 | buf[1]>>6
60 chars[2] = (buf[1] & 0x3E) >> 1
61 chars[3] = (buf[1]&0x01)<<4 | buf[2]>>4
62 chars[4] = (buf[2]&0x0F)<<1 | buf[3]>>7
63 chars[5] = (buf[3] & 0x7C) >> 2
64 chars[6] = (buf[3]&0x03)<<3 | buf[4]>>5
65 chars[7] = buf[4] & 0x1F
66
67 // how many characters this group actually carries
68 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(), nil
79}
80
81// Encode returns the RFC 4648 base32 of src, with '=' padding.
82func Encode(src string) (string, error) { return encode(src, StdAlphabet, true) }
83
84// EncodeUnpadded returns RFC 4648 base32 without padding.
85func EncodeUnpadded(src string) (string, error) { return encode(src, StdAlphabet, false) }
86
87// EncodeCrockford returns Crockford base32, which is never padded.
88func EncodeCrockford(src string) (string, error) { return encode(src, CrockfordAlphabet, false) }
89
90// 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 decode
97 case c >= '2' && c <= '7':
98 return int(c-'2') + 26
99 }
100 return -1
101}
102
103// 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, and
106// hyphens are skipped by the caller. This is what makes a Crockford identifier
107// survive being written down and typed back in.
108func crockfordValue(c byte) int {
109 if c >= 'a' && c <= 'z' {
110 c -= 32
111 }
112 switch c {
113 case 'O':
114 return 0
115 case 'I', 'L':
116 return 1
117 }
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 i
124 }
125 }
126 return -1
127}
128
129// 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 "", ErrTooLong
133 }
134 // strip padding and (for Crockford) hyphens
135 var clean strings.Builder
136 for i := 0; i < len(s); i++ {
137 c := s[i]
138 if c == '=' {
139 continue
140 }
141 if skipHyphen && c == '-' {
142 continue
143 }
144 clean.WriteByte(c)
145 }
146 in := clean.String()
147
148 var b strings.Builder
149 var acc uint64
150 bits := 0
151 for i := 0; i < len(in); i++ {
152 v := value(in[i])
153 if v < 0 {
154 return "", ErrCorrupt
155 }
156 acc = acc<<5 | uint64(v)
157 bits += 5
158 if bits >= 8 {
159 bits -= 8
160 b.WriteByte(byte(acc >> uint(bits)))
161 acc &= (1 << uint(bits)) - 1
162 }
163 }
164 // leftover bits must be zero padding, never data
165 if bits >= 5 || acc != 0 {
166 return "", ErrCorrupt
167 }
168 return b.String(), nil
169}
170
171// Decode parses RFC 4648 base32, tolerating lowercase and missing padding.
172func Decode(s string) (string, error) { return decode(s, stdValue, false) }
173
174// DecodeCrockford parses Crockford base32, folding case, reading I/L as 1 and
175// O as 0, and ignoring hyphens.
176func DecodeCrockford(s string) (string, error) { return decode(s, crockfordValue, true) }