lib.bend source
lib.bend on the hub · documented module
# Base64 — standard RFC 4648 Base64 over Base's List<&2, U32> and String.## This package intentionally stays Base-only. Input list elements are interpreted# as octets: only the low 8 bits of each U32 participate in encoding. Decode# returns canonical octets in List<&2, U32>. Invalid alphabet, padding, length,# or unused padding bits return None.import Base# The standard alphabet: A-Z, a-z, 0-9, +, /.def Base64.alphabet() -> String: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"def Base64.char(+n: U32) -> Char: # n is always 0..63 at call sites; the fallback is unreachable there. Maybe.default(&2, Char, String.get(Base64.alphabet(), U32.to_nat(n)), Chr{0})# Encode one complete 24-bit group.def Base64.encode.group(+a: U32, +b: U32, +c: U32) -> String: +a8 = U32.and(a, 255) +b8 = U32.and(b, 255) +c8 = U32.and(c, 255) +i0 = U32.shrn(a8, 2n) +i1 = U32.or(U32.shln(U32.and(a8, 3), 4n), U32.shrn(b8, 4n)) +i2 = U32.or(U32.shln(U32.and(b8, 15), 2n), U32.shrn(c8, 6n)) +i3 = U32.and(c8, 63) SCon{ Base64.char(i0), SCon{Base64.char(i1), SCon{Base64.char(i2), SCon{Base64.char(i3), SNil{}}}} }# Encode a final one-byte group (two alphabet chars and == padding).def Base64.encode.one(+a: U32) -> String: +a8 = U32.and(a, 255) +i0 = U32.shrn(a8, 2n) +i1 = U32.shln(U32.and(a8, 3), 4n) SCon{Base64.char(i0), SCon{Base64.char(i1), SCon{'=', SCon{'=', SNil{}}}}}# Encode a final two-byte group (three alphabet chars and = padding).def Base64.encode.two(+a: U32, +b: U32) -> String: +a8 = U32.and(a, 255) +b8 = U32.and(b, 255) +i0 = U32.shrn(a8, 2n) +i1 = U32.or(U32.shln(U32.and(a8, 3), 4n), U32.shrn(b8, 4n)) +i2 = U32.shln(U32.and(b8, 15), 2n) SCon{Base64.char(i0), SCon{Base64.char(i1), SCon{Base64.char(i2), SCon{'=', SNil{}}}}}# Encode octets. The result is padded to a multiple of four characters.def Base64.encode.go(xs: List<&2, U32>) -> String: match xs: case Nil{}: SNil{} case +a <> Nil{}: Base64.encode.one(a) case +a <> +b <> Nil{}: Base64.encode.two(a, b) case +a <> +b <> +c <> t: String.append(Base64.encode.group(a, b, c), Base64.encode.go(t))def Base64.encode(xs: List<&2, U32>) -> String: Base64.encode.go(xs)# Convert a standard Base64 alphabet character to its sextet value.def Base64.value(+c: Char) -> Maybe<&2, U32>: +x = Char.to_u32(c) Bool.pick(Maybe<&2, U32>, Bool.and(U32.is_ge(x, 65), U32.is_le(x, 90)), Some{U32.sub(x, 65)}, Bool.pick(Maybe<&2, U32>, Bool.and(U32.is_ge(x, 97), U32.is_le(x, 122)), Some{U32.add(26, U32.sub(x, 97))}, Bool.pick(Maybe<&2, U32>, Bool.and(U32.is_ge(x, 48), U32.is_le(x, 57)), Some{U32.add(52, U32.sub(x, 48))}, Bool.pick(Maybe<&2, U32>, Char.is_eq(c, '+'), Some{62}, Bool.pick(Maybe<&2, U32>, Char.is_eq(c, '/'), Some{63}, None{})))))# Prepend a decoded group to an already decoded tail, propagating failure.def Base64.decode.prefix( prefix: List<&2, U32>, m: Maybe<&2, List<&2, U32>>) -> Maybe<&2, List<&2, U32>>: match m: case None{}: None{} case Some{rest}: Some{List.append(&2, U32, prefix, rest)}# Padding is legal only on the final quartet.def Base64.decode.end( prefix: List<&2, U32>, r: Maybe<&2, List<&2, U32>>) -> Maybe<&2, List<&2, U32>>: match r: case Some{Nil{}}: Some{prefix} case _: None{}# Decode a quartet with no padding into three octets.def Base64.decode.full( r: Maybe<&2, List<&2, U32>>, ma: Maybe<&2, U32>, mb: Maybe<&2, U32>, mc: Maybe<&2, U32>, md: Maybe<&2, U32>) -> Maybe<&2, List<&2, U32>>: match ma mb mc md: case Some{+x0} Some{+x1} Some{+x2} Some{+x3}: +o0 = U32.or(U32.shln(x0, 2n), U32.shrn(x1, 4n)) +o1 = U32.or(U32.shln(U32.and(x1, 15), 4n), U32.shrn(x2, 2n)) +o2 = U32.or(U32.shln(U32.and(x2, 3), 6n), x3) Base64.decode.prefix(o0 <> o1 <> o2 <> Nil{}, r) case _ _ _ _: None{}# Decode a quartet ending in one '=' into two octets. RFC 4648 requires the# unused low four bits of the second sextet to be zero (canonical padding).def Base64.decode.one_pad( r: Maybe<&2, List<&2, U32>>, ma: Maybe<&2, U32>, mb: Maybe<&2, U32>, mc: Maybe<&2, U32>) -> Maybe<&2, List<&2, U32>>: match ma mb mc: case Some{+x0} Some{+x1} Some{+x2}: Bool.pick(Maybe<&2, List<&2, U32>>, U32.is_eq(U32.and(x2, 3), 0), Base64.decode.end( U32.or(U32.shln(x0, 2n), U32.shrn(x1, 4n)) <> U32.or(U32.shln(U32.and(x1, 15), 4n), U32.shrn(x2, 2n)) <> Nil{}, r), None{}) case _ _ _: None{}# Decode a quartet ending in '==' into one octet. Canonical padding requires# the unused low four bits of the second sextet to be zero.def Base64.decode.two_pad( r: Maybe<&2, List<&2, U32>>, ma: Maybe<&2, U32>, mb: Maybe<&2, U32>) -> Maybe<&2, List<&2, U32>>: match ma mb: case Some{+x0} Some{+x1}: Bool.pick(Maybe<&2, List<&2, U32>>, U32.is_eq(U32.and(x1, 15), 0), Base64.decode.end( U32.or(U32.shln(x0, 2n), U32.shrn(x1, 4n)) <> Nil{}, r), None{}) case _ _: None{}# Decode one quartet. Whitespace is not accepted; callers should normalize it# explicitly if a non-RFC transport requires whitespace tolerance.def Base64.decode.quartet( +a: Char, +b: Char, +c: Char, +d: Char, +r: Maybe<&2, List<&2, U32>>) -> Maybe<&2, List<&2, U32>>: +ma = Base64.value(a) +mb = Base64.value(b) +mc = Base64.value(c) +md = Base64.value(d) Bool.pick(Maybe<&2, List<&2, U32>>, Char.is_eq(c, '='), Bool.pick(Maybe<&2, List<&2, U32>>, Char.is_eq(d, '='), Base64.decode.two_pad(r, ma, mb), None{}), Bool.pick(Maybe<&2, List<&2, U32>>, Char.is_eq(d, '='), Base64.decode.one_pad(r, ma, mb, mc), Base64.decode.full(r, ma, mb, mc, md)))# Decode padded RFC 4648 Base64. Invalid input returns None.def Base64.decode.go(s: String) -> Maybe<&2, List<&2, U32>>: match s: case SNil{}: Some{Nil{}} case SCon{a, SCon{b, SCon{c, SCon{d, tail}}}}: +r = Base64.decode.go(tail) Base64.decode.quartet(a, b, c, d, r) case _: None{}def Base64.decode(s: String) -> Maybe<&2, List<&2, U32>>: Base64.decode.go(s)