Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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) }