|
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