diff --git a/README.md b/README.md index fe2d45b..ff4880c 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,14 @@ See the [SSB Binary Field Encodings Specification](https://github.com/ssbc/ssb-b SsbBfe.encode(value) ``` +## Decoding + +```elixir +SsbBfe.decode(value) +``` + +## Supported Types + ### Elixir Types Encoding of the following Elixir types is supported: diff --git a/lib/ssb_bfe.ex b/lib/ssb_bfe.ex index ff8ea35..b248df6 100644 --- a/lib/ssb_bfe.ex +++ b/lib/ssb_bfe.ex @@ -16,6 +16,8 @@ defmodule SsbBfe do :world end + # ENCODE + def encode(value) when is_list(value) do Enum.map(value, fn x -> encode(x) end) end @@ -31,7 +33,8 @@ defmodule SsbBfe do end def encode(value) when is_tuple(value) do - Enum.map(Tuple.to_list(value), fn x -> encode(x) end) + Enum.map(Tuple.to_list(value), fn x -> encode(x) end) |> + List.to_tuple() end def encode(value) when is_bitstring(value) do @@ -61,4 +64,54 @@ defmodule SsbBfe do def encode(value) when is_number(value), do: value def encode(value) when is_nil(value), do: SsbBfe.Encoder.encode_nil() + + # DECODE + + def decode(value) when is_binary(value) do + first_byte = :binary.first(value) + + cond do + 0 == first_byte -> + SsbBfe.Decoder.decode_feed(value) + + 1 == first_byte -> + SsbBfe.Decoder.decode_msg(value) + + 2 == first_byte -> + SsbBfe.Decoder.decode_blob(value) + + 4 == first_byte -> + SsbBfe.Decoder.decode_sig(value) + + 5 == first_byte -> + SsbBfe.Decoder.decode_box(value) + + 6 == first_byte -> + SsbBfe.Decoder.decode_generic(value) + + nil -> + true + end + end + + def decode(value) when is_number(value), do: value + + def decode(value) when is_list(value) do + Enum.map(value, fn x -> decode(x) end) + end + + def decode(value) when is_map(value) do + Enum.reduce( + value, + %{}, + fn {k, v}, acc -> + Map.put(acc, k, SsbBfe.decode(v)) + end + ) + end + + def decode(value) when is_tuple(value) do + Enum.map(Tuple.to_list(value), fn x -> decode(x) end) |> + List.to_tuple() + end end diff --git a/lib/ssb_bfe/decoder.ex b/lib/ssb_bfe/decoder.ex new file mode 100644 index 0000000..74fe5aa --- /dev/null +++ b/lib/ssb_bfe/decoder.ex @@ -0,0 +1,75 @@ +defmodule SsbBfe.Decoder do + defp extract_base64_data(bin, tf_tag) do + [_, base64_data] = :binary.split(bin, tf_tag) + Base.encode64(base64_data) + end + + def decode_blob(blob) do + encoded_base64_data = extract_base64_data(blob, <<2, 0>>) + "&" <> encoded_base64_data <> ".sha256" + end + + def decode_box(box) do + tf_tag = binary_part(box, 0, 2) + decode_box(box, tf_tag) + end + + # Matches box. + def decode_box(box, <<5, 0>>) do + encoded_base64_data = extract_base64_data(box, <<5, 0>>) + encoded_base64_data <> ".box" + end + + # Matches box2. + def decode_box(box, <<5, 1>>) do + encoded_base64_data = extract_base64_data(box, <<5, 1>>) + encoded_base64_data <> ".box2" + end + + def decode_feed(feed) do + tf_tag = binary_part(feed, 0, 2) + decode_feed(feed, tf_tag) + end + + # Matches classic feed. + def decode_feed(feed, <<0, 0>>) do + encoded_base64_data = extract_base64_data(feed, <<0, 0>>) + "@" <> encoded_base64_data <> ".ed25519" + end + + def decode_generic(<<6, 1, 1>>), do: true + + def decode_generic(<<6, 1, 0>>), do: false + + def decode_generic(<<6, 2>>), do: nil + + def decode_generic(generic) do + tf_tag = binary_part(generic, 0, 2) + decode_generic(generic, tf_tag) + end + + # Matches generic string. + def decode_generic(str, <<6, 0>>) do + [_, str_data] = :binary.split(str, <<6, 0>>) + str_data + end + + # Matches generic bytes. + def decode_generic(bytes, <<6, 3>>), do: bytes + + def decode_msg(msg) do + tf_tag = binary_part(msg, 0, 2) + decode_msg(msg, tf_tag) + end + + # Matches classic message. + def decode_msg(msg, <<1, 0>>) do + encoded_base64_data = extract_base64_data(msg, <<1, 0>>) + "%" <> encoded_base64_data <> ".sha256" + end + + def decode_sig(sig) do + encoded_base64_data = extract_base64_data(sig, <<4, 0>>) + encoded_base64_data <> ".sig.ed25519" + end +end diff --git a/lib/ssb_bfe/encoder.ex b/lib/ssb_bfe/encoder.ex index 159b4ac..77db783 100644 --- a/lib/ssb_bfe/encoder.ex +++ b/lib/ssb_bfe/encoder.ex @@ -1,6 +1,3 @@ -# might be good to use a multiclause function for the `encode` function -# - use the `when` conditional guard with e.g. `is_number` - defmodule SsbBfe.Encoder do # Extract the base64 substring from a sigil-link and decode it. defp extract_base64_data(str) do diff --git a/test/ssb_bfe_test.exs b/test/ssb_bfe_test.exs index 21e0ca7..656ff65 100644 --- a/test/ssb_bfe_test.exs +++ b/test/ssb_bfe_test.exs @@ -5,88 +5,174 @@ defmodule SsbBfeTest do setup do [ blob: "&S7+CwHM6dZ9si5Vn4ftpk/l/ldbRMqzzJos+spZbWf4=.sha256", + blob_encoded: <<2, 0, 75, 191, 130, 192, 115, 58, 117, 159, 108, 139, 149, 103, 225, 251, 105, 147, 249, 127, 149, 214, 209, 50, 172, 243, 38, 139, 62, 178, 150, 91, 89, 254>>, + bool_true_encoded: <<6, 1, 1>>, + bool_false_encoded: <<6, 1, 0>>, box1: "bG92ZSBjb2xsYXBzZXMgc3BhY2V0aW1l.box", + box1_encoded: <<5, 0, 108, 111, 118, 101, 32, 99, 111, 108, 108, 97, 112, 115, 101, 115, 32, 115, 112, 97, 99, 101, 116, 105, 109, 101>>, box2: "bG92ZSBjb2xsYXBzZXMgc3BhY2V0aW1l.box2", + box2_encoded: <<5, 1, 108, 111, 118, 101, 32, 99, 111, 108, 108, 97, 112, 115, 101, 115, 32, 115, 112, 97, 99, 101, 116, 105, 109, 101>>, feed_classic: "@d/zDvFswFbQaYJc03i47C9CgDev+/A8QQSfG5l/SEfw=.ed25519", + feed_classic_encoded: <<0, 0, 119, 252, 195, 188, 91, 48, 21, 180, 26, 96, 151, 52, 222, 46, 59, 11, 208, 160, 13, 235, 254, 252, 15, 16, 65, 39, 198, 230, 95, 210, 17, 252>>, msg_classic: "%R8heq/tQoxEIPkWf0Kxn1nCm/CsxG2CDpUYnAvdbXY8=.sha256", + msg_classic_encoded: <<1, 0, 71, 200, 94, 171, 251, 80, 163, 17, 8, 62, 69, 159, 208, 172, 103, 214, 112, 166, 252, 43, 49, 27, 96, 131, 165, 70, 39, 2, 247, 91, 93, 143>>, + nil_encoded: <<6, 2>>, sig: "nkY4Wsn9feosxvX7bpLK7OxjdSrw6gSL8sun1n2TMLXKySYK9L5itVQnV2nQUctFsrUOa2istD2vDk1B0uAMBQ==.sig.ed25519", + sig_encoded: <<4, 0, 158, 70, 56, 90, 201, 253, 125, 234, 44, 198, 245, 251, 110, 146, 202, 236, 236, 99, 117, 42, 240, 234, 4, 139, 242, 203, 167, 214, 125, 147, 48, 181, 202, 201, 38, 10, 244, 190, 98, 181, 84, 39, 87, 105, 208, 81, 203, 69, 178, 181, 14, 107, 104, 172, 180, 61, 175, 14, 77, 65, 210, 224, 12, 5>>, str: "golden ripples in the meshwork", + str_encoded: <<6, 0, 103, 111, 108, 100, 101, 110, 32, 114, 105, 112, 112, 108, 101, 115, 32, 105, 110, 32, 116, 104, 101, 32, 109, 101, 115, 104, 119, 111, 114, 107>> ] end - # HAPPY PATH ENCODING TESTS + # ENCODING TESTS: HAPPY PATH test "classic feed is encoded correctly", context do encoded_feed = SsbBfe.encode(context.feed_classic) - assert encoded_feed == <<0, 0, 119, 252, 195, 188, 91, 48, 21, 180, 26, 96, 151, 52, 222, 46, 59, 11, 208, 160, 13, 235, 254, 252, 15, 16, 65, 39, 198, 230, 95, 210, 17, 252>> + assert encoded_feed == context.feed_classic_encoded end test "classic msg is encoded correctly", context do encoded_msg = SsbBfe.encode(context.msg_classic) - assert encoded_msg == <<1, 0, 71, 200, 94, 171, 251, 80, 163, 17, 8, 62, 69, 159, 208, 172, 103, 214, 112, 166, 252, 43, 49, 27, 96, 131, 165, 70, 39, 2, 247, 91, 93, 143>> + assert encoded_msg == context.msg_classic_encoded end test "blob is encoded correctly", context do encoded_blob = SsbBfe.encode(context.blob) - assert encoded_blob == <<2, 0, 75, 191, 130, 192, 115, 58, 117, 159, 108, 139, 149, 103, 225, 251, 105, 147, 249, 127, 149, 214, 209, 50, 172, 243, 38, 139, 62, 178, 150, 91, 89, 254>> + assert encoded_blob == context.blob_encoded end test "signature is encoded correctly", context do encoded_sig = SsbBfe.encode(context.sig) - assert encoded_sig == <<4, 0, 158, 70, 56, 90, 201, 253, 125, 234, 44, 198, 245, 251, 110, 146, 202, 236, 236, 99, 117, 42, 240, 234, 4, 139, 242, 203, 167, 214, 125, 147, 48, 181, 202, 201, 38, 10, 244, 190, 98, 181, 84, 39, 87, 105, 208, 81, 203, 69, 178, 181, 14, 107, 104, 172, 180, 61, 175, 14, 77, 65, 210, 224, 12, 5>> + assert encoded_sig == context.sig_encoded end test "box is encoded correctly", context do encoded_box = SsbBfe.encode(context.box1) - assert encoded_box == <<5, 0, 108, 111, 118, 101, 32, 99, 111, 108, 108, 97, 112, 115, 101, 115, 32, 115, 112, 97, 99, 101, 116, 105, 109, 101>> + assert encoded_box == context.box1_encoded end test "box2 is encoded correctly", context do encoded_box2 = SsbBfe.encode(context.box2) - assert encoded_box2 == <<5, 1, 108, 111, 118, 101, 32, 99, 111, 108, 108, 97, 112, 115, 101, 115, 32, 115, 112, 97, 99, 101, 116, 105, 109, 101>> + assert encoded_box2 == context.box2_encoded end test "plain string is encoded correctly", context do encoded_str = SsbBfe.encode(context.str) - assert encoded_str == <<6, 0, 103, 111, 108, 100, 101, 110, 32, 114, 105, 112, 112, 108, 101, 115, 32, 105, 110, 32, 116, 104, 101, 32, 109, 101, 115, 104, 119, 111, 114, 107>> + assert encoded_str == context.str_encoded end - test "boolean is encoded correctly" do + test "boolean is encoded correctly", context do encoded_bool_true = SsbBfe.encode(true) encoded_bool_false = SsbBfe.encode(false) - assert encoded_bool_true == <<6, 1, 1>> - assert encoded_bool_false == <<6, 1, 0>> + assert encoded_bool_true == context.bool_true_encoded + assert encoded_bool_false == context.bool_false_encoded end - test "nil is encoded correctly" do + test "nil is encoded correctly", context do encoded_nil = SsbBfe.encode(nil) - assert encoded_nil == <<6, 2>> + assert encoded_nil == context.nil_encoded end - test "list is encoded correctly" do - encoded_list = SsbBfe.encode([true, nil, "ganoderma"]) + test "list is encoded correctly", context do + encoded_list = SsbBfe.encode([true, nil, context.str]) - assert encoded_list == [<<6, 1, 1>>, <<6, 2>>, <<6, 0, 103, 97, 110, 111, 100, 101, 114, 109, 97>>] + assert encoded_list == [context.bool_true_encoded, context.nil_encoded, context.str_encoded] end test "map is encoded correctly", context do encoded_map = SsbBfe.encode(%{"bool" => false, "feed" => context.feed_classic}) - assert encoded_map == %{"bool" => <<6, 1, 0>>, "feed" => <<0, 0, 119, 252, 195, 188, 91, 48, 21, 180, 26, 96, 151, 52, 222, 46, 59, 11, 208, 160, 13, 235, 254, 252, 15, 16, 65, 39, 198, 230, 95, 210, 17, 252>>} + assert encoded_map == %{"bool" => context.bool_false_encoded, "feed" => context.feed_classic_encoded} end test "tuple is encoded correctly", context do encoded_tuple = SsbBfe.encode({7, context.msg_classic}) - assert encoded_tuple == [7, <<1, 0, 71, 200, 94, 171, 251, 80, 163, 17, 8, 62, 69, 159, 208, 172, 103, 214, 112, 166, 252, 43, 49, 27, 96, 131, 165, 70, 39, 2, 247, 91, 93, 143>>] + assert encoded_tuple == {7, context.msg_classic_encoded} + end + + # DECODING TESTS: HAPPY PATH + + test "classic feed is decoded correctly", context do + decoded_feed = SsbBfe.decode(context.feed_classic_encoded) + + assert decoded_feed == context.feed_classic + end + + test "classic message is decoded correctly", context do + decoded_msg = SsbBfe.decode(context.msg_classic_encoded) + + assert decoded_msg == context.msg_classic + end + + test "blob is decoded correctly", context do + decoded_blob = SsbBfe.decode(context.blob_encoded) + + assert decoded_blob == context.blob + end + + test "signature is decoded correctly", context do + decoded_sig = SsbBfe.decode(context.sig_encoded) + + assert decoded_sig == context.sig + end + + test "box is decoded correctly", context do + decoded_box = SsbBfe.decode(context.box1_encoded) + + assert decoded_box == context.box1 + end + + test "box2 is decoded correctly", context do + decoded_box2 = SsbBfe.decode(context.box2_encoded) + + assert decoded_box2 == context.box2 + end + + test "plain string is decoded correctly", context do + decoded_str = SsbBfe.decode(context.str_encoded) + + assert decoded_str == context.str + end + + test "boolean is decoded correctly", context do + decoded_bool_true = SsbBfe.decode(context.bool_true_encoded) + decoded_bool_false = SsbBfe.decode(context.bool_false_encoded) + + assert decoded_bool_true == true + assert decoded_bool_false == false + end + + test "nil string is decoded correctly", context do + decoded_nil = SsbBfe.decode(context.nil_encoded) + + assert decoded_nil == nil + end + + test "list is decoded correctly", context do + decoded_list = SsbBfe.decode([context.bool_true_encoded, context.nil_encoded, context.str_encoded]) + + assert decoded_list == [true, nil, context.str] + end + + test "map is decoded correctly", context do + decoded_map = SsbBfe.decode(%{"bool" => context.bool_false_encoded, "feed" => context.feed_classic_encoded}) + + assert decoded_map == %{"bool" => false, "feed" => context.feed_classic} + end + + test "tuple is decoded correctly", context do + decoded_tuple = SsbBfe.decode({7, context.msg_classic_encoded}) + + assert decoded_tuple == {7, context.msg_classic} end end