Lowent Manual←↑→

codec — hex and base64

Source
lib/codec.low
Layer
L0 — pure computation (the caller’s buffer)
Capabilities
none

Turns bytes into hex or base64 text and back. Many places let only text through — a line of a configuration file, a log, a URL, a mail body, screen output. Putting raw bytes there lets zero and control bytes break the channel. Hex (one byte as two characters 0-9 a-f) and base64 (arbitrary bytes as 64 characters A-Z a-z 0-9 + /) are wrappings for carrying arbitrary bytes using only safe characters. Hex is easy to read but exactly doubles the size; base64 is hard to read but ends at about 4/3. Hex is usual for eyeballing hashes, base64 for carrying chunks.

use codec as c .

let n option usize c.hex_enc "abc" dst. . .
guard is_some n. . else return 1 . .

dst is not made by this module but is an output place the caller sets aside in advance (at least 6 bytes here). n is the number of bytes actually written, and the result is dst[0..n).

A wrapping keeps no secrets — anyone can undo it. If secrecy is needed, use aead, which seals (chapter 33). This module is pure computation from bytes to bytes, using only idx, set, shl, shr, bit_and, bit_or, guard and while, and no core op was added.

Design and boundaries#

opdst size neededSuccess returns
hex_enc2 × len srcsome (2 × len src)
hex_declen src / 2some (len src / 2)
b64_enc4 × ⌈len src / 3⌉some (4 × ⌈len src / 3⌉)
b64_dec3 × (len src / 4) − padding countsome <bytes written>

Table 50.1 — Output room needed

If dst is smaller than the formula the result is always none; if larger it does not matter — it writes from the front and returns the count. Empty input succeeds (hex_enc "", b64_enc "" and b64_dec "" all give some 0 .).

Ops at a glance#

opSignatureWhen it cannot
hex_digitfn (v u64) → u8never fails (assumes 0 … 15)
hex_valfn (c u8) → option u64none outside 0-9 a-f A-F
hex_encproc (src slice u8, dst mut slice u8) → option usizenone if dst too small
hex_decproc (src slice u8, dst mut slice u8) → option usizenone for odd length · non-hex characters · dst too small
b64_digitfn (v u64) → u8never fails (assumes 0 … 63)
b64_valfn (c u8) → option u64none outside the alphabet (= too)
b64_encproc (src slice u8, dst mut slice u8) → option usizenone if dst too small
b64_decproc (src slice u8, dst mut slice u8) → option usizenone if length not a multiple of 4 · outside the alphabet · misplaced padding · dst too small

Table 50.2 — Ops of codec

Usually only hex_enc, hex_dec, b64_enc and b64_dec are used; the other four are single-character parts.

Ops in detail#

Every conversion takes src (bytes to read) and dst (place to write) separately. There is no in-place conversion because encoding grows the result, and overwriting the original would trample bytes not yet read. The length of src is the whole input — no end marker is sought, only len src. . is trusted, so when passing a result held in a larger buffer back in, cut it exactly with subslice. The result length is not the length of dst but the n in the returned some n. ..

Using it#

Check buffer sizes first, convert, and cut by the returned count.

module ex_codec .

use codec as c .

proc hex_roundtrip input enc mut slice u8 . input dec mut slice u8 . output usize . effects none . do
  guard ge len enc. . 6 . else return 90 . .
  guard ge len dec. . 3 . else return 91 . .
  let en option usize c.hex_enc "abc" enc. . .
  guard is_some en. . else return 1 . .
  guard eq some_value en. . 6 . else return 2 . .
  rem the count written is the boundary --- passing all of enc makes trailing garbage input
  let dn option usize c.hex_dec subslice enc. 0 6 . dec. . .
  guard is_some dn. . else return 3 . .
  guard eq some_value dn. . 3 . else return 4 . .
  guard eq idx dec. 0 . 97 . else return 5 . .
  rem uppercase is accepted too: "4A" → 74
  let up option usize c.hex_dec "4A" dec. . .
  guard is_some up. . else return 6 . .
  guard eq idx dec. 0 . 74 . else return 7 . .
  return 42 .
end .

proc b64_roundtrip input enc mut slice u8 . input dec mut slice u8 . output usize . effects none . do
  rem 2 bytes make one group (4 characters) with one = at the end: "aGk="
  guard ge len enc. . 4 . else return 90 . .
  guard ge len dec. . 2 . else return 91 . .
  let e option usize c.b64_enc "hi" enc. . .
  guard is_some e. . else return 1 . .
  guard eq some_value e. . 4 . else return 2 . .
  let d option usize c.b64_dec subslice enc. 0 4 . dec. . .
  guard is_some d. . else return 3 . .
  guard eq some_value d. . 2 . else return 4 . .
  return 42 .
end .

Counter-examples#

None of these stop; all give none. The symptom is always “the result is none and guard falls to else”.

InputResultUsual cause, and dst
c.hex_dec "abc" dst. .noneOdd length — caught by the length check first, dst unchanged
c.hex_dec "zz" dst. .noneNon-hex character — found while scanning, so the front of dst may be overwritten
c.hex_enc "abcdefgh" dst3. .none16 needed in 3 bytes — not a byte is written
c.b64_dec "abc" dst. .noneLength not a multiple of 4 — often enc passed whole instead of cut with subslice
c.b64_dec "a?cd" dst. .noneOutside the alphabet — mostly URL-safe -, _ or newlines mixed in
c.b64_dec "aG==YWJj" dstnonePadding in the middle — what you get by simply concatenating two base64 strings

Table 50.3 — Inputs that commonly go wrong

Cautions#