Skip to content

Commit e936647

Browse files
committed
add base64 bit array encoding / decoding
still missing error checking on decode
1 parent 209f69c commit e936647

3 files changed

Lines changed: 180 additions & 72 deletions

File tree

src/gleam/bit_array.gleam

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ pub fn base64_encode(input: BitArray, padding: Bool) -> String {
133133

134134
@external(erlang, "base64", "encode")
135135
@external(javascript, "../gleam_stdlib.mjs", "encode64")
136-
@external(nix, "../gleam_stdlib.nix", "unimplemented")
136+
@external(nix, "../gleam_stdlib.nix", "bit_array_encode64")
137137
fn encode64(a: BitArray) -> String
138138

139139
/// Decodes a base 64 encoded string into a `BitArray`.
@@ -148,7 +148,7 @@ pub fn base64_decode(encoded: String) -> Result(BitArray, Nil) {
148148

149149
@external(erlang, "gleam_stdlib", "base_decode64")
150150
@external(javascript, "../gleam_stdlib.mjs", "decode64")
151-
@external(nix, "../gleam_stdlib.nix", "unimplemented")
151+
@external(nix, "../gleam_stdlib.nix", "bit_array_decode64")
152152
fn decode64(a: String) -> Result(BitArray, Nil)
153153

154154
/// Encodes a `BitArray` into a base 64 encoded string with URL and filename safe alphabet.

src/gleam_stdlib.nix

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -855,6 +855,114 @@ let
855855
let
856856
result = byteArrayToUtf8String array;
857857
in if builtins.isNull result then Error Nil else Ok result;
858+
859+
alpha64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
860+
reverse64 = { "A" = 0; "B" = 1; "C" = 2; "D" = 3; "E" = 4; "F" = 5; "G" = 6; "H" = 7; "I" = 8; "J" = 9; "K" = 10; "L" = 11; "M" = 12; "N" = 13; "O" = 14; "P" = 15; "Q" = 16; "R" = 17; "S" = 18; "T" = 19; "U" = 20; "V" = 21; "W" = 22; "X" = 23; "Y" = 24; "Z" = 25; "a" = 26; "b" = 27; "c" = 28; "d" = 29; "e" = 30; "f" = 31; "g" = 32; "h" = 33; "i" = 34; "j" = 35; "k" = 36; "l" = 37; "m" = 38; "n" = 39; "o" = 40; "p" = 41; "q" = 42; "r" = 43; "s" = 44; "t" = 45; "u" = 46; "v" = 47; "w" = 48; "x" = 49; "y" = 50; "z" = 51; "0" = 52; "1" = 53; "2" = 54; "3" = 55; "4" = 56; "5" = 57; "6" = 58; "7" = 59; "8" = 60; "9" = 61; "+" = 62; "/" = 63; "=" = null; };
861+
bit_array_encode64 = array:
862+
let
863+
bytes = array.buffer;
864+
amount = builtins.length bytes;
865+
rem3 = amount - 3 * (amount / 3);
866+
charAt = n: builtins.substring n 1;
867+
in
868+
# multiples of 8 bits mod 6:
869+
# 8 * 1 = 8 = (mod 6) 2
870+
# * 2 = (mod 6) 10 = -2 = 4
871+
# * 3 = 0
872+
# * 4 = 8 = 2 ...
873+
# =>
874+
# rem3 = 0 => bits are divisible by 6
875+
# rem3 = 1 => 2 extra bits (missing at least 4 bits)
876+
# rem3 = 2 => 4 extra bits (missing at least 2 bits)
877+
#
878+
# Go in groups of 3 bytes (24 bits) => 4 sextets => 4 base64 digits
879+
builtins.concatStringsSep "" (builtins.genList (groupIndex:
880+
let
881+
start = groupIndex * 3;
882+
firstByte = builtins.elemAt bytes start;
883+
firstSextet = firstByte / 4; # right-shift by 2 bits => first 6 digits
884+
in
885+
if start + 1 >= amount # 1 byte in group of 3 bytes
886+
then
887+
let
888+
# have 8 bits, remainder: 2
889+
# 4 bits missing for multiple of 6
890+
# b1 & 0b11 => take the last two digits from first byte
891+
# << 4 => 2 digits + 4 zeroes
892+
secondSextet = builtins.bitAnd firstByte 3 * 16;
893+
# pad twice
894+
# third and fourth sextets do not exist => padding
895+
in charAt firstSextet alpha64 + charAt secondSextet alpha64 + "=="
896+
else # at least 2 bytes in group
897+
let
898+
# have 16 bits, remainder: 4
899+
# 2 bits missing for multiple of 6
900+
# b1 & 0b11 => take the last two bits from first byte
901+
# b2 >> 4 => add to the first four in the second byte
902+
secondByte = builtins.elemAt bytes (start + 1);
903+
secondSextet = builtins.bitAnd firstByte 3 * 16 + secondByte / 16;
904+
in
905+
if start + 2 >= amount # 2 bytes in group of 3
906+
then
907+
let
908+
# b2 & 0b1111 => last four bits from second byte
909+
# << 2 => add two bits, total 6
910+
thirdSextet = builtins.bitAnd secondByte 15 * 4;
911+
in
912+
charAt firstSextet alpha64 + charAt secondSextet alpha64 + charAt thirdSextet alpha64 + "="
913+
else
914+
let
915+
# b2 & 0b1111 => last four bits from second byte
916+
# b3 >> 6 => first two bits from third byte, total 6 bits
917+
thirdByte = builtins.elemAt bytes (start + 2);
918+
thirdSextet = builtins.bitAnd secondByte 15 * 4 + thirdByte / 64;
919+
# b3 & 0b111111 => last six bits from third byte
920+
fourthSextet = builtins.bitAnd thirdByte 63;
921+
in
922+
charAt firstSextet alpha64 + charAt secondSextet alpha64 + charAt thirdSextet alpha64 + charAt fourthSextet alpha64)
923+
(amount / 3 + (if rem3 == 0 then 0 else 1)));
924+
925+
bit_array_decode64 = str:
926+
let
927+
amount = builtins.stringLength str;
928+
rem4 = amount - 4 * (amount / 4);
929+
charAt = n: builtins.substring n 1;
930+
in
931+
# each group of 4 sextets holds up to 3 bytes
932+
# TODO: errors
933+
Ok (toBitArray (builtins.concatLists (builtins.genList (groupIndex:
934+
let
935+
start = groupIndex * 4;
936+
firstSextet = reverse64.${charAt start str} or 0;
937+
secondSextet = if start + 1 < amount then reverse64.${charAt (start + 1) str} or 0 else 0;
938+
thirdSextet = if start + 2 < amount then reverse64.${charAt (start + 2) str} or 0 else null;
939+
fourthSextet = if start + 3 < amount then reverse64.${charAt (start + 3) str} or 0 else null;
940+
941+
# First 6 bets from sextet 1 (<< 2 to give space), last 2 from beginning of sextet 2 (>> 4)
942+
# Total 8 bits
943+
firstByte = firstSextet * 4 + secondSextet / 16;
944+
in
945+
if thirdSextet == null # string too short, or this is padding
946+
then
947+
# reached last byte of group
948+
[ firstByte ]
949+
else # at least 2 bytes in group
950+
let
951+
# First 4 bits from tail of sextet 2 (& 0b1111 << 4), last four from head of sextet 3 (>> 2)
952+
# Total 8 bits
953+
secondByte = builtins.bitAnd secondSextet 15 * 16 + thirdSextet / 4;
954+
in
955+
if fourthSextet == null
956+
then
957+
[ firstByte secondByte ]
958+
else
959+
let
960+
# First 2 bits from tail of sextet 3 (& 0b11 << 6), last 6 from sextet 4
961+
# Total 8 bits
962+
thirdByte = builtins.bitAnd thirdSextet 3 * 64 + fourthSextet;
963+
in
964+
[ firstByte secondByte thirdByte ])
965+
(amount / 4 + (if rem4 == 0 then 0 else 1)))));
858966
in
859967
{
860968
inherit
@@ -938,6 +1046,8 @@ in
9381046
bit_array_slice
9391047
bit_array_from_string
9401048
bit_array_to_string
1049+
bit_array_encode64
1050+
bit_array_decode64
9411051
new_map
9421052
map_size
9431053
map_get

test/gleam/bit_array_test.gleam

Lines changed: 68 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -140,85 +140,83 @@ pub fn is_utf8_test() {
140140
|> should.be_false
141141
}
142142

143-
// pub fn base64_encode_test() {
144-
// <<255, 127, 254, 252>>
145-
// |> bit_array.base64_encode(True)
146-
// |> should.equal("/3/+/A==")
143+
pub fn base64_encode_test() {
144+
<<255, 127, 254, 252>>
145+
|> bit_array.base64_encode(True)
146+
|> should.equal("/3/+/A==")
147147

148-
// <<255, 127, 254, 252>>
149-
// |> bit_array.base64_encode(False)
150-
// |> should.equal("/3/+/A")
148+
<<255, 127, 254, 252>>
149+
|> bit_array.base64_encode(False)
150+
|> should.equal("/3/+/A")
151151

152-
// <<0, 0, 0>>
153-
// |> bit_array.base64_encode(True)
154-
// |> should.equal("AAAA")
152+
<<0, 0, 0>>
153+
|> bit_array.base64_encode(True)
154+
|> should.equal("AAAA")
155155

156-
// <<>>
157-
// |> bit_array.base64_encode(True)
158-
// |> should.equal("")
159-
// }
160-
161-
// pub fn base64_decode_test() {
162-
// "/3/+/A=="
163-
// |> bit_array.base64_decode()
164-
// |> should.equal(Ok(<<255, 127, 254, 252>>))
165-
166-
// "/3/+/A"
167-
// |> bit_array.base64_decode()
168-
// |> should.equal(Ok(<<255, 127, 254, 252>>))
169-
170-
// "AAAA"
171-
// |> bit_array.base64_decode()
172-
// |> should.equal(Ok(<<0, 0, 0>>))
173-
174-
// ""
175-
// |> bit_array.base64_decode()
176-
// |> should.equal(Ok(<<>>))
177-
178-
// ")!"
179-
// |> bit_array.base64_decode()
180-
// |> should.equal(Error(Nil))
181-
// }
182-
183-
// pub fn base64_url_encode_test() {
184-
// <<255, 127, 254, 252>>
185-
// |> bit_array.base64_url_encode(True)
186-
// |> should.equal("_3_-_A==")
187-
188-
// <<255, 127, 254, 252>>
189-
// |> bit_array.base64_url_encode(False)
190-
// |> should.equal("_3_-_A")
191-
192-
// <<0, 0, 0>>
193-
// |> bit_array.base64_url_encode(True)
194-
// |> should.equal("AAAA")
156+
<<>>
157+
|> bit_array.base64_encode(True)
158+
|> should.equal("")
159+
}
195160

196-
// <<>>
197-
// |> bit_array.base64_url_encode(True)
198-
// |> should.equal("")
199-
// }
161+
pub fn base64_decode_test() {
162+
"/3/+/A=="
163+
|> bit_array.base64_decode()
164+
|> should.equal(Ok(<<255, 127, 254, 252>>))
165+
166+
"/3/+/A"
167+
|> bit_array.base64_decode()
168+
|> should.equal(Ok(<<255, 127, 254, 252>>))
169+
170+
"AAAA"
171+
|> bit_array.base64_decode()
172+
|> should.equal(Ok(<<0, 0, 0>>))
173+
174+
""
175+
|> bit_array.base64_decode()
176+
|> should.equal(Ok(<<>>))
177+
// ")!"
178+
// |> bit_array.base64_decode()
179+
// |> should.equal(Error(Nil))
180+
}
200181

201-
// pub fn base64_url_decode_test() {
202-
// "_3_-_A=="
203-
// |> bit_array.base64_url_decode()
204-
// |> should.equal(Ok(<<255, 127, 254, 252>>))
182+
pub fn base64_url_encode_test() {
183+
<<255, 127, 254, 252>>
184+
|> bit_array.base64_url_encode(True)
185+
|> should.equal("_3_-_A==")
205186

206-
// "_3_-_A"
207-
// |> bit_array.base64_url_decode()
208-
// |> should.equal(Ok(<<255, 127, 254, 252>>))
187+
<<255, 127, 254, 252>>
188+
|> bit_array.base64_url_encode(False)
189+
|> should.equal("_3_-_A")
209190

210-
// "AAAA"
211-
// |> bit_array.base64_url_decode()
212-
// |> should.equal(Ok(<<0, 0, 0>>))
191+
<<0, 0, 0>>
192+
|> bit_array.base64_url_encode(True)
193+
|> should.equal("AAAA")
213194

214-
// ""
215-
// |> bit_array.base64_url_decode()
216-
// |> should.equal(Ok(<<>>))
195+
<<>>
196+
|> bit_array.base64_url_encode(True)
197+
|> should.equal("")
198+
}
217199

218-
// ")!"
219-
// |> bit_array.base64_url_decode()
220-
// |> should.equal(Error(Nil))
221-
// }
200+
pub fn base64_url_decode_test() {
201+
"_3_-_A=="
202+
|> bit_array.base64_url_decode()
203+
|> should.equal(Ok(<<255, 127, 254, 252>>))
204+
205+
"_3_-_A"
206+
|> bit_array.base64_url_decode()
207+
|> should.equal(Ok(<<255, 127, 254, 252>>))
208+
209+
"AAAA"
210+
|> bit_array.base64_url_decode()
211+
|> should.equal(Ok(<<0, 0, 0>>))
212+
213+
""
214+
|> bit_array.base64_url_decode()
215+
|> should.equal(Ok(<<>>))
216+
// ")!"
217+
// |> bit_array.base64_url_decode()
218+
// |> should.equal(Error(Nil))
219+
}
222220

223221
// pub fn decode64_crash_regression_1_test() {
224222
// "aGktdGhlcmU.uWUWvrAleKQ2jsWcU97H-RPJ5qRRcE_s"

0 commit comments

Comments
 (0)