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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

base32.gno

4.81 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/nym-moultest123/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) }