diff --git a/CHANGELOG.md b/CHANGELOG.md index 16f2acd9..6b9adab6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ - The performance of `string.trim`, `string.trim_start`, and `string.trim_end` has been improved on JavaScript. +- The `base64_encode`, `base64_url_encode`, and `base16_encode` functions in the + `bit_array` module no longer throw an exception when called with a bit array + which is not a whole number of bytes. Instead, the bit array is now padded + with zero bits prior to being encoded. ## v0.44.0 - 2024-11-25 diff --git a/src/gleam/bit_array.gleam b/src/gleam/bit_array.gleam index e31709c7..8c9e9745 100644 --- a/src/gleam/bit_array.gleam +++ b/src/gleam/bit_array.gleam @@ -108,6 +108,9 @@ fn do_to_string(bits: BitArray) -> Result(String, Nil) { pub fn concat(bit_arrays: List(BitArray)) -> BitArray /// Encodes a BitArray into a base 64 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", "bit_array_base64_encode") @external(javascript, "../gleam_stdlib.mjs", "encode64") @@ -127,7 +130,11 @@ pub fn base64_decode(encoded: String) -> Result(BitArray, Nil) { @external(javascript, "../gleam_stdlib.mjs", "decode64") fn decode64(a: String) -> Result(BitArray, Nil) -/// Encodes a `BitArray` into a base 64 encoded string with URL and filename safe alphabet. +/// Encodes a `BitArray` into a base 64 encoded string with URL and filename +/// safe alphabet. +/// +/// If the bit array does not contain a whole number of bytes then it is padded +/// with zero bits prior to being encoded. /// pub fn base64_url_encode(input: BitArray, padding: Bool) -> String { base64_encode(input, padding) @@ -135,7 +142,8 @@ pub fn base64_url_encode(input: BitArray, padding: Bool) -> String { |> string.replace("/", "_") } -/// Decodes a base 64 encoded string with URL and filename safe alphabet into a `BitArray`. +/// Decodes a base 64 encoded string with URL and filename safe alphabet into a +/// `BitArray`. /// pub fn base64_url_decode(encoded: String) -> Result(BitArray, Nil) { encoded @@ -144,10 +152,17 @@ pub fn base64_url_decode(encoded: String) -> Result(BitArray, Nil) { |> base64_decode() } -@external(erlang, "binary", "encode_hex") +/// 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 +/// with zero bits prior to being encoded. +/// +@external(erlang, "gleam_stdlib", "base16_encode") @external(javascript, "../gleam_stdlib.mjs", "base16_encode") pub fn base16_encode(input: BitArray) -> String +/// Decodes a base 16 encoded string into a `BitArray`. +/// @external(erlang, "gleam_stdlib", "base16_decode") @external(javascript, "../gleam_stdlib.mjs", "base16_decode") pub fn base16_decode(input: String) -> Result(BitArray, Nil) diff --git a/src/gleam_stdlib.erl b/src/gleam_stdlib.erl index ffea232a..a7fc568f 100644 --- a/src/gleam_stdlib.erl +++ b/src/gleam_stdlib.erl @@ -5,7 +5,7 @@ decode_float/1, decode_list/1, decode_option/2, decode_field/2, parse_int/1, parse_float/1, 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, - decode_map/1, uri_parse/1, bit_array_int_to_u32/1, bit_array_int_from_u32/1, + decode_map/1, uri_parse/1, decode_result/1, bit_array_slice/3, decode_bit_array/1, compile_regex/2, regex_scan/2, percent_encode/1, percent_decode/1, regex_check/2, regex_split/2, base_decode64/1, parse_query/1, bit_array_concat/1, @@ -14,8 +14,8 @@ 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_decode/1, string_replace/3, regex_replace/3, slice/3, - bit_array_to_int_and_size/1 + base16_encode/1, base16_decode/1, string_replace/3, regex_replace/3, + slice/3, bit_array_to_int_and_size/1 ]). %% Taken from OTP's uri_string module @@ -212,7 +212,14 @@ bit_array_concat(BitArrays) -> -if(?OTP_RELEASE >= 26). bit_array_base64_encode(Bin, Padding) -> - base64:encode(Bin, #{padding => Padding}). + case erlang:bit_size(Bin) rem 8 of + 0 -> + base64:encode(Bin, #{padding => Padding}); + TrailingBits -> + PaddingBits = 8 - TrailingBits, + PaddedBin = <>, + base64:encode(PaddedBin, #{padding => Padding}) + end. -else. bit_array_base64_encode(_Bin, _Padding) -> erlang:error(<<"Erlang OTP/26 or higher is required to use base64:encode">>). @@ -223,16 +230,6 @@ bit_array_slice(Bin, Pos, Len) -> catch error:badarg -> {error, nil} end. -bit_array_int_to_u32(I) when 0 =< I, I < 4294967296 -> - {ok, <>}; -bit_array_int_to_u32(_) -> - {error, nil}. - -bit_array_int_from_u32(<>) -> - {ok, I}; -bit_array_int_from_u32(_) -> - {error, nil}. - compile_regex(String, Options) -> {options, Caseless, Multiline} = Options, OptionsList = [ @@ -552,6 +549,15 @@ crop_string(String, Prefix) -> contains_string(String, Substring) -> is_bitstring(string:find(String, Substring)). +base16_encode(Bin) -> + case erlang:bit_size(Bin) rem 8 of + 0 -> binary:encode_hex(Bin); + TrailingBits -> + PaddingBits = 8 - TrailingBits, + PaddedBin = <>, + binary:encode_hex(PaddedBin) + end. + base16_decode(String) -> try {ok, binary:decode_hex(String)} diff --git a/test/gleam/bit_array_test.gleam b/test/gleam/bit_array_test.gleam index 555c61ae..0ab5b3b0 100644 --- a/test/gleam/bit_array_test.gleam +++ b/test/gleam/bit_array_test.gleam @@ -207,6 +207,23 @@ pub fn base64_encode_test() { )) } +// This test is target specific since it's using non byte-aligned BitArrays +// and those are not supported on the JavaScript target. +@target(erlang) +pub fn base64_erlang_only_encode_test() { + <<-1:7>> + |> bit_array.base64_encode(True) + |> should.equal("/g==") + + <<0xFA, 5:3>> + |> bit_array.base64_encode(True) + |> should.equal("+qA=") + + <<0xFA, 0xBC, 0x6D, 1:1>> + |> bit_array.base64_encode(True) + |> should.equal("+rxtgA==") +} + pub fn base64_decode_test() { "/3/+/A==" |> bit_array.base64_decode() @@ -305,6 +322,27 @@ pub fn base16_test() { |> should.equal("A1B2C3D4E5F67891") } +// This test is target specific since it's using non byte-aligned BitArrays +// and those are not supported on the JavaScript target. +@target(erlang) +pub fn base16_encode_erlang_only_test() { + <<-1:7>> + |> bit_array.base16_encode() + |> should.equal("FE") + + <<0xFA, 5:3>> + |> bit_array.base16_encode() + |> should.equal("FAA0") + + <<0xFA, 5:4>> + |> bit_array.base16_encode() + |> should.equal("FA50") + + <<0xFA, 0xBC, 0x6D, 1:1>> + |> bit_array.base16_encode() + |> should.equal("FABC6D80") +} + pub fn base16_decode_test() { bit_array.base16_decode("") |> should.equal(Ok(<<>>))