|
1 | 1 | Module: base64 |
2 | | -Synopsis: Base64 encoding/decoding |
3 | | -Author: Carl Gay |
| 2 | +Synopsis: Base64 encoding/decoding as defined in RFC 4648 |
4 | 3 | License: This code is in the public domain |
5 | 4 | Warranty: Distributed WITHOUT WARRANTY OF ANY KIND |
6 | 5 |
|
7 | 6 |
|
8 | | -// This file implements the Base64 transfer encoding algorithm as |
9 | | -// defined in RFC 1521 by Borensten & Freed, September 1993. |
10 | | -// |
11 | | -// Original version written in Common Lisp by Juri Pakaste <[email protected]>. |
12 | | -// Converted to Dylan by Carl Gay, July 2002. |
| 7 | +// TODO: |
| 8 | +// * support line breaks |
| 9 | +// * streaming / chunking a la CL's qbase64 |
13 | 10 |
|
14 | | -define constant $standard-encoding-vector :: <byte-string> |
15 | | - = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; |
16 | 11 |
|
17 | | -define constant $http-encoding-vector :: <byte-string> |
18 | | - = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789$!@"; |
| 12 | +define constant $standard-scheme = #"_base64"; |
| 13 | +define constant $url-scheme = #"_base64url"; |
| 14 | +define constant <scheme> = one-of($standard-scheme, $url-scheme); |
19 | 15 |
|
20 | | -// ---TODO: line breaks? |
21 | | -//define constant $base64-line-break :: <byte-string> = "\n"; |
| 16 | +define constant $pad-char :: <character> = '='; |
22 | 17 |
|
23 | | -// I thought FunDev had <integer-vector> built in, but apparently not. |
24 | | -// |
25 | | -define constant <int-vector> = limited(<vector>, of: <integer>); |
| 18 | +// Base 64 Encoding |
| 19 | +// https://datatracker.ietf.org/doc/html/rfc4648#section-4 |
| 20 | +define constant $standard-encoding :: <byte-string> |
| 21 | + = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; |
| 22 | + |
| 23 | +// Base 64 Encoding with URL and Filename Safe Alphabet |
| 24 | +// https://datatracker.ietf.org/doc/html/rfc4648#section-5 |
| 25 | +define constant $url-encoding :: <byte-string> |
| 26 | + = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; |
| 27 | + |
| 28 | +define constant $standard-decoding :: <byte-vector> |
| 29 | + = make-decoding-vector($standard-encoding); |
| 30 | + |
| 31 | +define constant $url-decoding :: <byte-vector> |
| 32 | + = make-decoding-vector($url-encoding); |
26 | 33 |
|
27 | 34 | define function make-decoding-vector |
28 | | - (encoding-vector) => (v :: <int-vector>) |
29 | | - let v = make(<int-vector>, size: 256, fill: -1); |
30 | | - for (index from 0 below v.size, |
| 35 | + (encoding-vector :: <byte-string>) => (v :: <byte-vector>) |
| 36 | + let v = make(<byte-vector>, size: 128); |
| 37 | + for (i from 0, |
31 | 38 | char in encoding-vector) |
32 | | - v[as(<integer>, char)] := index; |
| 39 | + let code = as(<integer>, char); |
| 40 | + v[code] := i; |
33 | 41 | end; |
34 | 42 | v |
35 | | -end; |
| 43 | +end function; |
36 | 44 |
|
37 | | -define constant $standard-decoding-vector :: <int-vector> |
38 | | - = make-decoding-vector($standard-encoding-vector); |
| 45 | +define inline function encoded-length |
| 46 | + (input-length :: <integer>, pad? :: <boolean>) |
| 47 | + => (encoded-length :: <integer>) |
| 48 | + if (pad?) |
| 49 | + // Four chars for every group of 3 bytes including the final group. |
| 50 | + ceiling/(input-length, 3) * 4 |
| 51 | + else |
| 52 | + // One char for every 6 bits in the total bits. |
| 53 | + ceiling/(input-length * 8, 6) |
| 54 | + end |
| 55 | +end function; |
39 | 56 |
|
40 | | -define constant $http-decoding-vector :: <int-vector> |
41 | | - = make-decoding-vector($http-encoding-vector); |
| 57 | +define inline function decoded-length |
| 58 | + (input :: <byte-string>) => (decoded-length :: <integer>) |
| 59 | + let len = input.size; |
| 60 | + // Discard at most two trailing pad chars from the length. |
| 61 | + if (len - 2 >= 2 & input[len - 2] == $pad-char) |
| 62 | + len := len - 2; |
| 63 | + elseif (len - 1 >= 3 & input[len - 1] == $pad-char) |
| 64 | + len := len - 1 |
| 65 | + end; |
| 66 | + floor/(len * 6, 8) |
| 67 | +end function; |
42 | 68 |
|
| 69 | +// Encode `bytes` into base 64 in a <byte-string> using the character set specified by |
| 70 | +// `scheme`. If `pad?` is true the returned string will be a multiple of 4 characters in |
| 71 | +// length, padded with 0 to 2 '=' characters. |
43 | 72 | define function base64-encode |
44 | | - (string :: <byte-string>, #key encoding :: <symbol> = #"standard") |
45 | | - => (s :: <byte-string>) |
| 73 | + (bytes :: <sequence>, |
| 74 | + #key scheme :: <scheme> = $standard-scheme, pad? :: <boolean> = #t) |
| 75 | + => (string :: <byte-string>) |
46 | 76 | let encoding-vector :: <byte-string> |
47 | | - = select (encoding) |
48 | | - #"standard" => $standard-encoding-vector; |
49 | | - #"http" => $http-encoding-vector; |
50 | | - end; |
51 | | - let result = make(<byte-string>, size: 4 * floor/(2 + string.size, 3)); |
52 | | - for (sidx from 0 by 3, |
53 | | - didx from 0 by 4, |
54 | | - while: sidx < string.size) |
55 | | - let chars = 2; |
56 | | - let value = ash(logand(#xFF, as(<integer>, string[sidx])), 8); |
57 | | - for (n from 1 to 2) |
58 | | - when (sidx + n < string.size) |
59 | | - let char-code :: <integer> = as(<integer>, string[sidx + n]); |
60 | | - value := logior(value, logand(#xFF, char-code)); |
61 | | - chars := chars + 1; |
62 | | - end; |
63 | | - when (n = 1) |
64 | | - value := ash(value, 8); |
| 77 | + = select (scheme) |
| 78 | + $standard-scheme => $standard-encoding; |
| 79 | + $url-scheme => $url-encoding; |
65 | 80 | end; |
| 81 | + let convert = if (instance?(bytes, <string>)) |
| 82 | + curry(as, <integer>) |
| 83 | + else |
| 84 | + identity |
| 85 | + end; |
| 86 | + let nbytes :: <integer> = bytes.size; |
| 87 | + let nchars :: <integer> = encoded-length(nbytes, pad?); |
| 88 | + let result = make(<byte-string>, size: nchars); |
| 89 | + let bi :: <integer> = 0; // bytes index |
| 90 | + let ri :: <integer> = 0; // result index |
| 91 | + while (bi < nbytes) |
| 92 | + let b1 :: <byte> = convert(bytes[bi]); |
| 93 | + let b2 :: <byte> = if (bi + 1 < nbytes) convert(bytes[bi + 1]) else 0 end; |
| 94 | + let b3 :: <byte> = if (bi + 2 < nbytes) convert(bytes[bi + 2]) else 0 end; |
| 95 | + let n :: <integer> = ash(b1, 16) + ash(b2, 8) + b3; |
| 96 | + for (shift from -18 to 0 by 6, |
| 97 | + while: ri < nchars) // can happen for pad?: #f |
| 98 | + let index = logand(ash(n, shift), #b111111); |
| 99 | + result[ri] := encoding-vector[index]; |
| 100 | + ri := ri + 1; |
| 101 | + end; |
| 102 | + bi := bi + 3; |
| 103 | + end while; |
| 104 | + if (pad?) |
| 105 | + let len :: <integer> = encoded-length(nbytes, #f); |
| 106 | + for (i from len below nchars) |
| 107 | + result[i] := $pad-char; |
66 | 108 | end; |
67 | | - result[didx + 3] := encoding-vector[if (chars > 3) logand(value, #x3F) else 64 end]; |
68 | | - value := ash(value, -6); |
69 | | - result[didx + 2] := encoding-vector[if (chars > 2) logand(value, #x3F) else 64 end]; |
70 | | - value := ash(value, -6); |
71 | | - result[didx + 1] := encoding-vector[logand(value, #x3F)]; |
72 | | - value := ash(value, -6); |
73 | | - result[didx + 0] := encoding-vector[logand(value, #x3F)]; |
74 | 109 | end; |
75 | 110 | result |
76 | | -end; |
77 | | - |
| 111 | +end function; |
| 112 | + |
78 | 113 | define function base64-decode |
79 | | - (string :: <byte-string>, #key encoding :: <symbol> = #"standard") |
80 | | - => (s :: <byte-string>) |
81 | | - let result = make(<byte-string>, size: 3 * floor/(string.size, 4)); |
82 | | - let ridx :: <integer> = 0; |
83 | | - block (exit-block) |
84 | | - let decoding-vector :: <int-vector> |
85 | | - = select (encoding) |
86 | | - #"standard" => $standard-decoding-vector; |
87 | | - #"http" => $http-decoding-vector; |
88 | | - end; |
89 | | - let bitstore :: <integer> = 0; |
90 | | - let bitcount :: <integer> = 0; |
91 | | - for (char :: <byte-character> in string) |
92 | | - let value = decoding-vector[as(<integer>, char)]; |
93 | | - unless (value == -1 | value == 64) |
94 | | - bitstore := logior(ash(bitstore, 6), value); |
95 | | - bitcount := bitcount + 6; |
96 | | - when (bitcount >= 8) |
97 | | - bitcount := bitcount - 8; |
98 | | - let code = logand(ash(bitstore, 0 - bitcount), #xFF); |
99 | | - if (zero?(code)) |
100 | | - exit-block(); |
101 | | - else |
102 | | - result[ridx] := as(<byte-character>, code); |
103 | | - ridx := ridx + 1; |
104 | | - bitstore := logand(bitstore, #xFF); |
105 | | - end; |
106 | | - end; |
| 114 | + (string :: <byte-string>, #key scheme :: <scheme> = $standard-scheme) |
| 115 | + => (bytes :: <byte-vector>) |
| 116 | + let decoding-vector :: <byte-vector> |
| 117 | + = select (scheme) |
| 118 | + $standard-scheme => $standard-decoding; |
| 119 | + $url-scheme => $url-decoding; |
107 | 120 | end; |
| 121 | + let nchars :: <integer> = string.size; |
| 122 | + let nbytes :: <integer> = decoded-length(string); |
| 123 | + let bytes = make(<byte-vector>, size: nbytes); |
| 124 | + let bi :: <integer> = 0; |
| 125 | + let si :: <integer> = 0; |
| 126 | + while (si < nchars) |
| 127 | + let c1 = as(<integer>, string[si]); si := si + 1; |
| 128 | + let c2 = si < nchars & as(<integer>, string[si]); si := si + 1; |
| 129 | + let c3 = si < nchars & as(<integer>, string[si]); si := si + 1; |
| 130 | + let c4 = si < nchars & as(<integer>, string[si]); si := si + 1; |
| 131 | + let d1 :: <byte> = decoding-vector[c1]; |
| 132 | + let d2 :: <byte> = if (c2) decoding-vector[c2] else 0 end; |
| 133 | + let d3 :: <byte> = if (c3) decoding-vector[c3] else 0 end; |
| 134 | + let d4 :: <byte> = if (c4) decoding-vector[c4] else 0 end; |
| 135 | + let n :: <integer> = ash(d1, 18) + ash(d2, 12) + ash(d3, 6) + d4; |
| 136 | + for (shift from -16 to 0 by 8, |
| 137 | + while: bi < nbytes) |
| 138 | + bytes[bi] := logand(ash(n, shift), #xff); |
| 139 | + bi := bi + 1; |
108 | 140 | end; |
109 | | - end block; |
110 | | - copy-sequence(result, start: 0, end: ridx) |
111 | | -end; |
| 141 | + end while; |
| 142 | + bytes |
| 143 | +end function; |
112 | 144 |
|
0 commit comments