plum
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()) |