diff --git a/src/at/internal/base32.gleam b/src/at/internal/base32.gleam new file mode 100644 index 0000000..1b26c0e --- /dev/null +++ b/src/at/internal/base32.gleam @@ -0,0 +1,157 @@ +// DECODING -------------------------------------------------------------------- + +pub fn decode(value: String) -> Result(BitArray, Nil) { + do_decode(value, <<>>, 0) +} + +fn do_decode( + value: String, + out: BitArray, + length: Int, +) -> Result(BitArray, Nil) { + case value { + "" -> finish_decode_base32(out, length, 0) + + "=" <> rest -> do_decode_padding(rest, out, length, 1) + "a" <> rest -> do_decode(rest, <>, length + 1) + "b" <> rest -> do_decode(rest, <>, length + 1) + "c" <> rest -> do_decode(rest, <>, length + 1) + "d" <> rest -> do_decode(rest, <>, length + 1) + "e" <> rest -> do_decode(rest, <>, length + 1) + "f" <> rest -> do_decode(rest, <>, length + 1) + "g" <> rest -> do_decode(rest, <>, length + 1) + "h" <> rest -> do_decode(rest, <>, length + 1) + "i" <> rest -> do_decode(rest, <>, length + 1) + "j" <> rest -> do_decode(rest, <>, length + 1) + "k" <> rest -> do_decode(rest, <>, length + 1) + "l" <> rest -> do_decode(rest, <>, length + 1) + "m" <> rest -> do_decode(rest, <>, length + 1) + "n" <> rest -> do_decode(rest, <>, length + 1) + "o" <> rest -> do_decode(rest, <>, length + 1) + "p" <> rest -> do_decode(rest, <>, length + 1) + "q" <> rest -> do_decode(rest, <>, length + 1) + "r" <> rest -> do_decode(rest, <>, length + 1) + "s" <> rest -> do_decode(rest, <>, length + 1) + "t" <> rest -> do_decode(rest, <>, length + 1) + "u" <> rest -> do_decode(rest, <>, length + 1) + "v" <> rest -> do_decode(rest, <>, length + 1) + "w" <> rest -> do_decode(rest, <>, length + 1) + "x" <> rest -> do_decode(rest, <>, length + 1) + "y" <> rest -> do_decode(rest, <>, length + 1) + "z" <> rest -> do_decode(rest, <>, length + 1) + "2" <> rest -> do_decode(rest, <>, length + 1) + "3" <> rest -> do_decode(rest, <>, length + 1) + "4" <> rest -> do_decode(rest, <>, length + 1) + "5" <> rest -> do_decode(rest, <>, length + 1) + "6" <> rest -> do_decode(rest, <>, length + 1) + "7" <> rest -> do_decode(rest, <>, length + 1) + + _ -> Error(Nil) + } +} + +fn do_decode_padding( + value: String, + out: BitArray, + length: Int, + padding: Int, +) -> Result(BitArray, Nil) { + case value { + "" -> finish_decode_base32(out, length, padding) + "=" <> rest -> do_decode_padding(rest, out, length, padding + 1) + _ -> Error(Nil) + } +} + +fn finish_decode_base32( + out: BitArray, + length: Int, + padding: Int, +) -> Result(BitArray, Nil) { + case length % 8, padding { + 0, 0 | 2, 0 | 2, 6 | 4, 0 | 4, 4 | 5, 0 | 5, 3 | 7, 0 | 7, 1 -> { + let bit_size = length * 5 + let byte_size = bit_size / 8 + + case out { + <> -> Ok(decoded) + _ -> Error(Nil) + } + } + _, _ -> Error(Nil) + } +} + +// ENCODING -------------------------------------------------------------------- + +pub fn encode(bits: BitArray, pad: Bool) -> String { + do_encode(bits, "", 0, pad) +} + +fn do_encode(bits: BitArray, out: String, length: Int, pad: Bool) -> String { + case bits { + <> -> + do_encode(rest, out <> do_encode_symbol(chunk), length + 1, pad) + + <> -> + do_encode(<<>>, out <> do_encode_symbol(chunk * 2), length + 1, pad) + + <> -> + do_encode(<<>>, out <> do_encode_symbol(chunk * 4), length + 1, pad) + + <> -> + do_encode(<<>>, out <> do_encode_symbol(chunk * 8), length + 1, pad) + + <> -> + do_encode(<<>>, out <> do_encode_symbol(chunk * 16), length + 1, pad) + + <<>> if pad -> + case length % 8 { + 2 -> out <> "======" + 4 -> out <> "====" + 5 -> out <> "===" + 7 -> out <> "=" + _ -> out + } + + _ -> out + } +} + +fn do_encode_symbol(value: Int) -> String { + case value { + 0 -> "a" + 1 -> "b" + 2 -> "c" + 3 -> "d" + 4 -> "e" + 5 -> "f" + 6 -> "g" + 7 -> "h" + 8 -> "i" + 9 -> "j" + 10 -> "k" + 11 -> "l" + 12 -> "m" + 13 -> "n" + 14 -> "o" + 15 -> "p" + 16 -> "q" + 17 -> "r" + 18 -> "s" + 19 -> "t" + 20 -> "u" + 21 -> "v" + 22 -> "w" + 23 -> "x" + 24 -> "y" + 25 -> "z" + 26 -> "2" + 27 -> "3" + 28 -> "4" + 29 -> "5" + 30 -> "6" + 31 -> "7" + _ -> "" + } +} diff --git a/test/at/internal/base32_test.gleam b/test/at/internal/base32_test.gleam new file mode 100644 index 0000000..89e1800 --- /dev/null +++ b/test/at/internal/base32_test.gleam @@ -0,0 +1,115 @@ +// IMPORTS --------------------------------------------------------------------- + +import at/internal/base32 +import gleam/bit_array +import gleam/list + +// FIXTURES -------------------------------------------------------------------- +// https://go.dev/src/encoding/base32/base32_test.go + +const valid = [ + #("", ""), + #("my", "f"), + #("mzxq", "fo"), + #("mzxw6", "foo"), + #("mzxw6yq", "foob"), + #("mzxw6ytb", "fooba"), + #("mzxw6ytboi", "foobar"), + #("on2xezjo", "sure."), + #("on2xezi", "sure"), + #("on2xe", "sur"), + #("on2q", "su"), + #("nrswc43vojss4", "leasure."), + #("mvqxg5lsmuxa", "easure."), + #("mfzxk4tffy", "asure."), +] + +const valid_padding = [ + #("", ""), + #("my======", "f"), + #("mzxq====", "fo"), + #("mzxw6===", "foo"), + #("mzxw6yq=", "foob"), + #("mzxw6ytb", "fooba"), + #("mzxw6ytboi======", "foobar"), + #("on2xezjo", "sure."), + #("on2xezi=", "sure"), + #("on2xe===", "sur"), + #("on2q====", "su"), + #("nrswc43vojss4===", "leasure."), + #("mvqxg5lsmuxa====", "easure."), + #("mfzxk4tffy======", "asure."), +] + +const valid_edge_cases = [ + "", + "aa======", + "aaaa====", + "aaaaa===", + "aaaaaaa=", + "aaaaaaaa", +] + +const invalid = [ + "!!!!", + "x===", + "aa=a====", + "aaa=aaaa", + "mmmmmmmmm", + "mmmmmm", + "a=", + "aa=", + "aa==", + "aa===", + "aaaa=", + "aaaa==", + "aaaaa=", + "aaaaa==", + "a=======", + "aaa=====", + "aaaaaa==", +] + +// DECODING -------------------------------------------------------------------- + +pub fn base32_decode_test() { + use #(encoded, expected) <- list.each(valid) + + let assert Ok(decoded) = base32.decode(encoded) as encoded + assert decoded == bit_array.from_string(expected) as encoded +} + +pub fn base32_decode_with_padding_test() { + use #(encoded, expected) <- list.each(valid_padding) + + let assert Ok(decoded) = base32.decode(encoded) as encoded + assert decoded == bit_array.from_string(expected) as encoded +} + +pub fn base32_decode_edge_case_test() { + use encoded <- list.each(valid_edge_cases) + + let assert Ok(_) = base32.decode(encoded) as encoded +} + +pub fn base32_decode_invalid_test() { + use encoded <- list.each(invalid) + + assert base32.decode(encoded) == Error(Nil) as encoded +} + +// ENCODING -------------------------------------------------------------------- + +pub fn base32_encode_test() { + use #(encoded, decoded) <- list.each(valid) + + assert base32.encode(bit_array.from_string(decoded), False) == encoded + as decoded +} + +pub fn base32_encode_with_padding_test() { + use #(encoded, decoded) <- list.each(valid_padding) + + assert base32.encode(bit_array.from_string(decoded), True) == encoded + as decoded +}