diff --git a/.github/workflows/test-authnz.yaml b/.github/workflows/test-authnz.yaml index a2497807e45d..5e446ff1bad2 100644 --- a/.github/workflows/test-authnz.yaml +++ b/.github/workflows/test-authnz.yaml @@ -35,7 +35,7 @@ jobs: - chrome include: - erlang_version: "27.3" - elixir_version: 1.17.3 + elixir_version: 1.18 env: SELENIUM_DIR: selenium DOCKER_NETWORK: rabbitmq_net diff --git a/.github/workflows/test-make-type-check.yaml b/.github/workflows/test-make-type-check.yaml index 9862cd6aa050..5413e66e3d07 100644 --- a/.github/workflows/test-make-type-check.yaml +++ b/.github/workflows/test-make-type-check.yaml @@ -17,7 +17,7 @@ jobs: plugin: # These are using plugin-specific test jobs. - rabbit - # - rabbitmq_mqtt # disabled due to Elixir 1.18 JSON conficts + - rabbitmq_mqtt - rabbitmq_peer_discovery_aws # These are from the test-plugin test job. - amqp10_client @@ -57,14 +57,14 @@ jobs: - rabbitmq_shovel - rabbitmq_shovel_management - rabbitmq_shovel_prometheus - # - rabbitmq_stomp # disabled due to Elixir 1.18 JSON conficts - # - rabbitmq_stream # disabled due to Elixir 1.18 JSON conficts + - rabbitmq_stomp + - rabbitmq_stream - rabbitmq_stream_common - rabbitmq_stream_management - rabbitmq_tracing - rabbitmq_trust_store - rabbitmq_web_dispatch - # - rabbitmq_web_mqtt # disabled due to Elixir 1.18 JSON conficts + - rabbitmq_web_mqtt - rabbitmq_web_stomp # This one we do not want to run tests so no corresponding test job. - rabbitmq_ct_helpers diff --git a/.github/workflows/test-make.yaml b/.github/workflows/test-make.yaml index c45d1ee2186b..49904aecff77 100644 --- a/.github/workflows/test-make.yaml +++ b/.github/workflows/test-make.yaml @@ -25,7 +25,7 @@ jobs: - '27' - '28' elixir_version: - - '1.18' + - '1.19' # @todo Add macOS and Windows. runs-on: ubuntu-latest timeout-minutes: 60 @@ -64,7 +64,7 @@ jobs: erlang_version: - '28' elixir_version: - - '1.18' + - '1.19' uses: ./.github/workflows/test-make-tests.yaml with: erlang_version: ${{ matrix.erlang_version }} @@ -79,7 +79,7 @@ jobs: erlang_version: - '28' elixir_version: - - '1.18' + - '1.19' uses: ./.github/workflows/test-make-tests.yaml with: erlang_version: ${{ matrix.erlang_version }} @@ -94,7 +94,7 @@ jobs: erlang_version: # Latest OTP - '28' elixir_version: # Latest Elixir - - '1.18' + - '1.19' uses: ./.github/workflows/test-make-type-check.yaml with: erlang_version: ${{ matrix.erlang_version }} diff --git a/.github/workflows/test-management-ui-for-pr.yaml b/.github/workflows/test-management-ui-for-pr.yaml index e233dbf63462..910a276eea6c 100644 --- a/.github/workflows/test-management-ui-for-pr.yaml +++ b/.github/workflows/test-management-ui-for-pr.yaml @@ -26,7 +26,7 @@ jobs: - chrome include: - erlang_version: "27.3" - elixir_version: 1.17 + elixir_version: 1.18 env: SELENIUM_DIR: selenium DOCKER_NETWORK: rabbitmq_net diff --git a/.github/workflows/test-management-ui.yaml b/.github/workflows/test-management-ui.yaml index f10a41becacb..adad108b72c0 100644 --- a/.github/workflows/test-management-ui.yaml +++ b/.github/workflows/test-management-ui.yaml @@ -33,7 +33,7 @@ jobs: - chrome include: - erlang_version: "27.3" - elixir_version: 1.17.3 + elixir_version: 1.18 env: SELENIUM_DIR: selenium DOCKER_NETWORK: rabbitmq_net diff --git a/.github/workflows/test-upgrades.yaml b/.github/workflows/test-upgrades.yaml index 52c2912d4284..df0b73d2ea8b 100644 --- a/.github/workflows/test-upgrades.yaml +++ b/.github/workflows/test-upgrades.yaml @@ -62,7 +62,7 @@ jobs: uses: erlef/setup-beam@v1 with: # Versions repeated later in this file. otp-version: '27' - elixir-version: '1.18' + elixir-version: '1.19' hexpm-mirrors: | https://builds.hex.pm https://cdn.jsdelivr.net/hex @@ -123,7 +123,7 @@ jobs: uses: erlef/setup-beam@v1 with: otp-version: '27' - elixir-version: '1.18' + elixir-version: '1.19' hexpm-mirrors: | https://builds.hex.pm https://cdn.jsdelivr.net/hex diff --git a/Makefile b/Makefile index 88ae069746cb..e05ef6db346e 100644 --- a/Makefile +++ b/Makefile @@ -37,8 +37,6 @@ XREF_SCOPE = app deps # protocols directly. XREF_IGNORE = [ \ {'Elixir.CSV.Encode',impl_for,1}, \ - {'Elixir.JSON.Decoder',impl_for,1}, \ - {'Elixir.JSON.Encoder',impl_for,1}, \ {'Elixir.RabbitMQ.CLI.Core.DataCoercion',impl_for,1}] # Include Elixir libraries in the Xref checks. diff --git a/deps/rabbitmq_cli/Makefile b/deps/rabbitmq_cli/Makefile index 8c2b4f5dd434..90f96e5ec618 100644 --- a/deps/rabbitmq_cli/Makefile +++ b/deps/rabbitmq_cli/Makefile @@ -13,14 +13,13 @@ define PROJECT_ENV endef BUILD_DEPS = rabbit_common -DEPS = csv json stdout_formatter +DEPS = csv stdout_formatter LOCAL_DEPS = elixir TEST_DEPS = amqp amqp_client temp x509 rabbit dep_amqp = hex 3.3.0 dep_csv = hex 3.2.1 -dep_json = hex 1.4.1 dep_temp = hex 0.4.9 dep_x509 = hex 0.9.2 @@ -141,6 +140,12 @@ endif dialyzer:: escript MIX_ENV=test mix dialyzer +# rabbitmq_cli is a pure Elixir project, so erlang.mk's `dialyze` target +# has no .erl files to analyze. Override it as a successful no-op; use +# `gmake dialyzer` (above) for an Elixir Dialyzer run via Mix. +dialyze: + @: + .PHONY: install install: $(ESCRIPT_FILE) diff --git a/deps/rabbitmq_cli/lib/rabbitmq/cli/core/json.ex b/deps/rabbitmq_cli/lib/rabbitmq/cli/core/json.ex new file mode 100644 index 000000000000..4401ce34aa41 --- /dev/null +++ b/deps/rabbitmq_cli/lib/rabbitmq/cli/core/json.ex @@ -0,0 +1,131 @@ +## This Source Code Form is subject to the terms of the Mozilla Public +## License, v. 2.0. If a copy of the MPL was not distributed with this +## file, You can obtain one at https://mozilla.org/MPL/2.0/. +## +## Copyright (c) 2007-2026 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. + +defmodule RabbitMQ.CLI.Core.JSON do + @moduledoc """ + Thin JSON facade used by the CLI tools. + + Wraps `:thoas` so the rest of the codebase does not depend on a specific + backend, and so the module name does not collide with the `JSON` module + added to Elixir's standard library in 1.18. + + `encode/1` returns `{:ok, binary}` and `decode/1` returns + `{:ok, term} | {:error, term}`, matching the shape that callers throughout + `rabbitmq_cli` already rely on. + """ + + @spec encode(term()) :: {:ok, binary()} + def encode(term) do + {:ok, :thoas.encode(normalize(term))} + end + + @spec decode(iodata()) :: {:ok, term()} | {:error, term()} + def decode(bin) do + :thoas.decode(bin) + end + + # Convert Erlang strings (lists of integers) to binaries for proper JSON + # encoding and convert other Erlang-specific terms to readable strings. + defp normalize(data) when is_function(data) do + "Fun()" + end + + defp normalize(data) when is_pid(data) do + "Pid(#{inspect(data)})" + end + + defp normalize(data) when is_port(data) do + "Port(#{inspect(data)})" + end + + defp normalize(data) when is_reference(data) do + "Ref(#{inspect(data)})" + end + + defp normalize(data) when is_binary(data) do + convert_binary(data) + end + + defp normalize([]), do: [] + + # Likely a value like [5672], which we don't want to convert to the + # equivalent unicode codepoint. + defp normalize([val] = data) when is_integer(val) and val > 255 do + data + end + + # Likely a value like [5672, 5682], which we don't want to convert to + # the equivalent unicode codepoint. + defp normalize([v0, v1] = data) + when is_integer(v0) and v0 > 255 and is_integer(v1) and v1 > 255 do + data + end + + defp normalize([b | rest]) when is_binary(b) do + [convert_binary(b) | normalize(rest)] + end + + defp normalize(data) when is_list(data) do + if proplist?(data) do + # `:thoas` encodes maps as JSON objects but is unreliable on lists of + # 2-tuples that contain non-proplist values nested inside, so we hand it + # a real map. + Map.new(data, fn {k, v} -> {normalize(k), normalize(v)} end) + else + try do + case :unicode.characters_to_binary(data, :utf8) do + binary when is_binary(binary) -> + binary + + _ -> + Enum.map(data, &normalize/1) + end + rescue + ArgumentError -> + Enum.map(data, &normalize/1) + end + end + end + + # `:thoas` does not accept bare tuples (only proplist 2-tuples nested in a + # list, handled above). Convert any other tuple to a list so it encodes as a + # JSON array, matching what the previous JSON library used to do. + defp normalize(data) when is_tuple(data) do + data + |> Tuple.to_list() + |> Enum.map(&normalize/1) + end + + defp normalize(data) when is_map(data) do + Enum.into(data, %{}, fn {k, v} -> {normalize(k), normalize(v)} end) + end + + defp normalize(data), do: data + + defp proplist?([_ | _] = list) do + Enum.all?(list, fn + {k, _v} when is_atom(k) or is_binary(k) -> true + _ -> false + end) + end + + defp proplist?(_), do: false + + defp convert_binary(data) when is_binary(data) do + try do + case :unicode.characters_to_binary(data, :utf8) do + binary when is_binary(binary) -> + binary + + _ -> + Base.encode64(data) + end + rescue + ArgumentError -> + Base.encode64(data) + end + end +end diff --git a/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/export_definitions_command.ex b/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/export_definitions_command.ex index fbd1057eaa10..7d350ec21278 100644 --- a/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/export_definitions_command.ex +++ b/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/export_definitions_command.ex @@ -151,7 +151,7 @@ defmodule RabbitMQ.CLI.Ctl.Commands.ExportDefinitionsCommand do defp serialise(raw_map, "json") do # rabbit_definitions already takes care of transforming all # proplists into maps - {:ok, json} = JSON.encode(raw_map) + {:ok, json} = RabbitMQ.CLI.Core.JSON.encode(raw_map) json end diff --git a/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/import_definitions_command.ex b/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/import_definitions_command.ex index 442727a930d0..dd7dd33db179 100644 --- a/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/import_definitions_command.ex +++ b/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/import_definitions_command.ex @@ -166,7 +166,7 @@ defmodule RabbitMQ.CLI.Ctl.Commands.ImportDefinitionsCommand do # defp deserialise(bin, "json") do - JSON.decode(bin) + RabbitMQ.CLI.Core.JSON.decode(bin) end defp deserialise(bin, "erlang") do diff --git a/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/list_user_limits_command.ex b/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/list_user_limits_command.ex index bbad0501c9fe..3c91a08c4c63 100644 --- a/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/list_user_limits_command.ex +++ b/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/list_user_limits_command.ex @@ -36,7 +36,7 @@ defmodule RabbitMQ.CLI.Ctl.Commands.ListUserLimitsCommand do val -> Enum.map(val, fn {user, val} -> - {:ok, val_encoded} = JSON.encode(Map.new(val)) + {:ok, val_encoded} = RabbitMQ.CLI.Core.JSON.encode(Map.new(val)) [user: user, limits: val_encoded] end) end @@ -56,7 +56,7 @@ defmodule RabbitMQ.CLI.Ctl.Commands.ListUserLimitsCommand do {:badrpc, node} val when is_list(val) or is_map(val) -> - {:ok, val_encoded} = JSON.encode(Map.new(val)) + {:ok, val_encoded} = RabbitMQ.CLI.Core.JSON.encode(Map.new(val)) val_encoded end end diff --git a/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/list_vhost_limits_command.ex b/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/list_vhost_limits_command.ex index 1d8fb36aef6a..a91108046b50 100644 --- a/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/list_vhost_limits_command.ex +++ b/deps/rabbitmq_cli/lib/rabbitmq/cli/ctl/commands/list_vhost_limits_command.ex @@ -36,7 +36,7 @@ defmodule RabbitMQ.CLI.Ctl.Commands.ListVhostLimitsCommand do val -> Enum.map(val, fn {vhost, val} -> - {:ok, val_encoded} = JSON.encode(Map.new(val)) + {:ok, val_encoded} = RabbitMQ.CLI.Core.JSON.encode(Map.new(val)) [vhost: vhost, limits: val_encoded] end) end @@ -54,7 +54,7 @@ defmodule RabbitMQ.CLI.Ctl.Commands.ListVhostLimitsCommand do {:badrpc, node} val when is_list(val) or is_map(val) -> - JSON.encode(Map.new(val)) + RabbitMQ.CLI.Core.JSON.encode(Map.new(val)) end end diff --git a/deps/rabbitmq_cli/lib/rabbitmq/cli/formatters/json.ex b/deps/rabbitmq_cli/lib/rabbitmq/cli/formatters/json.ex index 382ebfe251f8..b8844009380f 100644 --- a/deps/rabbitmq_cli/lib/rabbitmq/cli/formatters/json.ex +++ b/deps/rabbitmq_cli/lib/rabbitmq/cli/formatters/json.ex @@ -18,7 +18,7 @@ defmodule RabbitMQ.CLI.Formatters.Json do end def format_output(output, _opts) do - {:ok, json} = JSON.encode(keys_to_atoms(convert_erlang_strings(output))) + {:ok, json} = RabbitMQ.CLI.Core.JSON.encode(keys_to_atoms(output)) json end @@ -72,94 +72,4 @@ defmodule RabbitMQ.CLI.Formatters.Json do end def machine_readable?, do: true - - # Convert Erlang strings (lists of integers) to binaries for proper JSON encoding - # Also convert other Erlang-specific terms to readable strings - defp convert_erlang_strings(data) when is_function(data) do - "Fun()" - end - - defp convert_erlang_strings(data) when is_pid(data) do - "Pid(#{inspect(data)})" - end - - defp convert_erlang_strings(data) when is_port(data) do - "Port(#{inspect(data)})" - end - - defp convert_erlang_strings(data) when is_reference(data) do - "Ref(#{inspect(data)})" - end - - defp convert_erlang_strings(data) when is_binary(data) do - convert_binary(data) - end - - defp convert_erlang_strings([]), do: [] - - defp convert_erlang_strings([val]=data) when is_integer(val) and val > 255 do - # This is likely a value like [5672], which we don't want - # to convert to the equivalent unicode codepoint. - data - end - - defp convert_erlang_strings([v0, v1]=data) when - is_integer(v0) and v0 > 255 and is_integer(v1) and v1 > 255 do - # This is likely a value like [5672, 5682], which we don't want - # to convert to the equivalent unicode codepoint. - data - end - - defp convert_erlang_strings([b | rest]) when is_binary(b) do - [convert_binary(b) | convert_erlang_strings(rest)] - end - - defp convert_erlang_strings(data) when is_list(data) do - try do - case :unicode.characters_to_binary(data, :utf8) do - binary when is_binary(binary) -> - # Successfully converted - it was a valid Unicode string - binary - _ -> - # Conversion failed - not a Unicode string, process as regular list - Enum.map(data, &convert_erlang_strings/1) - end - rescue - ArgumentError -> - # badarg exception - not valid character data, process as regular list - Enum.map(data, &convert_erlang_strings/1) - end - end - - defp convert_erlang_strings(data) when is_tuple(data) do - data - |> Tuple.to_list() - |> Enum.map(&convert_erlang_strings/1) - |> List.to_tuple() - end - - defp convert_erlang_strings(data) when is_map(data) do - Enum.into(data, %{}, fn {k, v} -> - {convert_erlang_strings(k), convert_erlang_strings(v)} - end) - end - - defp convert_erlang_strings(data), do: data - - defp convert_binary(data) when is_binary(data) do - try do - case :unicode.characters_to_binary(data, :utf8) do - binary when is_binary(binary) -> - # Successfully converted - it was a valid Unicode string - binary - _ -> - # Conversion failed - not a Unicode string - Base.encode64(data) - end - rescue - ArgumentError -> - # badarg exception - just base64 encode it. - Base.encode64(data) - end - end end diff --git a/deps/rabbitmq_cli/lib/rabbitmq/cli/formatters/json_stream.ex b/deps/rabbitmq_cli/lib/rabbitmq/cli/formatters/json_stream.ex index fc08edf2a042..2ef210bcc66e 100644 --- a/deps/rabbitmq_cli/lib/rabbitmq/cli/formatters/json_stream.ex +++ b/deps/rabbitmq_cli/lib/rabbitmq/cli/formatters/json_stream.ex @@ -31,7 +31,7 @@ defmodule RabbitMQ.CLI.Formatters.JsonStream do end def format_output(output, _opts) do - {:ok, json} = JSON.encode(keys_to_atoms(output)) + {:ok, json} = RabbitMQ.CLI.Core.JSON.encode(keys_to_atoms(output)) json end diff --git a/deps/rabbitmq_cli/lib/rabbitmqctl.ex b/deps/rabbitmq_cli/lib/rabbitmqctl.ex index 561b978d12bb..019925acd2d1 100644 --- a/deps/rabbitmq_cli/lib/rabbitmqctl.ex +++ b/deps/rabbitmq_cli/lib/rabbitmqctl.ex @@ -569,7 +569,7 @@ defmodule RabbitMQCtl do end defp format_error({:error, :check_failed, err}, %{formatter: "json"}, _) when is_map(err) do - {:ok, res} = JSON.encode(err) + {:ok, res} = RabbitMQ.CLI.Core.JSON.encode(err) {:error, ExitCodes.exit_unavailable(), res} end @@ -589,12 +589,12 @@ defmodule RabbitMQCtl do # Catch all clauses defp format_error({:error, err}, %{formatter: "json"}, _) when is_map(err) do - {:ok, res} = JSON.encode(err) + {:ok, res} = RabbitMQ.CLI.Core.JSON.encode(err) {:error, ExitCodes.exit_unavailable(), res} end defp format_error({:error, exit_code, err}, %{formatter: "json"}, _) when is_map(err) do - {:ok, res} = JSON.encode(err) + {:ok, res} = RabbitMQ.CLI.Core.JSON.encode(err) {:error, exit_code, res} end diff --git a/deps/rabbitmq_cli/mix.exs b/deps/rabbitmq_cli/mix.exs index d91a78ba6aa8..e61874adda42 100644 --- a/deps/rabbitmq_cli/mix.exs +++ b/deps/rabbitmq_cli/mix.exs @@ -11,7 +11,7 @@ defmodule RabbitMQCtl.MixfileBase do [ app: :rabbitmqctl, version: "4.0.0-dev", - elixir: ">= 1.13.4 and < 1.20.0", + elixir: ">= 1.17.0 and < 1.20.0", build_embedded: Mix.env() == :prod, start_permanent: Mix.env() == :prod, escript: [ @@ -26,8 +26,8 @@ defmodule RabbitMQCtl.MixfileBase do exclude: [ CSV, CSV.Encode, - JSON, :mnesia, + :thoas, :msacc, :public_key, :pubkey_cert, @@ -141,8 +141,9 @@ defmodule RabbitMQCtl.MixfileBase do # Note that normal deps will be fetched by Erlang.mk on build. [ { - :json, - path: Path.join(deps_dir, "json") + :thoas, + path: Path.join(deps_dir, "thoas"), + override: true }, { :csv, diff --git a/deps/rabbitmq_cli/test/core/json_test.exs b/deps/rabbitmq_cli/test/core/json_test.exs new file mode 100644 index 000000000000..d26cfc843873 --- /dev/null +++ b/deps/rabbitmq_cli/test/core/json_test.exs @@ -0,0 +1,127 @@ +## This Source Code Form is subject to the terms of the Mozilla Public +## License, v. 2.0. If a copy of the MPL was not distributed with this +## file, You can obtain one at https://mozilla.org/MPL/2.0/. +## +## Copyright (c) 2007-2026 Broadcom. All Rights Reserved. The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. + +defmodule RabbitMQ.CLI.Core.JSONTest do + use ExUnit.Case, async: true + + alias RabbitMQ.CLI.Core.JSON + + describe "encode/1 + decode/1 round-trip" do + test "encodes and decodes a flat map with atom keys" do + {:ok, bin} = JSON.encode(%{a: 1, b: "two"}) + assert {:ok, %{"a" => 1, "b" => "two"}} == JSON.decode(bin) + end + + test "encodes and decodes nested maps" do + {:ok, bin} = JSON.encode(%{outer: %{inner: [<<"a">>, <<"b">>]}}) + assert {:ok, %{"outer" => %{"inner" => ["a", "b"]}}} == JSON.decode(bin) + end + + test "decodes returns {:error, _} on invalid JSON" do + assert {:error, _} = JSON.decode("not json") + end + end + + describe "encode/1 — proplist to JSON object" do + test "atom-keyed proplist becomes a JSON object" do + {:ok, bin} = JSON.encode(a: 1, b: 2) + assert {:ok, %{"a" => 1, "b" => 2}} == JSON.decode(bin) + end + + test "binary-keyed proplist becomes a JSON object" do + {:ok, bin} = JSON.encode([{<<"a">>, 1}, {<<"b">>, 2}]) + assert {:ok, %{"a" => 1, "b" => 2}} == JSON.decode(bin) + end + + test "empty list stays an empty JSON array, not an object" do + {:ok, bin} = JSON.encode([]) + assert {:ok, []} == JSON.decode(bin) + end + + test "list of mixed elements is encoded as a JSON array, not an object" do + {:ok, bin} = JSON.encode([{:a, 1}, "loose"]) + assert {:ok, [["a", 1], "loose"]} == JSON.decode(bin) + end + end + + describe "encode/1 — bare tuple handling (regression for cluster_nodes-style values)" do + test "a non-proplist 2-tuple value is encoded as a JSON array" do + # `:cluster_nodes` in rabbit's environment looks like + # `{[node1, node2], :disc}`. Before the fix, this crashed `:thoas` with + # `function_clause` because the first element of the tuple is a list, + # not an atom or binary, so it does not match thoas's proplist heuristic. + input = [cluster_nodes: {[:rabbit@n1, :rabbit@n2], :disc}] + {:ok, bin} = JSON.encode(input) + assert {:ok, %{"cluster_nodes" => [["rabbit@n1", "rabbit@n2"], "disc"]}} == + JSON.decode(bin) + end + + test "a 3-tuple value is encoded as a 3-element JSON array" do + {:ok, bin} = JSON.encode(point: {1, 2, 3}) + assert {:ok, %{"point" => [1, 2, 3]}} == JSON.decode(bin) + end + + test "a deeply nested non-proplist tuple is encoded as a JSON array" do + {:ok, bin} = JSON.encode(outer: %{inner: {1, 2}}) + assert {:ok, %{"outer" => %{"inner" => [1, 2]}}} == JSON.decode(bin) + end + end + + describe "encode/1 — Erlang string and port number heuristics" do + test "an Erlang charlist is encoded as a JSON string" do + {:ok, bin} = JSON.encode(host: ~c"localhost") + assert {:ok, %{"host" => "localhost"}} == JSON.decode(bin) + end + + test "a single integer port stays a one-element JSON array" do + {:ok, bin} = JSON.encode(tcp_listeners: [5672]) + assert {:ok, %{"tcp_listeners" => [5672]}} == JSON.decode(bin) + end + + test "two integer ports stay a two-element JSON array" do + {:ok, bin} = JSON.encode(tcp_listeners: [5672, 5683]) + assert {:ok, %{"tcp_listeners" => [5672, 5683]}} == JSON.decode(bin) + end + + test "a list of binaries is preserved" do + {:ok, bin} = JSON.encode(files: [<<"a.conf">>, <<"b.conf">>]) + assert {:ok, %{"files" => ["a.conf", "b.conf"]}} == JSON.decode(bin) + end + + # Documented footgun: an integer list whose values are valid Unicode + # codepoints is treated as an Erlang charlist and coerced to a binary. + # The `[5672]` and `[5672, 5683]` special cases above exist precisely to + # avoid this for typical port-number values. + test "an integer list with small values is coerced to a string (charlist)" do + {:ok, bin} = JSON.encode(%{xs: [1, 2, 3]}) + assert {:ok, %{"xs" => <<1, 2, 3>>}} == JSON.decode(bin) + end + end + + describe "encode/1 — Erlang-specific terms become readable strings" do + test "a pid is encoded as a Pid(...) string" do + {:ok, bin} = JSON.encode(%{p: self()}) + assert {:ok, %{"p" => "Pid(" <> _}} = JSON.decode(bin) + end + + test "a reference is encoded as a Ref(...) string" do + {:ok, bin} = JSON.encode(%{r: make_ref()}) + assert {:ok, %{"r" => "Ref(" <> _}} = JSON.decode(bin) + end + + test "a function is encoded as Fun()" do + {:ok, bin} = JSON.encode(%{f: fn -> :ok end}) + assert {:ok, %{"f" => "Fun()"}} == JSON.decode(bin) + end + + test "a binary with invalid UTF-8 falls back to Base64" do + invalid = <<0xFF, 0xFE>> + {:ok, bin} = JSON.encode(%{b: invalid}) + assert {:ok, %{"b" => encoded}} = JSON.decode(bin) + assert encoded == Base.encode64(invalid) + end + end +end diff --git a/deps/rabbitmq_cli/test/ctl/export_definitions_command_test.exs b/deps/rabbitmq_cli/test/ctl/export_definitions_command_test.exs index a5aaaefb37df..82c31ef3e2da 100644 --- a/deps/rabbitmq_cli/test/ctl/export_definitions_command_test.exs +++ b/deps/rabbitmq_cli/test/ctl/export_definitions_command_test.exs @@ -111,7 +111,7 @@ defmodule ExportDefinitionsCommandTest do {:ok, nil} = @command.run([valid_file_path()], context[:opts]) {:ok, bin} = File.read(valid_file_path()) - {:ok, map} = JSON.decode(bin) + {:ok, map} = RabbitMQ.CLI.Core.JSON.decode(bin) assert Map.has_key?(map, "rabbitmq_version") end @@ -128,7 +128,7 @@ defmodule ExportDefinitionsCommandTest do clear_parameter("/", "federation-upstream", "up-1") {:ok, bin} = File.read(valid_file_path()) - {:ok, map} = JSON.decode(bin) + {:ok, map} = RabbitMQ.CLI.Core.JSON.decode(bin) assert Map.has_key?(map, "rabbitmq_version") params = map["parameters"] assert is_map(hd(params)["value"]) diff --git a/deps/rabbitmq_cli/test/ctl/set_user_limits_command_test.exs b/deps/rabbitmq_cli/test/ctl/set_user_limits_command_test.exs index 9fc9e51f6763..4db343db7501 100644 --- a/deps/rabbitmq_cli/test/ctl/set_user_limits_command_test.exs +++ b/deps/rabbitmq_cli/test/ctl/set_user_limits_command_test.exs @@ -131,6 +131,6 @@ defmodule SetUserLimitsCommandTest do defp assert_limits(context, definition) do limits = get_user_limits(context[:user]) - assert {:ok, limits} == JSON.decode(definition) + assert {:ok, limits} == RabbitMQ.CLI.Core.JSON.decode(definition) end end diff --git a/deps/rabbitmq_cli/test/ctl/set_vhost_limits_command_test.exs b/deps/rabbitmq_cli/test/ctl/set_vhost_limits_command_test.exs index 118223887802..817ce90e645c 100644 --- a/deps/rabbitmq_cli/test/ctl/set_vhost_limits_command_test.exs +++ b/deps/rabbitmq_cli/test/ctl/set_vhost_limits_command_test.exs @@ -138,6 +138,6 @@ defmodule SetVhostLimitsCommandTest do defp assert_limits(context) do limits = get_vhost_limits(context[:vhost]) - assert {:ok, limits} == JSON.decode(context[:definition]) + assert {:ok, limits} == RabbitMQ.CLI.Core.JSON.decode(context[:definition]) end end diff --git a/deps/rabbitmq_cli/test/json_formatting_test.exs b/deps/rabbitmq_cli/test/json_formatting_test.exs index 45545390beb0..23bb2d204d67 100644 --- a/deps/rabbitmq_cli/test/json_formatting_test.exs +++ b/deps/rabbitmq_cli/test/json_formatting_test.exs @@ -29,7 +29,7 @@ defmodule JSONFormattingTest do error_check(command, exit_ok()) end) - {:ok, doc} = JSON.decode(output) + {:ok, doc} = RabbitMQ.CLI.Core.JSON.decode(output) assert Map.has_key?(doc, "memory") assert Map.has_key?(doc, "listeners") @@ -67,7 +67,7 @@ defmodule JSONFormattingTest do error_check(command, exit_ok()) end) - {:ok, doc} = JSON.decode(output) + {:ok, doc} = RabbitMQ.CLI.Core.JSON.decode(output) assert Map.has_key?(doc, "running_nodes") running_nodes = doc["running_nodes"] @@ -93,7 +93,7 @@ defmodule JSONFormattingTest do error_check(command, exit_ok()) end) - {:ok, doc} = JSON.decode(output) + {:ok, doc} = RabbitMQ.CLI.Core.JSON.decode(output) assert Map.has_key?(doc, "rabbit") rabbit = doc["rabbit"] assert Map.has_key?(rabbit, "data_dir") diff --git a/deps/rabbitmq_cli/test/test_helper.exs b/deps/rabbitmq_cli/test/test_helper.exs index 260ef8a67d6a..40cbf759f52d 100644 --- a/deps/rabbitmq_cli/test/test_helper.exs +++ b/deps/rabbitmq_cli/test/test_helper.exs @@ -19,7 +19,6 @@ true = Code.append_path(Path.join([System.get_env("DEPS_DIR"), "rabbit_common", true = Code.append_path(Path.join([System.get_env("DEPS_DIR"), "rabbit", "ebin"])) true = Code.append_path(Path.join(["_build", Atom.to_string(Mix.env()), "lib", "amqp", "ebin"])) -true = Code.append_path(Path.join(["_build", Atom.to_string(Mix.env()), "lib", "json", "ebin"])) true = Code.append_path(Path.join(["_build", Atom.to_string(Mix.env()), "lib", "x509", "ebin"])) if function_exported?(Mix, :ensure_application!, 1) do diff --git a/deps/rabbitmq_mqtt/src/rabbit_mqtt_processor.erl b/deps/rabbitmq_mqtt/src/rabbit_mqtt_processor.erl index a21b83da44d7..bc96bf00a7df 100644 --- a/deps/rabbitmq_mqtt/src/rabbit_mqtt_processor.erl +++ b/deps/rabbitmq_mqtt/src/rabbit_mqtt_processor.erl @@ -89,11 +89,6 @@ %% Maximum MQTT packet size in bytes for packets sent from server to client. max_packet_size_outbound :: max_packet_size(), topic_alias_maximum_outbound :: non_neg_integer(), - %% https://github.com/rabbitmq/rabbitmq-server/issues/13040 - %% The database stores the MQTT subscription options in the binding arguments for: - %% * v1 as Erlang record #mqtt_subscription_opts{} - %% * v2 as AMQP 0.9.1 table - binding_args_v2 :: boolean(), msg_interceptor_ctx :: rabbit_msg_interceptor:context() }).