plum

#treesitter#compiler#wasm

git clone https://git.pyrossh.dev/plum

A statically typed, imperative programming language inspired by rust, python


plum-std/Base64.plum
141de54 1
module std
b7071c9 2
import std/Str
b7071c9 3
import std/Buffer
b7071c9 4
import std/Option
73b5e55 5
import std/Bool
ca5fd6f 6
import std/Number
141de54 7
141de54 8
# The Base64 package contains support for doing Base64 binary-to-text encodings.
141de54 9
#
a271f34 10
# Implemented on top of `[]Byte`/`Buffer` (`libs/std/bytes.plum`/`buffer.plum`)
a271f34 11
# — the original note here said a real bit-twiddling implementation couldn't
a271f34 12
# be expressed without indexed byte-buffer mutation, which didn't exist in
a271f34 13
# this language yet at the time it was written; it does now.
a271f34 14
#
a271f34 15
# `data`/the decoded payload are represented as `Str` throughout (matching
a271f34 16
# the original public shape) rather than `[]Byte` — `Str` is itself just a
a271f34 17
# byte array here (see `libs/std/str.plum`), so this is a plain byte string,
a271f34 18
# not a claim that the content is valid UTF-8 text.
a271f34 19
a271f34 20
# Encode for PEM (RFC 1421): 64-character lines.
0fe3528 21
fun encodePEM(data: Str) -> Str =
a271f34 22
  return encode(data, "+", "/", "=", 64, "\r\n")
141de54 23
a271f34 24
# Encode for MIME (RFC 2045): 76-character lines.
0fe3528 25
fun encodeMIME(data: Str) -> Str =
a271f34 26
  return encode(data, "+", "/", "=", 76, "\r\n")
141de54 27
141de54 28
# Encode for URLs (RFC 4648). Padding characters are stripped by default.
0fe3528 29
fun encodeURL(data: Str, pad: Bool = False) -> Str =
a271f34 30
  return encode(data, "-", "_", pad ? "=" : "", 0, "")
141de54 31
a271f34 32
# Configurable encoding. The defaults are for RFC 4648. `pad` is the padding
a271f34 33
# character, or `""` to omit padding entirely; `linelen` is the output line
a271f34 34
# length to wrap at (`0` disables wrapping), separated by `linesep`.
0fe3528 35
fun encode(data: Str, at62: Str = "+", at63: Str = "/", pad: Str = "=", linelen: Int = 0, linesep: Str = "\r\n") -> Str =
a271f34 36
  at62b := at62.byteAt(0)
a271f34 37
  at63b := at63.byteAt(0)
a271f34 38
  has_pad := pad.length() > 0
a271f34 39
  padb := has_pad ? pad.byteAt(0) : 0
4a2384c 40
  res := Buffer()
a271f34 41
  n := data.length()
a271f34 42
  i := 0
a271f34 43
  line_chars := 0
a271f34 44
  while i < n
a271f34 45
    b0 := data.byteAt(i)
a271f34 46
    has1 := i + 1 < n
a271f34 47
    has2 := i + 2 < n
a271f34 48
    b1 := has1 ? data.byteAt(i + 1) : 0
a271f34 49
    b2 := has2 ? data.byteAt(i + 2) : 0
a271f34 50
    res.writeByte(Byte(encodeByte(b0 >> 2, at62b, at63b)))
a271f34 51
    res.writeByte(Byte(encodeByte({b0 & 0x03} << 4 | b1 >> 4, at62b, at63b)))
a271f34 52
    line_chars = line_chars + 2
a271f34 53
    if has1
a271f34 54
      res.writeByte(Byte(encodeByte({b1 & 0x0F} << 2 | b2 >> 6, at62b, at63b)))
a271f34 55
      line_chars = line_chars + 1
a271f34 56
    else if has_pad
a271f34 57
      res.writeByte(Byte(padb))
a271f34 58
      line_chars = line_chars + 1
a271f34 59
    if has2
a271f34 60
      res.writeByte(Byte(encodeByte(b2 & 0x3F, at62b, at63b)))
a271f34 61
      line_chars = line_chars + 1
a271f34 62
    else if has_pad
a271f34 63
      res.writeByte(Byte(padb))
a271f34 64
      line_chars = line_chars + 1
a271f34 65
    i = i + 3
a271f34 66
    if linelen > 0 && line_chars >= linelen && i < n
a271f34 67
      res.write(linesep)
a271f34 68
      line_chars = 0
a271f34 69
  return res.toStr()
a271f34 70
a271f34 71
# Encodes a single 6-bit value (0-63) as its base64 alphabet character.
a271f34 72
fun encodeByte(i: Int, at62: Int, at63: Int) -> Int =
a271f34 73
  if i < 26
a271f34 74
    return 65 + i # 'A'..'Z'
a271f34 75
  if i < 52
a271f34 76
    return 97 + {i - 26} # 'a'..'z'
a271f34 77
  if i < 62
a271f34 78
    return 48 + {i - 52} # '0'..'9'
a271f34 79
  if i == 62
a271f34 80
    return at62
a271f34 81
  return at63
a271f34 82
a271f34 83
# The inverse of `encodeByte`: a base64 alphabet character's 6-bit value, or
a271f34 84
# -1 if `b` isn't one of the 64 alphabet characters (whitespace/padding are
a271f34 85
# handled by the caller, not here).
a271f34 86
fun decodeByte(b: Int, at62: Int, at63: Int) -> Int =
a271f34 87
  if b >= 65 && b <= 90
a271f34 88
    return b - 65
a271f34 89
  if b >= 97 && b <= 122
a271f34 90
    return b - 97 + 26
a271f34 91
  if b >= 48 && b <= 57
a271f34 92
    return b - 48 + 52
a271f34 93
  if b == at62
a271f34 94
    return 62
a271f34 95
  if b == at63
a271f34 96
    return 63
a271f34 97
  return -1
a271f34 98
a271f34 99
fun isBase64Whitespace(b: Int) -> Bool =
a271f34 100
  b == 32 || b == 9 || b == 10 || b == 13
141de54 101
141de54 102
# Decode for URLs (RFC 4648).
a271f34 103
fun decodeUrl(data: Str) -> Option[Str] =
a271f34 104
  return decode(data, "-", "_", "=")
141de54 105
141de54 106
# Configurable decoding. The defaults are for RFC 4648. Missing padding is
141de54 107
# not an error. Non-base64 data, other than whitespace (which can appear at
a271f34 108
# any point), is an error (`None`).
a271f34 109
fun decode(data: Str, at62: Str = "+", at63: Str = "/", pad: Str = "=") -> Option[Str] =
a271f34 110
  at62b := at62.byteAt(0)
a271f34 111
  at63b := at63.byteAt(0)
a271f34 112
  has_pad := pad.length() > 0
a271f34 113
  padb := has_pad ? pad.byteAt(0) : -1
4a2384c 114
  res := Buffer()
a271f34 115
  n := data.length()
a271f34 116
  i := 0
a271f34 117
  bits := 0
a271f34 118
  nbits := 0
a271f34 119
  while i < n
a271f34 120
    b := data.byteAt(i)
a271f34 121
    i = i + 1
a271f34 122
    if isBase64Whitespace(b) || b == padb
a271f34 123
      continue
a271f34 124
    v := decodeByte(b, at62b, at63b)
a271f34 125
    if v < 0
a271f34 126
      return None
a271f34 127
    bits = {bits << 6} | v
a271f34 128
    nbits = nbits + 6
a271f34 129
    if nbits >= 8
a271f34 130
      nbits = nbits - 8
a271f34 131
      res.writeByte(Byte({bits >> nbits} & 0xFF))
a271f34 132
  return Some(res.toStr())