Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/gleam/bit_array.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,29 @@ pub fn base64_url_decode(encoded: String) -> Result(BitArray, Nil) {
|> base64_decode()
}

/// Encodes a `BitArray` into a base 32 encoded string.
///
/// If the bit array does not contain a whole number of bytes then it is padded
/// with zero bits prior to being encoded.
///
@external(erlang, "gleam_stdlib", "base_encode32")
@external(javascript, "../gleam_stdlib.mjs", "encode32")
pub fn base32_encode(input: BitArray, padding: Bool) -> String

/// Decodes a base 32 encoded string into a `BitArray`.
///
pub fn base32_decode(encoded: String) -> Result(BitArray, Nil) {
let padded = case byte_size(from_string(encoded)) % 8 {
0 -> encoded
n -> string.append(encoded, string.repeat("=", 8 - n))
}
decode32(padded)
}

@external(erlang, "gleam_stdlib", "base_decode32")
@external(javascript, "../gleam_stdlib.mjs", "decode32")
fn decode32(a: String) -> Result(BitArray, Nil)

/// Encodes a `BitArray` into a base 16 encoded string.
///
/// If the bit array does not contain a whole number of bytes then it is padded
Expand Down
73 changes: 71 additions & 2 deletions src/gleam_stdlib.erl
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
less_than/2, string_pop_grapheme/1, string_pop_codeunit/1,
string_starts_with/2, wrap_list/1, string_ends_with/2, string_pad/4,
uri_parse/1, bit_array_slice/3, percent_encode/1, percent_decode/1,
base_decode64/1, parse_query/1, bit_array_concat/1,
bit_array_base64_encode/2, tuple_get/2, classify_dynamic/1, print/1,
base_decode64/1, base_decode32/1, parse_query/1, bit_array_concat/1,
bit_array_base64_encode/2, base_encode32/2, tuple_get/2, classify_dynamic/1, print/1,
println/1, print_error/1, println_error/1, inspect/1, float_to_string/1,
int_from_base_string/2, utf_codepoint_list_to_string/1, contains_string/2,
crop_string/2, base16_encode/1, base16_decode/1, string_replace/3, slice/3,
Expand Down Expand Up @@ -149,6 +149,20 @@ bit_array_base64_encode(_Bin, _Padding) ->
erlang:error(<<"Erlang OTP/26 or higher is required to use base64:encode">>).
-endif.

base_encode32(Bin, Padding) ->
PaddedBin = bit_array_pad_to_bytes(Bin),
{Encoded0, Rest} = base32_encode_body(PaddedBin),
{Encoded1, _PadBy} = base32_encode_rest(Rest),
Encoded = <<Encoded0/binary, Encoded1/binary>>,
case Padding of
true ->
Rem = byte_size(Encoded) rem 8,
PadLen = case Rem of 0 -> 0; N -> 8 - N end,
list_to_binary([Encoded, lists:duplicate(PadLen, $=)]);
false ->
Encoded
end.

bit_array_slice(Bin, Pos, Len) ->
try {ok, binary:part(Bin, Pos, Len)}
catch error:badarg -> {error, nil}
Expand All @@ -159,6 +173,61 @@ base_decode64(S) ->
catch error:_ -> {error, nil}
end.

base_decode32(S) ->
try
Pad = base32_count_trailing_equals(S),
Size = byte_size(S),
S1 = case Pad of 0 -> S; _ -> binary:part(S, 0, Size - Pad) end,
case binary:match(S1, <<"=">>) of
nomatch -> ok;
_ -> erlang:error(badarg)
end,
Bits = <<<<(base32_std_dec(C)):5>> || <<C>> <= S1>>,
Bytes = (erlang:bit_size(Bits) div 8) * 8,
<<Body:Bytes/bits, _/bits>> = Bits,
{ok, Body}
catch error:_ -> {error, nil}
end.

base32_count_trailing_equals(Bin) ->
base32_count_trailing_equals(Bin, byte_size(Bin), 0).

base32_count_trailing_equals(_Bin, 0, N) -> N;
base32_count_trailing_equals(Bin, I, N) ->
C = binary:at(Bin, I - 1),
case C of
$= -> base32_count_trailing_equals(Bin, I - 1, N + 1);
_ -> N
end.

base32_std_enc(I) when is_integer(I) andalso I >= 26 andalso I =< 31 -> I + 24;
base32_std_enc(I) when is_integer(I) andalso I >= 0 andalso I =< 25 ->
I + $A.

base32_std_dec(C) when is_integer(C) andalso C >= $A andalso C =< $Z -> C - $A;
base32_std_dec(C) when is_integer(C) andalso C >= $a andalso C =< $z -> C - $a;
base32_std_dec(C) when is_integer(C) andalso C >= $2 andalso C =< $7 -> C - $2 + 26;
base32_std_dec(_) -> erlang:error(badarg).

base32_encode_body(Bin) ->
Offset = 5 * (byte_size(Bin) div 5),
<<Body:Offset/binary, Rest/binary>> = Bin,
{<<<<(base32_std_enc(I))>> || <<I:5>> <= Body>>, Rest}.

base32_encode_rest(Bin) ->
Whole = erlang:bit_size(Bin) div 5,
Offset = 5 * Whole,
<<Body:Offset/bits, Rest/bits>> = Bin,
Body0 = <<<<(base32_std_enc(I))>> || <<I:5>> <= Body>>,
{Body1, Pad} = case Rest of
<<I:3>> -> {<<(base32_std_enc(I bsl 2))>>, 6};
<<I:1>> -> {<<(base32_std_enc(I bsl 4))>>, 4};
<<I:4>> -> {<<(base32_std_enc(I bsl 1))>>, 3};
<<I:2>> -> {<<(base32_std_enc(I bsl 3))>>, 1};
<<>> -> {<<>>, 0}
end,
{<<Body0/binary, Body1/binary>>, Pad}.

wrap_list(X) when is_list(X) -> X;
wrap_list(X) -> [X].

Expand Down
75 changes: 75 additions & 0 deletions src/gleam_stdlib.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,81 @@ export function decode64(sBase64) {
}
}

const b32Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";

export function encode32(bit_array, padding) {
bit_array = bit_array_pad_to_bytes(bit_array);

let output = "";
let bitBuffer = 0;
let bitCount = 0;

for (let i = 0; i < bit_array.byteSize; i++) {
bitBuffer = (bitBuffer << 8) | bit_array.byteAt(i);
bitCount += 8;

while (bitCount >= 5) {
const index = (bitBuffer >> (bitCount - 5)) & 31;
output += b32Alphabet[index];
bitCount -= 5;
}
}

if (bitCount > 0) {
output += b32Alphabet[(bitBuffer << (5 - bitCount)) & 31];
}

if (padding) {
const mod = output.length % 8;
if (mod !== 0) output += "=".repeat(8 - mod);
}

return output;
}

export function decode32(s) {
try {
const clean = s.replace(/=+$/g, "");

// ASCII lookup table initialised to -1 (invalid)
const map = new Int16Array(128);
for (let i = 0; i < map.length; i++) map[i] = -1;
for (let i = 0; i < 26; i++) {
map[65 + i] = i; // 'A'..'Z'
map[97 + i] = i; // 'a'..'z'
}
for (let i = 0; i < 6; i++) {
map[50 + i] = 26 + i; // '2'..'7'
}

let bitBuffer = 0;
let bitCount = 0;

const outLen = Math.floor((clean.length * 5) / 8);
const bytes = new Uint8Array(outLen);
let j = 0;

for (let i = 0; i < clean.length; i++) {
const code = clean.charCodeAt(i);
if (code >= 128) return new Error(Nil);
const val = map[code];
if (val < 0) return new Error(Nil);

bitBuffer = (bitBuffer << 5) | val;
bitCount += 5;

if (bitCount >= 8) {
bitCount -= 8;
bytes[j++] = (bitBuffer >> bitCount) & 255;
}
}

return new Ok(new BitArray(j === bytes.length ? bytes : bytes.slice(0, j)));
} catch {
return new Error(Nil);
}
}

export function classify_dynamic(data) {
if (typeof data === "string") {
return "String";
Expand Down
66 changes: 66 additions & 0 deletions test/gleam/bit_array_test.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,72 @@ pub fn decode64_crash_regression_1_test() {
== Error(Nil)
}

pub fn base32_encode_test() {
assert bit_array.base32_encode(<<>>, True) == ""

assert bit_array.base32_encode(<<"f":utf8>>, True) == "MY======"

assert bit_array.base32_encode(<<"fo":utf8>>, True) == "MZXQ===="

assert bit_array.base32_encode(<<"foo":utf8>>, True) == "MZXW6==="

assert bit_array.base32_encode(<<"foob":utf8>>, True) == "MZXW6YQ="

assert bit_array.base32_encode(<<"fooba":utf8>>, True) == "MZXW6YTB"

assert bit_array.base32_encode(<<"foobar":utf8>>, True) == "MZXW6YTBOI======"

assert bit_array.base32_encode(<<"f":utf8>>, False) == "MY"

assert bit_array.base32_encode(<<"fo":utf8>>, False) == "MZXQ"

assert bit_array.base32_encode(<<"foo":utf8>>, False) == "MZXW6"

assert bit_array.base32_encode(<<"foob":utf8>>, False) == "MZXW6YQ"

assert bit_array.base32_encode(<<"fooba":utf8>>, False) == "MZXW6YTB"

assert bit_array.base32_encode(<<"foobar":utf8>>, False) == "MZXW6YTBOI"

assert bit_array.base32_encode(<<-1:7>>, True) == "7Y======"

assert bit_array.base32_encode(<<-1:7>>, False) == "7Y"
}

pub fn base32_decode_test() {
assert bit_array.base32_decode("") == Ok(<<>>)

assert bit_array.base32_decode("MY======") == Ok(<<"f":utf8>>)

assert bit_array.base32_decode("MZXQ====") == Ok(<<"fo":utf8>>)

assert bit_array.base32_decode("MZXW6===") == Ok(<<"foo":utf8>>)

assert bit_array.base32_decode("MZXW6YQ=") == Ok(<<"foob":utf8>>)

assert bit_array.base32_decode("MZXW6YTB") == Ok(<<"fooba":utf8>>)

assert bit_array.base32_decode("MZXW6YTBOI======") == Ok(<<"foobar":utf8>>)

assert bit_array.base32_decode("MY") == Ok(<<"f":utf8>>)

assert bit_array.base32_decode("MZXQ") == Ok(<<"fo":utf8>>)

assert bit_array.base32_decode("MZXW6") == Ok(<<"foo":utf8>>)

assert bit_array.base32_decode("MZXW6YQ") == Ok(<<"foob":utf8>>)

assert bit_array.base32_decode("MZXW6YTB") == Ok(<<"fooba":utf8>>)

assert bit_array.base32_decode("MZXW6YTBOI") == Ok(<<"foobar":utf8>>)

assert bit_array.base32_decode("mzxw6ytboi======") == Ok(<<"foobar":utf8>>)

assert bit_array.base32_decode("!)") == Error(Nil)

assert bit_array.base32_decode("=AAAAAAA") == Error(Nil)
}

pub fn base16_encode_test() {
assert bit_array.base16_encode(<<"":utf8>>) == ""

Expand Down