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 u64 . be 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 index, set, shl, shr, bit_and, bit_or, guard and while, and no builtin 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 u64none if dst too small
hex_decproc (src slice u8, dst mut slice u8) → option u64none 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 u64none if dst too small
b64_decproc (src slice u8, dst mut slice u8) → option u64none 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 u64 . effects none . do
  guard ge (len enc) 6 . else return 90 .
  guard ge (len dec) 3 . else return 91 .
  let en option u64 . be 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 u64 . be 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 (index dec 0) 97 . else return 5 .
  rem uppercase is accepted too: "4A" → 74
  let up option u64 . be c.hex_dec "4A" dec .
  guard is_some up . else return 6 .
  guard eq (index dec 0) 74 . else return 7 .
  return 42 .
end

proc b64_roundtrip input enc mut slice u8 . . input dec mut slice u8 . . output u64 . 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 u64 . be c.b64_enc "hi" enc .
  guard is_some e . else return 1 .
  guard eq (some_value e) 4 . else return 2 .
  let d option u64 . be 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" dstnoneOdd length — caught by the length check first, dst unchanged
c.hex_dec "zz" dstnoneNon-hex character — found while scanning, so the front of dst may be overwritten
c.hex_enc "abcdefgh" dst3none16 needed in 3 bytes — not a byte is written
c.b64_dec "abc" dstnoneLength not a multiple of 4 — often enc passed whole instead of cut with subslice
c.b64_dec "a?cd" dstnoneOutside 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#