Skip to content

Commit 0b6aab6

Browse files
committed
Pad to a whole number of bytes when encoding bit arrays
1 parent b0afb8f commit 0b6aab6

File tree

4 files changed

+80
-17
lines changed

4 files changed

+80
-17
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
- The performance of `string.trim`, `string.trim_start`, and `string.trim_end`
66
has been improved on JavaScript.
7+
- The `base64_encode`, `base64_url_encode`, and `base16_encode` functions in the
8+
`bit_array` module no longer throw an exception when called with a bit array
9+
which is not a whole number of bytes. Instead, the bit array is now padded
10+
with zero bits prior to being encoded.
711

812
## v0.44.0 - 2024-11-25
913

src/gleam/bit_array.gleam

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@ fn do_to_string(bits: BitArray) -> Result(String, Nil) {
108108
pub fn concat(bit_arrays: List(BitArray)) -> BitArray
109109

110110
/// Encodes a BitArray into a base 64 encoded string.
111+
///
112+
/// If the bit array does not contain a whole number of bytes then it is padded
113+
/// with zero bits prior to being encoded.
111114
///
112115
@external(erlang, "gleam_stdlib", "bit_array_base64_encode")
113116
@external(javascript, "../gleam_stdlib.mjs", "encode64")
@@ -127,15 +130,20 @@ pub fn base64_decode(encoded: String) -> Result(BitArray, Nil) {
127130
@external(javascript, "../gleam_stdlib.mjs", "decode64")
128131
fn decode64(a: String) -> Result(BitArray, Nil)
129132

130-
/// Encodes a `BitArray` into a base 64 encoded string with URL and filename safe alphabet.
133+
/// Encodes a `BitArray` into a base 64 encoded string with URL and filename
134+
/// safe alphabet.
135+
///
136+
/// If the bit array does not contain a whole number of bytes then it is padded
137+
/// with zero bits prior to being encoded.
131138
///
132139
pub fn base64_url_encode(input: BitArray, padding: Bool) -> String {
133140
base64_encode(input, padding)
134141
|> string.replace("+", "-")
135142
|> string.replace("/", "_")
136143
}
137144

138-
/// Decodes a base 64 encoded string with URL and filename safe alphabet into a `BitArray`.
145+
/// Decodes a base 64 encoded string with URL and filename safe alphabet into a
146+
/// `BitArray`.
139147
///
140148
pub fn base64_url_decode(encoded: String) -> Result(BitArray, Nil) {
141149
encoded
@@ -144,10 +152,17 @@ pub fn base64_url_decode(encoded: String) -> Result(BitArray, Nil) {
144152
|> base64_decode()
145153
}
146154

147-
@external(erlang, "binary", "encode_hex")
155+
/// Encodes a `BitArray` into a base 16 encoded string.
156+
///
157+
/// If the bit array does not contain a whole number of bytes then it is padded
158+
/// with zero bits prior to being encoded.
159+
///
160+
@external(erlang, "gleam_stdlib", "base16_encode")
148161
@external(javascript, "../gleam_stdlib.mjs", "base16_encode")
149162
pub fn base16_encode(input: BitArray) -> String
150163

164+
/// Decodes a base 16 encoded string into a `BitArray`.
165+
///
151166
@external(erlang, "gleam_stdlib", "base16_decode")
152167
@external(javascript, "../gleam_stdlib.mjs", "base16_decode")
153168
pub fn base16_decode(input: String) -> Result(BitArray, Nil)

src/gleam_stdlib.erl

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
decode_float/1, decode_list/1, decode_option/2, decode_field/2, parse_int/1,
66
parse_float/1, less_than/2, string_pop_grapheme/1, string_pop_codeunit/1,
77
string_starts_with/2, wrap_list/1, string_ends_with/2, string_pad/4,
8-
decode_map/1, uri_parse/1, bit_array_int_to_u32/1, bit_array_int_from_u32/1,
8+
decode_map/1, uri_parse/1,
99
decode_result/1, bit_array_slice/3, decode_bit_array/1, compile_regex/2,
1010
regex_scan/2, percent_encode/1, percent_decode/1, regex_check/2,
1111
regex_split/2, base_decode64/1, parse_query/1, bit_array_concat/1,
@@ -14,8 +14,8 @@
1414
tuple_get/2, classify_dynamic/1, print/1, println/1, print_error/1,
1515
println_error/1, inspect/1, float_to_string/1, int_from_base_string/2,
1616
utf_codepoint_list_to_string/1, contains_string/2, crop_string/2,
17-
base16_decode/1, string_replace/3, regex_replace/3, slice/3,
18-
bit_array_to_int_and_size/1
17+
base16_encode/1, base16_decode/1, string_replace/3, regex_replace/3,
18+
slice/3, bit_array_to_int_and_size/1
1919
]).
2020

2121
%% Taken from OTP's uri_string module
@@ -212,7 +212,14 @@ bit_array_concat(BitArrays) ->
212212

213213
-if(?OTP_RELEASE >= 26).
214214
bit_array_base64_encode(Bin, Padding) ->
215-
base64:encode(Bin, #{padding => Padding}).
215+
case erlang:bit_size(Bin) rem 8 of
216+
0 ->
217+
base64:encode(Bin, #{padding => Padding});
218+
TrailingBits ->
219+
PaddingBits = 8 - TrailingBits,
220+
PaddedBin = <<Bin/bits, 0:PaddingBits>>,
221+
base64:encode(PaddedBin, #{padding => Padding})
222+
end.
216223
-else.
217224
bit_array_base64_encode(_Bin, _Padding) ->
218225
erlang:error(<<"Erlang OTP/26 or higher is required to use base64:encode">>).
@@ -223,16 +230,6 @@ bit_array_slice(Bin, Pos, Len) ->
223230
catch error:badarg -> {error, nil}
224231
end.
225232

226-
bit_array_int_to_u32(I) when 0 =< I, I < 4294967296 ->
227-
{ok, <<I:32>>};
228-
bit_array_int_to_u32(_) ->
229-
{error, nil}.
230-
231-
bit_array_int_from_u32(<<I:32>>) ->
232-
{ok, I};
233-
bit_array_int_from_u32(_) ->
234-
{error, nil}.
235-
236233
compile_regex(String, Options) ->
237234
{options, Caseless, Multiline} = Options,
238235
OptionsList = [
@@ -552,6 +549,15 @@ crop_string(String, Prefix) ->
552549
contains_string(String, Substring) ->
553550
is_bitstring(string:find(String, Substring)).
554551

552+
base16_encode(Bin) ->
553+
case erlang:bit_size(Bin) rem 8 of
554+
0 -> binary:encode_hex(Bin);
555+
TrailingBits ->
556+
PaddingBits = 8 - TrailingBits,
557+
PaddedBin = <<Bin/bits, 0:PaddingBits>>,
558+
binary:encode_hex(PaddedBin)
559+
end.
560+
555561
base16_decode(String) ->
556562
try
557563
{ok, binary:decode_hex(String)}

test/gleam/bit_array_test.gleam

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,23 @@ pub fn base64_encode_test() {
207207
))
208208
}
209209

210+
// This test is target specific since it's using non byte-aligned BitArrays
211+
// and those are not supported on the JavaScript target.
212+
@target(erlang)
213+
pub fn base64_erlang_only_encode_test() {
214+
<<-1:7>>
215+
|> bit_array.base64_encode(True)
216+
|> should.equal("/g==")
217+
218+
<<0xFA, 5:3>>
219+
|> bit_array.base64_encode(True)
220+
|> should.equal("+qA=")
221+
222+
<<0xFA, 0xBC, 0x6D, 1:1>>
223+
|> bit_array.base64_encode(True)
224+
|> should.equal("+rxtgA==")
225+
}
226+
210227
pub fn base64_decode_test() {
211228
"/3/+/A=="
212229
|> bit_array.base64_decode()
@@ -305,6 +322,27 @@ pub fn base16_test() {
305322
|> should.equal("A1B2C3D4E5F67891")
306323
}
307324

325+
// This test is target specific since it's using non byte-aligned BitArrays
326+
// and those are not supported on the JavaScript target.
327+
@target(erlang)
328+
pub fn base16_encode_erlang_only_test() {
329+
<<-1:7>>
330+
|> bit_array.base16_encode()
331+
|> should.equal("FE")
332+
333+
<<0xFA, 5:3>>
334+
|> bit_array.base16_encode()
335+
|> should.equal("FAA0")
336+
337+
<<0xFA, 5:4>>
338+
|> bit_array.base16_encode()
339+
|> should.equal("FA50")
340+
341+
<<0xFA, 0xBC, 0x6D, 1:1>>
342+
|> bit_array.base16_encode()
343+
|> should.equal("FABC6D80")
344+
}
345+
308346
pub fn base16_decode_test() {
309347
bit_array.base16_decode("")
310348
|> should.equal(Ok(<<>>))

0 commit comments

Comments
 (0)