<!-- canonical: https://efficientnewlanguage.org/eml-p/projects/P088-codec-workbench/ | updated: 2026-10-15 -->

# P088 Codec workbench

Text as bytes, and bytes as hex, Base64 and Base32 (RFC 4648) both ways, with padding and the bits of one Base64 group shown; decoding is strict, accepting only canonical encodings and saying why anything else is refused.

EML-P project `projects/codec-workbench` in the EML language repo: 2 module(s), entry `main.eml`, terminal UI. There, `eml project run projects/codec-workbench` runs it and `eml project verify projects/codec-workbench` replays every session under CPython (two hash seeds) and in the interpreter; the site build replays every session in the interpreter again and publishes a session only if its screen matches.

Built on verified corpus cases: base32-codec-padding (https://efficientnewlanguage.org/cases/200-base32-codec-padding/).

## Sessions

### bad-input - interpreter: byte-equal to the golden

Input:

```text
0
x
1
say "hi"
1
a\b
1
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
2
abc
zz

3
TWE
TW=E
TWF=
TW!=

4
JBSWY3D=
JBSWY3==
jbswy3dp

5
```

Screen:

```text
== Codec workbench ==
Text as bytes, and bytes as hex, Base64 and Base32 - both ways, strictly.

1) encode text  2) decode hex  3) decode Base64  4) decode Base32  5) quit
choice> 0
Pick a number from 1 to 5.

1) encode text  2) decode hex  3) decode Base64  4) decode Base32  5) quit
choice> x
Pick a number from 1 to 5.

1) encode text  2) decode hex  3) decode Base64  4) decode Base32  5) quit
choice> 1
text (up to 60 printable characters, no double quote or backslash)> say "hi"
That has a character this program does not handle, or is over 60.

1) encode text  2) decode hex  3) decode Base64  4) decode Base32  5) quit
choice> 1
text (up to 60 printable characters, no double quote or backslash)> a\b
That has a character this program does not handle, or is over 60.

1) encode text  2) decode hex  3) decode Base64  4) decode Base32  5) quit
choice> 1
text (up to 60 printable characters, no double quote or backslash)> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
That has a character this program does not handle, or is over 60.

1) encode text  2) decode hex  3) decode Base64  4) decode Base32  5) quit
choice> 2
hex to decode (empty to stop)> abc
Not valid hex: hex needs two digits a byte, so an even number of them.
hex to decode (empty to stop)> zz
Not valid hex: it has a character that is not a hex digit.
hex to decode (empty to stop)> 
Cancelled.

1) encode text  2) decode hex  3) decode Base64  4) decode Base32  5) quit
choice> 3
Base64 to decode (empty to stop)> TWE
Not valid Base64: Base64 comes in groups of 4 characters.
Base64 to decode (empty to stop)> TW=E
Not valid Base64: = can only end the last group, and at most two of them.
Base64 to decode (empty to stop)> TWF=
Not valid Base64: the bits the padding leaves over are not 0.
Base64 to decode (empty to stop)> TW!=
Not valid Base64: it has a character outside the Base64 alphabet.
Base64 to decode (empty to stop)> 
Cancelled.

1) encode text  2) decode hex  3) decode Base64  4) decode Base32  5) quit
choice> 4
Base32 to decode (empty to stop)> JBSWY3D=
Not valid Base32: the bits the padding leaves over are not 0.
Base32 to decode (empty to stop)> JBSWY3==
Not valid Base32: a Base32 group can only end in 0, 1, 3, 4 or 6 =.
Base32 to decode (empty to stop)> jbswy3dp
Not valid Base32: it has a character outside the Base32 alphabet.
Base32 to decode (empty to stop)> 
Cancelled.

1) encode text  2) decode hex  3) decode Base64  4) decode Base32  5) quit
choice> 5
Bye.
```

### basic - interpreter: byte-equal to the golden

Input:

```text
1
Man
1
Hello, EML!
1
Ma
3
SGVsbG8sIEVNTCE=
4
JBSWY3DPFQQEKTKMEE======
2
48656C6C6F
3
AP8=
5
```

Screen:

```text
== Codec workbench ==
Text as bytes, and bytes as hex, Base64 and Base32 - both ways, strictly.

1) encode text  2) decode hex  3) decode Base64  4) decode Base32  5) quit
choice> 1
text (up to 60 printable characters, no double quote or backslash)> Man
hex:    4d616e
Base64: TWFu
Base32: JVQW4===
The first Base64 group, bit by bit:
  bytes 01001101 01100001 01101110
  6-bit 010011 010110 000101 101110
  as        T     W     F     u
The bytes come in whole threes, so Base64 needs no padding.

1) encode text  2) decode hex  3) decode Base64  4) decode Base32  5) quit
choice> 1
text (up to 60 printable characters, no double quote or backslash)> Hello, EML!
hex:    48656c6c6f2c20454d4c21
Base64: SGVsbG8sIEVNTCE=
Base32: JBSWY3DPFQQEKTKMEE======
The first Base64 group, bit by bit:
  bytes 01001000 01100101 01101100
  6-bit 010010 000110 010101 101100
  as        S     G     V     s
Two bytes are left at the end: 16 bits make 3 characters, and = pads the group to 4.

1) encode text  2) decode hex  3) decode Base64  4) decode Base32  5) quit
choice> 1
text (up to 60 printable characters, no double quote or backslash)> Ma
hex:    4d61
Base64: TWE=
Base32: JVQQ====
The first Base64 group, bit by bit:
  bytes 01001101 01100001
  6-bit 010011 010110 000100
  as        T     W     E
Two bytes are left at the end: 16 bits make 3 characters, and = pads the group to 4.

1) encode text  2) decode hex  3) decode Base64  4) decode Base32  5) quit
choice> 3
Base64 to decode (empty to stop)> SGVsbG8sIEVNTCE=
bytes: 11 (hex 48656c6c6f2c20454d4c21)
text:  Hello, EML!

1) encode text  2) decode hex  3) decode Base64  4) decode Base32  5) quit
choice> 4
Base32 to decode (empty to stop)> JBSWY3DPFQQEKTKMEE======
bytes: 11 (hex 48656c6c6f2c20454d4c21)
text:  Hello, EML!

1) encode text  2) decode hex  3) decode Base64  4) decode Base32  5) quit
choice> 2
hex to decode (empty to stop)> 48656C6C6F
bytes: 5 (hex 48656c6c6f)
text:  Hello

1) encode text  2) decode hex  3) decode Base64  4) decode Base32  5) quit
choice> 3
Base64 to decode (empty to stop)> AP8=
bytes: 2 (hex 00ff)
text: not all of them are printable characters this program shows

1) encode text  2) decode hex  3) decode Base64  4) decode Base32  5) quit
choice> 5
Bye.
```

## Modules

### main.eml

```eml
# P088 codec workbench: text to hex, Base64 and Base32, the bits behind
# one Base64 group, and decoding each of them back - strictly, with the
# reason when a string is not a valid encoding.
import codec

def trim(s):
    0 => i
    len(s) => j
    while i < j and s[i] == " ":
        i + 1 => i
    while j > i and s[j - 1] == " ":
        j - 1 => j
    return s[i:j]

def show_bytes(bs):
    codec.text_of(bs) => t
    ("bytes: " + str(len(bs)) + " (hex " + codec.hex_encode(bs) + ")") ^0
    if len(bs) == 0:
        "text: (empty)" ^0
    elif t == "":
        "text: not all of them are printable characters this program shows" ^0
    else:
        ("text:  " + t) ^0

def picture(bs):
    # The first group of three bytes as bits, cut into 6-bit pieces.
    0 => n
    "" => b8
    for k in [0:2]:
        0 => b
        if k < len(bs):
            bs[k] => b
        n * 256 + b => n
        if k < len(bs):
            if b8 != "":
                b8 + " " => b8
            b8 + codec.bits(b, 8) => b8
    ("  bytes " + b8) ^0
    "" => b6
    "" => cs
    [262144, 4096, 64, 1] => p
    len(bs) => left
    if left > 3:
        3 => left
    for k in [0:left]:
        codec.quotient(n, p[k]) % 64 => v
        if b6 != "":
            b6 + " " => b6
        b6 + codec.bits(v, 6) => b6
        cs + "     " + codec.B64[v] => cs
    ("  6-bit " + b6) ^0
    ("  as   " + cs) ^0

def encode():
    trim(input("text (up to 60 printable characters, no double quote or backslash)> ")) => s
    if s == "":
        "Cancelled." ^0
        return
    codec.bytes_of(s) => bs
    if len(s) > 60 or (len(bs) == 1 and bs[0] == -1):
        "That has a character this program does not handle, or is over 60." ^0
        return
    ("hex:    " + codec.hex_encode(bs)) ^0
    ("Base64: " + codec.b64_encode(bs)) ^0
    ("Base32: " + codec.b32_encode(bs)) ^0
    "The first Base64 group, bit by bit:" ^0
    picture(bs)
    len(bs) % 3 => r3
    if r3 == 1:
        "One byte is left at the end: 8 bits make 2 characters, and == pads the group to 4." ^0
    elif r3 == 2:
        "Two bytes are left at the end: 16 bits make 3 characters, and = pads the group to 4." ^0
    else:
        "The bytes come in whole threes, so Base64 needs no padding." ^0

def decode(which):
    while True:
        trim(input(which + " to decode (empty to stop)> ")) => s
        if s == "":
            "Cancelled." ^0
            return
        if which == "hex":
            codec.hex_decode(s) => r
        elif which == "Base64":
            codec.b64_decode(s) => r
        else:
            codec.b32_decode(s) => r
        if len(r) == 1:
            show_bytes(r[0])
            return
        ("Not valid " + which + ": " + r[1] + ".") ^0

"== Codec workbench ==" ^0
"Text as bytes, and bytes as hex, Base64 and Base32 - both ways, strictly." ^0
True => running
while running:
    "" ^0
    "1) encode text  2) decode hex  3) decode Base64  4) decode Base32  5) quit" ^0
    trim(input("choice> ")) => choice
    if choice == "1":
        encode()
    elif choice == "2":
        decode("hex")
    elif choice == "3":
        decode("Base64")
    elif choice == "4":
        decode("Base32")
    elif choice == "5":
        False => running
    else:
        "Pick a number from 1 to 5." ^0
"Bye." ^0
```

Python projection of main.eml:

```python
import codec

def trim(s):
    i = 0
    j = len(s)
    while i < j and s[i] == " ":
        i = i + 1
    while j > i and s[j - 1] == " ":
        j = j - 1
    return s[i:j]

def show_bytes(bs):
    t = codec.text_of(bs)
    print("bytes: " + str(len(bs)) + " (hex " + codec.hex_encode(bs) + ")")
    if len(bs) == 0:
        print("text: (empty)")
    elif t == "":
        print("text: not all of them are printable characters this program shows")
    else:
        print("text:  " + t)

def picture(bs):
    n = 0
    b8 = ""
    for k in range(0, 3):
        b = 0
        if k < len(bs):
            b = bs[k]
        n = n * 256 + b
        if k < len(bs):
            if b8 != "":
                b8 = b8 + " "
            b8 = b8 + codec.bits(b, 8)
    print("  bytes " + b8)
    b6 = ""
    cs = ""
    p = [262144, 4096, 64, 1]
    left = len(bs)
    if left > 3:
        left = 3
    for k in range(0, left+1):
        v = codec.quotient(n, p[k]) % 64
        if b6 != "":
            b6 = b6 + " "
        b6 = b6 + codec.bits(v, 6)
        cs = cs + "     " + codec.B64[v]
    print("  6-bit " + b6)
    print("  as   " + cs)

def encode():
    s = trim(input("text (up to 60 printable characters, no double quote or backslash)> "))
    if s == "":
        print("Cancelled.")
        return
    bs = codec.bytes_of(s)
    if len(s) > 60 or len(bs) == 1 and bs[0] == -1:
        print("That has a character this program does not handle, or is over 60.")
        return
    print("hex:    " + codec.hex_encode(bs))
    print("Base64: " + codec.b64_encode(bs))
    print("Base32: " + codec.b32_encode(bs))
    print("The first Base64 group, bit by bit:")
    picture(bs)
    r3 = len(bs) % 3
    if r3 == 1:
        print("One byte is left at the end: 8 bits make 2 characters, and == pads the group to 4.")
    elif r3 == 2:
        print("Two bytes are left at the end: 16 bits make 3 characters, and = pads the group to 4.")
    else:
        print("The bytes come in whole threes, so Base64 needs no padding.")

def decode(which):
    while True:
        s = trim(input(which + " to decode (empty to stop)> "))
        if s == "":
            print("Cancelled.")
            return
        if which == "hex":
            r = codec.hex_decode(s)
        elif which == "Base64":
            r = codec.b64_decode(s)
        else:
            r = codec.b32_decode(s)
        if len(r) == 1:
            show_bytes(r[0])
            return
        print("Not valid " + which + ": " + r[1] + ".")

print("== Codec workbench ==")
print("Text as bytes, and bytes as hex, Base64 and Base32 - both ways, strictly.")
running = True
while running:
    print("")
    print("1) encode text  2) decode hex  3) decode Base64  4) decode Base32  5) quit")
    choice = trim(input("choice> "))
    if choice == "1":
        encode()
    elif choice == "2":
        decode("hex")
    elif choice == "3":
        decode("Base64")
    elif choice == "4":
        decode("Base32")
    elif choice == "5":
        running = False
    else:
        print("Pick a number from 1 to 5.")
print("Bye.")
```

### codec.eml

```eml
# P088 codec workbench - text as bytes, and bytes as hex, Base64 and Base32
# (RFC 4648), both ways. Bits are worked with plain arithmetic: a group of
# bytes becomes one whole number (n x 256 + byte ...), and its 6- or 5-bit
# pieces come out by division by powers of two. The numbers stay below
# 2^40, far under 2^53, so the float division used is exact.

" !" => PART1
"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[" => PART2
"]^_`abcdefghijklmnopqrstuvwxyz{|}~" => PART3
"0123456789abcdef" => HEX
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" => B64
"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567" => B32

def index_in(s, c):
    for i in [0:len(s) - 1]:
        if s[i] == c:
            return i
    return -1

def code_of(c):
    # The ASCII code of a printable character other than " and \, or -1.
    index_in(PART1, c) => i
    if i >= 0:
        return 32 + i
    index_in(PART2, c) => i
    if i >= 0:
        return 35 + i
    index_in(PART3, c) => i
    if i >= 0:
        return 93 + i
    return -1

def char_of(n):
    # The character for an ASCII code, or "" if this program does not show it.
    if n >= 32 and n <= 33:
        return PART1[n - 32]
    if n >= 35 and n <= 91:
        return PART2[n - 35]
    if n >= 93 and n <= 126:
        return PART3[n - 93]
    return ""

def bytes_of(text):
    [] => out
    for c in text:
        code_of(c) => n
        if n < 0:
            return [-1]
        out + [n] => out
    return out

def text_of(bs):
    # The bytes as text, or "" if any is not a printable character here.
    "" => s
    for b in bs:
        char_of(b) => c
        if c == "":
            return ""
        s + c => s
    return s

def quotient(a, b):
    return int((a - a % b) / b)

def hex_encode(bs):
    "" => s
    for b in bs:
        s + HEX[quotient(b, 16)] + HEX[b % 16] => s
    return s

def hex_decode(s):
    # [bytes] or [[], why]. Upper or lower case digits.
    "0123456789ABCDEF" => upper
    if len(s) % 2 != 0:
        return [[], "hex needs two digits a byte, so an even number of them"]
    [] => out
    0 => i
    while i < len(s):
        index_in(HEX, s[i]) => a
        if a < 0:
            index_in(upper, s[i]) => a
        index_in(HEX, s[i + 1]) => b
        if b < 0:
            index_in(upper, s[i + 1]) => b
        if a < 0 or b < 0:
            return [[], "it has a character that is not a hex digit"]
        out + [a * 16 + b] => out
        i + 2 => i
    return [out]

def b64_encode(bs):
    # Three bytes make 24 bits make four 6-bit characters. A last group of
    # 1 or 2 bytes gives 2 or 3 characters, padded with = to four.
    "" => s
    0 => i
    while i < len(bs):
        len(bs) - i => left
        if left > 3:
            3 => left
        0 => n
        for k in [0:2]:
            0 => b
            if k < left:
                bs[i + k] => b
            n * 256 + b => n
        [262144, 4096, 64, 1] => p
        for k in [0:3]:
            if k <= left:
                s + B64[quotient(n, p[k]) % 64] => s
            else:
                s + "=" => s
        i + 3 => i
    return s

def b64_decode(s):
    # [bytes] or [[], why]. Strict: a multiple of 4 characters, = only at
    # the end and at most two, and the bits the padding leaves over are 0 -
    # so every byte string has exactly one encoding.
    if len(s) % 4 != 0:
        return [[], "Base64 comes in groups of 4 characters"]
    [] => out
    0 => i
    while i < len(s):
        0 => n
        0 => pad
        for k in [0:3]:
            s[i + k] => c
            if c == "=":
                if i + 4 < len(s) or k < 2:
                    return [[], "= can only end the last group, and at most two of them"]
                pad + 1 => pad
                n * 64 => n
            else:
                if pad > 0:
                    return [[], "= can only end the last group, and at most two of them"]
                index_in(B64, c) => v
                if v < 0:
                    return [[], "it has a character outside the Base64 alphabet"]
                n * 64 + v => n
        if pad == 1 and n % 256 != 0:
            return [[], "the bits the padding leaves over are not 0"]
        if pad == 2 and n % 65536 != 0:
            return [[], "the bits the padding leaves over are not 0"]
        [65536, 256, 1] => p
        for k in [0:2 - pad]:
            out + [quotient(n, p[k]) % 256] => out
        i + 4 => i
    return [out]

def b32_encode(bs):
    # The corpus case base32-codec-padding: five bytes make 40 bits make
    # eight 5-bit characters. A last group of 1, 2, 3 or 4 bytes gives 2, 4,
    # 5 or 7 characters and 6, 4, 3 or 1 =.
    [0, 2, 4, 5, 7, 8] => chars
    "" => s
    0 => i
    while i < len(bs):
        len(bs) - i => left
        if left > 5:
            5 => left
        0 => n
        for k in [0:4]:
            0 => b
            if k < left:
                bs[i + k] => b
            n * 256 + b => n
        34359738368 => p
        for k in [0:7]:
            if k < chars[left]:
                s + B32[quotient(n, p) % 32] => s
            else:
                s + "=" => s
            quotient(p, 32) => p
        i + 5 => i
    return s

def b32_decode(s):
    # [bytes] or [[], why]. Strict like Base64: groups of 8, padding only as
    # 6, 4, 3 or 1 at the very end, and the leftover bits 0.
    if len(s) % 8 != 0:
        return [[], "Base32 comes in groups of 8 characters"]
    [] => out
    0 => i
    while i < len(s):
        0 => n
        0 => pad
        for k in [0:7]:
            s[i + k] => c
            if c == "=":
                if i + 8 < len(s):
                    return [[], "= can only end the last group"]
                pad + 1 => pad
                n * 32 => n
            else:
                if pad > 0:
                    return [[], "= can only end the last group"]
                index_in(B32, c) => v
                if v < 0:
                    return [[], "it has a character outside the Base32 alphabet"]
                n * 32 + v => n
        # padding 0, 1, 3, 4, 6 leaves 5, 4, 3, 2, 1 bytes
        [5, 4, -1, 3, 2, -1, 1] => kept
        if pad > 6 or kept[pad] == -1:
            return [[], "a Base32 group can only end in 0, 1, 3, 4 or 6 ="]
        kept[pad] => nb
        1 => drop
        for k in [1:40 - 8 * nb]:
            drop * 2 => drop
        if n % drop != 0:
            return [[], "the bits the padding leaves over are not 0"]
        4294967296 => p
        for k in [1:nb]:
            out + [quotient(n, p) % 256] => out
            quotient(p, 256) => p
        i + 8 => i
    return [out]

def bits(n, width):
    # n as `width` binary digits.
    "" => s
    for k in [1:width]:
        str(n % 2) + s => s
        quotient(n, 2) => n
    return s
```

Python projection of codec.eml:

```python
PART1 = " !"
PART2 = "#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ["
PART3 = "]^_`abcdefghijklmnopqrstuvwxyz{|}~"
HEX = "0123456789abcdef"
B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
B32 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"

def index_in(s, c):
    for i in range(0, len(s)):
        if s[i] == c:
            return i
    return -1

def code_of(c):
    i = index_in(PART1, c)
    if i >= 0:
        return 32 + i
    i = index_in(PART2, c)
    if i >= 0:
        return 35 + i
    i = index_in(PART3, c)
    if i >= 0:
        return 93 + i
    return -1

def char_of(n):
    if n >= 32 and n <= 33:
        return PART1[n - 32]
    if n >= 35 and n <= 91:
        return PART2[n - 35]
    if n >= 93 and n <= 126:
        return PART3[n - 93]
    return ""

def bytes_of(text):
    out = []
    for c in text:
        n = code_of(c)
        if n < 0:
            return [-1]
        out = out + [n]
    return out

def text_of(bs):
    s = ""
    for b in bs:
        c = char_of(b)
        if c == "":
            return ""
        s = s + c
    return s

def quotient(a, b):
    return int((a - a % b) / b)

def hex_encode(bs):
    s = ""
    for b in bs:
        s = s + HEX[quotient(b, 16)] + HEX[b % 16]
    return s

def hex_decode(s):
    upper = "0123456789ABCDEF"
    if len(s) % 2 != 0:
        return [[], "hex needs two digits a byte, so an even number of them"]
    out = []
    i = 0
    while i < len(s):
        a = index_in(HEX, s[i])
        if a < 0:
            a = index_in(upper, s[i])
        b = index_in(HEX, s[i + 1])
        if b < 0:
            b = index_in(upper, s[i + 1])
        if a < 0 or b < 0:
            return [[], "it has a character that is not a hex digit"]
        out = out + [a * 16 + b]
        i = i + 2
    return [out]

def b64_encode(bs):
    s = ""
    i = 0
    while i < len(bs):
        left = len(bs) - i
        if left > 3:
            left = 3
        n = 0
        for k in range(0, 3):
            b = 0
            if k < left:
                b = bs[i + k]
            n = n * 256 + b
        p = [262144, 4096, 64, 1]
        for k in range(0, 4):
            if k <= left:
                s = s + B64[quotient(n, p[k]) % 64]
            else:
                s = s + "="
        i = i + 3
    return s

def b64_decode(s):
    if len(s) % 4 != 0:
        return [[], "Base64 comes in groups of 4 characters"]
    out = []
    i = 0
    while i < len(s):
        n = 0
        pad = 0
        for k in range(0, 4):
            c = s[i + k]
            if c == "=":
                if i + 4 < len(s) or k < 2:
                    return [[], "= can only end the last group, and at most two of them"]
                pad = pad + 1
                n = n * 64
            else:
                if pad > 0:
                    return [[], "= can only end the last group, and at most two of them"]
                v = index_in(B64, c)
                if v < 0:
                    return [[], "it has a character outside the Base64 alphabet"]
                n = n * 64 + v
        if pad == 1 and n % 256 != 0:
            return [[], "the bits the padding leaves over are not 0"]
        if pad == 2 and n % 65536 != 0:
            return [[], "the bits the padding leaves over are not 0"]
        p = [65536, 256, 1]
        for k in range(0, 2 - pad+1):
            out = out + [quotient(n, p[k]) % 256]
        i = i + 4
    return [out]

def b32_encode(bs):
    chars = [0, 2, 4, 5, 7, 8]
    s = ""
    i = 0
    while i < len(bs):
        left = len(bs) - i
        if left > 5:
            left = 5
        n = 0
        for k in range(0, 5):
            b = 0
            if k < left:
                b = bs[i + k]
            n = n * 256 + b
        p = 34359738368
        for k in range(0, 8):
            if k < chars[left]:
                s = s + B32[quotient(n, p) % 32]
            else:
                s = s + "="
            p = quotient(p, 32)
        i = i + 5
    return s

def b32_decode(s):
    if len(s) % 8 != 0:
        return [[], "Base32 comes in groups of 8 characters"]
    out = []
    i = 0
    while i < len(s):
        n = 0
        pad = 0
        for k in range(0, 8):
            c = s[i + k]
            if c == "=":
                if i + 8 < len(s):
                    return [[], "= can only end the last group"]
                pad = pad + 1
                n = n * 32
            else:
                if pad > 0:
                    return [[], "= can only end the last group"]
                v = index_in(B32, c)
                if v < 0:
                    return [[], "it has a character outside the Base32 alphabet"]
                n = n * 32 + v
        kept = [5, 4, -1, 3, 2, -1, 1]
        if pad > 6 or kept[pad] == -1:
            return [[], "a Base32 group can only end in 0, 1, 3, 4 or 6 ="]
        nb = kept[pad]
        drop = 1
        for k in range(1, 40 - 8 * nb+1):
            drop = drop * 2
        if n % drop != 0:
            return [[], "the bits the padding leaves over are not 0"]
        p = 4294967296
        for k in range(1, nb+1):
            out = out + [quotient(n, p) % 256]
            p = quotient(p, 256)
        i = i + 8
    return [out]

def bits(n, width):
    s = ""
    for k in range(1, width+1):
        s = str(n % 2) + s
        n = quotient(n, 2)
    return s
```

## README

# P088 - Codec workbench

Text as bytes, and bytes as hex, Base64 and Base32 (RFC 4648), both ways.
Encoding shows the padding and the bits of the first Base64 group.
Decoding is strict: it accepts only canonical encodings, and says why it
refuses anything else.

- `main.eml` - the menu, reading text and encodings, and the reports
- `codec.eml` - characters and their codes, hex, Base64 and Base32 both
  ways, and binary digits

How each part works:

- Characters: the printable ASCII characters except the double quote and
  the backslash (which would need escaping in the program's own text) map
  to their codes, 32 to 126. Bytes that are not among them are shown as
  hex only.
- Bits without bit operations: a group of bytes becomes one whole number
  (n x 256 + byte, again and again), and its 6- or 5-bit pieces come out
  by dividing by powers of two. The numbers stay below 2^40.
- Base64: three bytes are 24 bits, which make four 6-bit characters. A
  last group of one or two bytes makes two or three characters, padded
  with = to four.
- Base32, as in the corpus case `base32-codec-padding`: five bytes are 40
  bits, eight 5-bit characters. A last group of 1, 2, 3 or 4 bytes makes 2,
  4, 5 or 7 characters, with 6, 4, 3 or 1 =.
- Strict decoding: the length must be whole groups, = may only end the last
  group (in Base32 only as 6, 4, 3 or 1 of them), and the bits the padding
  leaves over must be 0. "TWF=" would read as "Ma" with two stray bits, so
  it is refused. Every byte string then has exactly one accepted encoding.

What is checked: menu choices 1 to 5; text of up to 60 characters from the
set above; hex with an even number of digits, in either case. An empty
answer cancels.

Sessions: `sessions/basic.in` gives:
- "Man" (TWFu, no padding, its bits shown), "Hello, EML!" and "Ma" (TWE=,
  JVQQ====);
- "Hello, EML!" decoded back from Base64 and from Base32, "Hello" from
  upper-case hex, and AP8=, two bytes that are not printable.

`sessions/bad-input.in` gives:
- menu choices 0 and x;
- text with a double quote, with a backslash, and of 61 characters;
- hex "abc" and "zz";
- Base64 "TWE", "TW=E", "TWF=" (stray bits) and "TW!=";
- Base32 "JBSWY3D=" (stray bits), "JBSWY3==" (two = cannot end a group)
  and lower-case "jbswy3dp".

Built on the verified corpus case `base32-codec-padding` (the padding
table, and the round trip at every length).
