Skip to content

Commit

Permalink
Pad to a whole number of bytes when encoding bit arrays
Browse files Browse the repository at this point in the history
  • Loading branch information
richard-viney committed Nov 27, 2024
1 parent b8785ed commit 2ad488e
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 17 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
21 changes: 18 additions & 3 deletions src/gleam/bit_array.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -127,15 +130,20 @@ 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)
|> string.replace("+", "-")
|> 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
Expand All @@ -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)
Expand Down
34 changes: 20 additions & 14 deletions src/gleam_stdlib.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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 = <<Bin/bits, 0:PaddingBits>>,
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">>).
Expand All @@ -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, <<I:32>>};
bit_array_int_to_u32(_) ->
{error, nil}.

bit_array_int_from_u32(<<I:32>>) ->
{ok, I};
bit_array_int_from_u32(_) ->
{error, nil}.

compile_regex(String, Options) ->
{options, Caseless, Multiline} = Options,
OptionsList = [
Expand Down Expand Up @@ -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 = <<Bin/bits, 0:PaddingBits>>,
binary:encode_hex(PaddedBin)
end.

base16_decode(String) ->
try
{ok, binary:decode_hex(String)}
Expand Down
38 changes: 38 additions & 0 deletions test/gleam/bit_array_test.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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(<<>>))
Expand Down

0 comments on commit 2ad488e

Please sign in to comment.