-module(dns_json). -moduledoc {file, "JSON_FORMAT.md"}. -moduledoc #{since => "v5.0.0"}. -export([to_map/1, from_map/1]). -include_lib("dns_erlang/include/dns.hrl"). -doc "Converts a DNS record to a map representation suitable for JSON encoding.". -spec to_map(tuple()) -> map(). to_map(#dns_rr{name = Name, type = Type, class = Class, ttl = Ttl, data = Data}) -> TypeBin = case dns_names:type_name(Type) of undefined -> integer_to_binary(Type); TB when is_binary(TB) -> TB end, ClassBin = case dns_names:class_name(Class) of undefined -> integer_to_binary(Class); CB when is_binary(CB) -> CB end, DataMap = to_map_rrdata(Data), #{ ~"name" => Name, ~"type" => TypeBin, ~"class" => ClassBin, ~"ttl" => Ttl, ~"data" => DataMap }; to_map(Record) when is_tuple(Record) -> Tag = element(1, Record), is_rrdata_tag(Tag) andalso erlang:error({rrdata_requires_wrapper, Tag}), Fields = record_fields(Tag), Values = tl(tuple_to_list(Record)), DataMap = lists:foldl( fun({Field, Value}, Acc) -> NewValue = to_map_value(Tag, Field, Value), FieldBin = atom_to_binary(Field), Acc#{FieldBin => NewValue} end, #{}, lists:zip(Fields, Values) ), RecordKey = record_key_name(Tag), #{RecordKey => DataMap}. -doc "Converts a map representation back to a DNS record.". -spec from_map(map()) -> tuple(). from_map( #{~"name" := Name, ~"type" := TypeBin, ~"ttl" := Ttl, ~"data" := DataMap} = Map ) when is_binary(TypeBin) -> Type = binary_to_dns_type(TypeBin), Class = case maps:get(~"class", Map, undefined) of undefined -> ?DNS_CLASS_IN; ClassBin when is_binary(ClassBin) -> case dns_names:name_class(ClassBin) of undefined -> ?DNS_CLASS_IN; ClassInt -> ClassInt end; ClassInt when is_integer(ClassInt) -> ClassInt end, %% Convert the data map to RRDATA record Data = from_map_rrdata(Type, DataMap), #dns_rr{ name = Name, type = Type, class = Class, ttl = Ttl, data = Data }; from_map(Map) when is_map(Map), 1 =:= map_size(Map) -> [{Key, DataMap}] = maps:to_list(Map), Tag = record_type_from_key(Key), is_binary(Key) orelse erlang:error({invalid_record_key, Key, Map}), undefined =/= Tag orelse erlang:error({unknown_record_key, Key, Map}), is_rrdata_tag(Tag) andalso erlang:error({rrdata_requires_wrapper, Tag}), Fields = record_fields(Tag), Values = [ from_map_field(Tag, Field, maps:get(atom_to_binary(Field), DataMap, undefined)) || Field <- Fields ], list_to_tuple([Tag | Values]); from_map(Map) -> erlang:error({invalid_map_format, Map}). %% Helper to map record type atom to JSON key name %% Other records use descriptive names (message, query, rr, OPT, etc.) %% Note: RRDATA records must be wrapped in dns_rr, so they don't need key names -spec record_key_name(atom()) -> binary(). record_key_name(dns_message) -> ~"message"; record_key_name(dns_query) -> ~"query"; record_key_name(dns_optrr) -> ~"OPT"; record_key_name(dns_opt_llq) -> ?DNS_EOPTCODE_LLQ_BSTR; record_key_name(dns_opt_nsid) -> ?DNS_EOPTCODE_NSID_BSTR; record_key_name(dns_opt_owner) -> ?DNS_EOPTCODE_OWNER_BSTR; record_key_name(dns_opt_ul) -> ?DNS_EOPTCODE_UL_BSTR; record_key_name(dns_opt_ecs) -> ?DNS_EOPTCODE_ECS_BSTR; record_key_name(dns_opt_cookie) -> ?DNS_EOPTCODE_COOKIE_BSTR; record_key_name(dns_opt_ede) -> ?DNS_EOPTCODE_EDE_BSTR; record_key_name(dns_opt_unknown) -> ~"OPT_UNKNOWN". %% Helper to map JSON key name back to record type atom %% Note: RRDATA records are only used internally via type_to_rrdata_tag, not for standalone parsing -spec record_type_from_key(undefined | binary()) -> atom() | undefined. record_type_from_key(~"message") -> dns_message; record_type_from_key(~"query") -> dns_query; record_type_from_key(~"rr") -> dns_rr; record_type_from_key(~"OPT") -> dns_optrr; record_type_from_key(~"OPT_UNKNOWN") -> dns_opt_unknown; %% RRDATA records record_type_from_key(?DNS_TYPE_A_BSTR) -> dns_rrdata_a; record_type_from_key(?DNS_TYPE_AAAA_BSTR) -> dns_rrdata_aaaa; record_type_from_key(?DNS_TYPE_AFSDB_BSTR) -> dns_rrdata_afsdb; record_type_from_key(?DNS_TYPE_CAA_BSTR) -> dns_rrdata_caa; record_type_from_key(?DNS_TYPE_CERT_BSTR) -> dns_rrdata_cert; record_type_from_key(?DNS_TYPE_CNAME_BSTR) -> dns_rrdata_cname; record_type_from_key(?DNS_TYPE_DHCID_BSTR) -> dns_rrdata_dhcid; record_type_from_key(?DNS_TYPE_DNAME_BSTR) -> dns_rrdata_dname; record_type_from_key(?DNS_TYPE_OPENPGPKEY_BSTR) -> dns_rrdata_openpgpkey; record_type_from_key(?DNS_TYPE_DNSKEY_BSTR) -> dns_rrdata_dnskey; record_type_from_key(?DNS_TYPE_CDNSKEY_BSTR) -> dns_rrdata_cdnskey; record_type_from_key(?DNS_TYPE_DS_BSTR) -> dns_rrdata_ds; record_type_from_key(?DNS_TYPE_CDS_BSTR) -> dns_rrdata_cds; record_type_from_key(?DNS_TYPE_DLV_BSTR) -> dns_rrdata_dlv; record_type_from_key(?DNS_TYPE_ZONEMD_BSTR) -> dns_rrdata_zonemd; record_type_from_key(?DNS_TYPE_HINFO_BSTR) -> dns_rrdata_hinfo; record_type_from_key(?DNS_TYPE_IPSECKEY_BSTR) -> dns_rrdata_ipseckey; record_type_from_key(?DNS_TYPE_KEY_BSTR) -> dns_rrdata_key; record_type_from_key(?DNS_TYPE_KX_BSTR) -> dns_rrdata_kx; record_type_from_key(?DNS_TYPE_LOC_BSTR) -> dns_rrdata_loc; record_type_from_key(?DNS_TYPE_MB_BSTR) -> dns_rrdata_mb; record_type_from_key(?DNS_TYPE_MG_BSTR) -> dns_rrdata_mg; record_type_from_key(?DNS_TYPE_MINFO_BSTR) -> dns_rrdata_minfo; record_type_from_key(?DNS_TYPE_MR_BSTR) -> dns_rrdata_mr; record_type_from_key(?DNS_TYPE_MX_BSTR) -> dns_rrdata_mx; record_type_from_key(?DNS_TYPE_NAPTR_BSTR) -> dns_rrdata_naptr; record_type_from_key(?DNS_TYPE_NS_BSTR) -> dns_rrdata_ns; record_type_from_key(?DNS_TYPE_NSEC_BSTR) -> dns_rrdata_nsec; record_type_from_key(?DNS_TYPE_NSEC3_BSTR) -> dns_rrdata_nsec3; record_type_from_key(?DNS_TYPE_NSEC3PARAM_BSTR) -> dns_rrdata_nsec3param; record_type_from_key(?DNS_TYPE_CSYNC_BSTR) -> dns_rrdata_csync; record_type_from_key(?DNS_TYPE_DSYNC_BSTR) -> dns_rrdata_dsync; record_type_from_key(?DNS_TYPE_NXT_BSTR) -> dns_rrdata_nxt; record_type_from_key(?DNS_TYPE_PTR_BSTR) -> dns_rrdata_ptr; record_type_from_key(?DNS_TYPE_RP_BSTR) -> dns_rrdata_rp; record_type_from_key(?DNS_TYPE_RRSIG_BSTR) -> dns_rrdata_rrsig; record_type_from_key(?DNS_TYPE_RT_BSTR) -> dns_rrdata_rt; record_type_from_key(?DNS_TYPE_SOA_BSTR) -> dns_rrdata_soa; record_type_from_key(?DNS_TYPE_SPF_BSTR) -> dns_rrdata_spf; record_type_from_key(?DNS_TYPE_SRV_BSTR) -> dns_rrdata_srv; record_type_from_key(?DNS_TYPE_SSHFP_BSTR) -> dns_rrdata_sshfp; record_type_from_key(?DNS_TYPE_SVCB_BSTR) -> dns_rrdata_svcb; record_type_from_key(?DNS_TYPE_HTTPS_BSTR) -> dns_rrdata_https; record_type_from_key(?DNS_TYPE_TLSA_BSTR) -> dns_rrdata_tlsa; record_type_from_key(?DNS_TYPE_SMIMEA_BSTR) -> dns_rrdata_smimea; record_type_from_key(?DNS_TYPE_TSIG_BSTR) -> dns_rrdata_tsig; record_type_from_key(?DNS_TYPE_TXT_BSTR) -> dns_rrdata_txt; record_type_from_key(?DNS_TYPE_URI_BSTR) -> dns_rrdata_uri; record_type_from_key(?DNS_TYPE_EUI48_BSTR) -> dns_rrdata_eui48; record_type_from_key(?DNS_TYPE_EUI64_BSTR) -> dns_rrdata_eui64; record_type_from_key(?DNS_TYPE_RESINFO_BSTR) -> dns_rrdata_resinfo; record_type_from_key(?DNS_TYPE_WALLET_BSTR) -> dns_rrdata_wallet; record_type_from_key(?DNS_EOPTCODE_LLQ_BSTR) -> dns_opt_llq; record_type_from_key(?DNS_EOPTCODE_NSID_BSTR) -> dns_opt_nsid; record_type_from_key(?DNS_EOPTCODE_OWNER_BSTR) -> dns_opt_owner; record_type_from_key(?DNS_EOPTCODE_UL_BSTR) -> dns_opt_ul; record_type_from_key(?DNS_EOPTCODE_ECS_BSTR) -> dns_opt_ecs; record_type_from_key(?DNS_EOPTCODE_COOKIE_BSTR) -> dns_opt_cookie; record_type_from_key(?DNS_EOPTCODE_EDE_BSTR) -> dns_opt_ede; record_type_from_key(_) -> undefined. %% Convert RRDATA record to a flat map -spec to_map_rrdata(tuple() | binary()) -> map(). to_map_rrdata(Record) when is_tuple(Record) -> Tag = element(1, Record), Fields = record_fields(Tag), Values = tl(tuple_to_list(Record)), lists:foldl( fun({Field, Value}, Acc) -> NewValue = to_map_value(Tag, Field, Value), FieldBin = field_name(Tag, Field), Acc#{FieldBin => NewValue} end, #{}, lists:zip(Fields, Values) ); to_map_rrdata(Binary) when is_binary(Binary) -> %% For unknown/unsupported record types, data is a raw binary %% Encode as base64 for JSON representation #{~"data" => base64:encode(Binary)}. %% Check if a tag is an RRDATA record %% RRDATA records must be wrapped in dns_rr -spec is_rrdata_tag(atom()) -> boolean(). is_rrdata_tag(Tag) when is_atom(Tag) -> string:prefix(atom_to_list(Tag), "dns_rrdata_") =/= nomatch. -spec field_name(atom(), atom()) -> binary(). field_name(dns_rrdata_txt, txt) -> ~"txts"; field_name(dns_rrdata_sshfp, fp_type) -> ~"fptype"; field_name(_Tag, Field) -> atom_to_binary(Field). %% Convert binary DNS type name to type integer -spec binary_to_dns_type(binary()) -> dns:type(). binary_to_dns_type(TypeBin) when is_binary(TypeBin) -> case dns_names:name_type(TypeBin) of undefined -> case string:to_integer(TypeBin) of {TypeNum, <<>>} when is_integer(TypeNum) -> TypeNum; _ -> erlang:error({unknown_type_name, TypeBin}) end; Type -> Type end. %% Convert a data map to RRDATA record based on type -spec from_map_rrdata(dns:type(), map()) -> tuple() | binary(). from_map_rrdata(Type, DataMap) -> %% Try to map DNS type to RRDATA record tag case type_to_rrdata_tag(Type) of undefined -> %% Unknown type - decode base64 binary if present case DataMap of #{~"data" := Base64Data} -> base64:decode(Base64Data); _ -> erlang:error({unknown_type_cannot_convert_to_rrdata, Type}) end; Tag when is_atom(Tag) -> %% Known type - convert to record Fields = record_fields(Tag), Values = [ from_map_field(Tag, Field, maps:get(field_name(Tag, Field), DataMap, undefined)) || Field <- Fields ], list_to_tuple([Tag | Values]) end. %% Convert DNS type to RRDATA record tag -spec type_to_rrdata_tag(dns:type()) -> atom() | no_return(). type_to_rrdata_tag(Type) when is_integer(Type) -> TypeName = dns_names:type_name(Type), record_type_from_key(TypeName). %% Helper to get record fields (needed because record_info requires literal atoms) -spec record_fields(atom()) -> [atom()]. record_fields(dns_message) -> record_info(fields, dns_message); record_fields(dns_query) -> record_info(fields, dns_query); record_fields(dns_rr) -> record_info(fields, dns_rr); record_fields(dns_rrdata_a) -> record_info(fields, dns_rrdata_a); record_fields(dns_rrdata_afsdb) -> record_info(fields, dns_rrdata_afsdb); record_fields(dns_rrdata_aaaa) -> record_info(fields, dns_rrdata_aaaa); record_fields(dns_rrdata_caa) -> record_info(fields, dns_rrdata_caa); record_fields(dns_rrdata_cname) -> record_info(fields, dns_rrdata_cname); record_fields(dns_rrdata_dhcid) -> record_info(fields, dns_rrdata_dhcid); record_fields(dns_rrdata_dname) -> record_info(fields, dns_rrdata_dname); record_fields(dns_rrdata_openpgpkey) -> record_info(fields, dns_rrdata_openpgpkey); record_fields(dns_rrdata_dnskey) -> record_info(fields, dns_rrdata_dnskey); record_fields(dns_rrdata_cdnskey) -> record_info(fields, dns_rrdata_cdnskey); record_fields(dns_rrdata_key) -> record_info(fields, dns_rrdata_key); record_fields(dns_rrdata_mx) -> record_info(fields, dns_rrdata_mx); record_fields(dns_rrdata_kx) -> record_info(fields, dns_rrdata_kx); record_fields(dns_rrdata_ns) -> record_info(fields, dns_rrdata_ns); record_fields(dns_rrdata_ptr) -> record_info(fields, dns_rrdata_ptr); record_fields(dns_rrdata_rrsig) -> record_info(fields, dns_rrdata_rrsig); record_fields(dns_rrdata_soa) -> record_info(fields, dns_rrdata_soa); record_fields(dns_rrdata_srv) -> record_info(fields, dns_rrdata_srv); record_fields(dns_rrdata_txt) -> record_info(fields, dns_rrdata_txt); record_fields(dns_rrdata_hinfo) -> record_info(fields, dns_rrdata_hinfo); record_fields(dns_rrdata_eui48) -> record_info(fields, dns_rrdata_eui48); record_fields(dns_rrdata_eui64) -> record_info(fields, dns_rrdata_eui64); record_fields(dns_rrdata_ipseckey) -> record_info(fields, dns_rrdata_ipseckey); record_fields(dns_rrdata_loc) -> record_info(fields, dns_rrdata_loc); record_fields(dns_rrdata_mb) -> record_info(fields, dns_rrdata_mb); record_fields(dns_rrdata_mg) -> record_info(fields, dns_rrdata_mg); record_fields(dns_rrdata_minfo) -> record_info(fields, dns_rrdata_minfo); record_fields(dns_rrdata_mr) -> record_info(fields, dns_rrdata_mr); record_fields(dns_rrdata_nsec) -> record_info(fields, dns_rrdata_nsec); record_fields(dns_rrdata_nsec3) -> record_info(fields, dns_rrdata_nsec3); record_fields(dns_rrdata_nsec3param) -> record_info(fields, dns_rrdata_nsec3param); record_fields(dns_rrdata_csync) -> record_info(fields, dns_rrdata_csync); record_fields(dns_rrdata_dsync) -> record_info(fields, dns_rrdata_dsync); record_fields(dns_rrdata_tlsa) -> record_info(fields, dns_rrdata_tlsa); record_fields(dns_rrdata_smimea) -> record_info(fields, dns_rrdata_smimea); record_fields(dns_rrdata_nxt) -> record_info(fields, dns_rrdata_nxt); record_fields(dns_rrdata_rp) -> record_info(fields, dns_rrdata_rp); record_fields(dns_rrdata_rt) -> record_info(fields, dns_rrdata_rt); record_fields(dns_rrdata_spf) -> record_info(fields, dns_rrdata_spf); record_fields(dns_rrdata_sshfp) -> record_info(fields, dns_rrdata_sshfp); record_fields(dns_rrdata_svcb) -> record_info(fields, dns_rrdata_svcb); record_fields(dns_rrdata_https) -> record_info(fields, dns_rrdata_https); record_fields(dns_rrdata_naptr) -> record_info(fields, dns_rrdata_naptr); record_fields(dns_rrdata_ds) -> record_info(fields, dns_rrdata_ds); record_fields(dns_rrdata_cds) -> record_info(fields, dns_rrdata_cds); record_fields(dns_rrdata_dlv) -> record_info(fields, dns_rrdata_dlv); record_fields(dns_rrdata_zonemd) -> record_info(fields, dns_rrdata_zonemd); record_fields(dns_rrdata_cert) -> record_info(fields, dns_rrdata_cert); record_fields(dns_rrdata_tsig) -> record_info(fields, dns_rrdata_tsig); record_fields(dns_rrdata_uri) -> record_info(fields, dns_rrdata_uri); record_fields(dns_rrdata_resinfo) -> record_info(fields, dns_rrdata_resinfo); record_fields(dns_rrdata_wallet) -> record_info(fields, dns_rrdata_wallet); record_fields(dns_optrr) -> record_info(fields, dns_optrr); record_fields(dns_opt_llq) -> record_info(fields, dns_opt_llq); record_fields(dns_opt_nsid) -> record_info(fields, dns_opt_nsid); record_fields(dns_opt_owner) -> record_info(fields, dns_opt_owner); record_fields(dns_opt_ul) -> record_info(fields, dns_opt_ul); record_fields(dns_opt_ecs) -> record_info(fields, dns_opt_ecs); record_fields(dns_opt_cookie) -> record_info(fields, dns_opt_cookie); record_fields(dns_opt_ede) -> record_info(fields, dns_opt_ede); record_fields(dns_opt_unknown) -> record_info(fields, dns_opt_unknown); record_fields(Other) -> erlang:error({unknown_record_type, Other}). to_map_value(dns_message, questions, Value) when is_list(Value) -> [to_map(V) || V <- Value]; to_map_value(dns_message, answers, Value) when is_list(Value) -> [to_map(V) || V <- Value]; to_map_value(dns_message, authority, Value) when is_list(Value) -> [to_map(V) || V <- Value]; to_map_value(dns_message, additional, Value) when is_list(Value) -> [to_map(V) || V <- Value]; to_map_value(dns_optrr, data, Value) when is_list(Value) -> [to_map(V) || V <- Value]; to_map_value(Tag, ip, Value) when (Tag =:= dns_rrdata_a orelse Tag =:= dns_rrdata_aaaa) andalso is_tuple(Value) -> list_to_binary(inet:ntoa(Value)); to_map_value(Tag, public_key, Value) when is_list(Value) andalso (dns_rrdata_dnskey =:= Tag orelse dns_rrdata_cdnskey =:= Tag) -> base64:encode(to_map_dnskey_publickey(Value)); to_map_value(dns_rrdata_key, public_key, Value) when is_binary(Value) -> base64:encode(iolist_to_binary(Value)); to_map_value(Tag, salt, Value) when (Tag =:= dns_rrdata_nsec3 orelse Tag =:= dns_rrdata_nsec3param) andalso is_binary(Value) -> case Value of <<>> -> ~"-"; _ -> binary:encode_hex(Value) end; to_map_value(dns_rrdata_nsec3, hash, Value) when is_binary(Value) -> base32:encode(Value, [hex]); to_map_value(dns_rrdata_ipseckey, gateway, Value) when is_tuple(Value) -> list_to_binary(inet:ntoa(Value)); to_map_value(dns_rrdata_ipseckey, gateway, Value) when is_binary(Value) -> Value; to_map_value(Tag, svc_params, Value) when (Tag =:= dns_rrdata_svcb orelse Tag =:= dns_rrdata_https) andalso is_map(Value) -> dns_svcb_params:to_json(Value); to_map_value(dns_rrdata_cert, cert, Value) when is_binary(Value) -> base64:encode(Value); to_map_value(dns_rrdata_dhcid, data, Value) when is_binary(Value) -> base64:encode(Value); to_map_value(dns_rrdata_openpgpkey, data, Value) when is_binary(Value) -> base64:encode(Value); to_map_value(dns_rrdata_dnskey, public_key, Value) when is_binary(Value) -> base64:encode(Value); to_map_value(dns_rrdata_cdnskey, public_key, Value) when is_binary(Value) -> base64:encode(Value); to_map_value(dns_rrdata_ipseckey, public_key, Value) when is_binary(Value) -> base64:encode(Value); to_map_value(dns_rrdata_rrsig, signature, Value) when is_binary(Value) -> base64:encode(Value); to_map_value(dns_rrdata_tlsa, certificate, Value) when is_binary(Value) -> binary:encode_hex(Value); to_map_value(dns_rrdata_smimea, certificate, Value) when is_binary(Value) -> binary:encode_hex(Value); to_map_value(dns_rrdata_tsig, mac, Value) when is_binary(Value) -> base64:encode(Value); to_map_value(dns_rrdata_wallet, data, Value) when is_binary(Value) -> base64:encode(Value); to_map_value(dns_rrdata_ds, digest, Value) when is_binary(Value) -> binary:encode_hex(Value); to_map_value(dns_rrdata_cds, digest, Value) when is_binary(Value) -> binary:encode_hex(Value); to_map_value(dns_rrdata_dlv, digest, Value) when is_binary(Value) -> binary:encode_hex(Value); to_map_value(dns_rrdata_zonemd, hash, Value) when is_binary(Value) -> binary:encode_hex(Value); to_map_value(dns_rrdata_sshfp, fp, Value) when is_binary(Value) -> binary:encode_hex(Value); to_map_value(dns_rrdata_eui48, address, Value) when is_binary(Value) -> binary:encode_hex(Value); to_map_value(dns_rrdata_eui64, address, Value) when is_binary(Value) -> binary:encode_hex(Value); to_map_value(dns_rrdata_tsig, other, Value) when is_binary(Value) -> binary:encode_hex(Value); to_map_value(dns_opt_nsid, data, Value) when is_binary(Value) -> binary:encode_hex(Value); to_map_value(dns_opt_owner, _Field, Value) when is_binary(Value) -> binary:encode_hex(Value); to_map_value(dns_opt_ecs, _Field, Value) when is_binary(Value) -> binary:encode_hex(Value); to_map_value(dns_opt_unknown, bin, Value) when is_binary(Value) -> binary:encode_hex(Value); to_map_value(_Tag, _Field, Value) -> Value. %% Convert JSON-friendly values back to record field values from_map_field(dns_message, questions, Value) when is_list(Value) -> [from_map(V) || V <- Value]; from_map_field(dns_message, answers, Value) when is_list(Value) -> [from_map(V) || V <- Value]; from_map_field(dns_message, authority, Value) when is_list(Value) -> [from_map(V) || V <- Value]; from_map_field(dns_message, additional, Value) when is_list(Value) -> [from_map(V) || V <- Value]; from_map_field(dns_optrr, data, Value) when is_list(Value) -> [from_map(V) || V <- Value]; from_map_field(dns_rrdata_a, ip, Value) when is_binary(Value) -> case inet:parse_ipv4strict_address(binary_to_list(Value)) of {ok, Tuple} -> Tuple; {error, _} -> erlang:error({invalid_ip, Value}) end; from_map_field(dns_rrdata_aaaa, ip, Value) when is_binary(Value) -> case inet:parse_ipv6strict_address(binary_to_list(Value)) of {ok, Tuple} -> Tuple; {error, _} -> erlang:error({invalid_ip, Value}) end; from_map_field(dns_rrdata_dnskey, public_key, Value) when is_binary(Value) -> from_map_dnskey_publickey(base64:decode(Value)); from_map_field(dns_rrdata_cdnskey, public_key, Value) when is_binary(Value) -> from_map_dnskey_publickey(base64:decode(Value)); from_map_field(Tag, salt, ~"-") when Tag =:= dns_rrdata_nsec3 orelse Tag =:= dns_rrdata_nsec3param -> <<>>; from_map_field(dns_rrdata_nsec3, hash, Value) when is_binary(Value) -> base32:decode(Value, [hex]); from_map_field(dns_rrdata_ipseckey, gateway, Value) when is_binary(Value) -> case inet:parse_address(binary_to_list(Value)) of {ok, Tuple} -> Tuple; {error, _} -> Value end; %% Missing or null svc_params: default to empty map (alias form, no service params). from_map_field(Tag, svc_params, Value) when (Tag =:= dns_rrdata_svcb orelse Tag =:= dns_rrdata_https) andalso (Value =:= undefined orelse Value =:= null) -> #{}; from_map_field(Tag, svc_params, Value) when (Tag =:= dns_rrdata_svcb orelse Tag =:= dns_rrdata_https) andalso is_map(Value) -> dns_svcb_params:from_json(Value); from_map_field(Tag, Field, Value) when is_binary(Value) -> decode_field(Tag, Field, Value); from_map_field(_Tag, _Field, Value) -> Value. %% Decode binary fields based on tag and field name -spec decode_field(atom(), atom(), binary()) -> binary() | [integer()]. decode_field(dns_rrdata_cert, cert, Value) -> base64:decode(Value); decode_field(dns_rrdata_dhcid, data, Value) -> base64:decode(Value); decode_field(dns_rrdata_openpgpkey, data, Value) -> base64:decode(Value); decode_field(dns_rrdata_key, public_key, Value) -> base64:decode(Value); decode_field(dns_rrdata_ipseckey, public_key, Value) -> base64:decode(Value); decode_field(dns_rrdata_rrsig, signature, Value) -> base64:decode(Value); decode_field(dns_rrdata_tlsa, certificate, Value) -> binary:decode_hex(Value); decode_field(dns_rrdata_smimea, certificate, Value) -> binary:decode_hex(Value); decode_field(dns_rrdata_tsig, mac, Value) -> base64:decode(Value); decode_field(dns_rrdata_wallet, data, Value) -> base64:decode(Value); decode_field(dns_rrdata_ds, digest, Value) -> binary:decode_hex(Value); decode_field(dns_rrdata_cds, digest, Value) -> binary:decode_hex(Value); decode_field(dns_rrdata_dlv, digest, Value) -> binary:decode_hex(Value); decode_field(dns_rrdata_zonemd, hash, Value) -> binary:decode_hex(Value); decode_field(dns_rrdata_sshfp, fp, Value) -> binary:decode_hex(Value); decode_field(dns_rrdata_eui48, address, Value) -> binary:decode_hex(Value); decode_field(dns_rrdata_eui64, address, Value) -> binary:decode_hex(Value); decode_field(dns_rrdata_tsig, other, Value) -> binary:decode_hex(Value); decode_field(Tag, salt, Value) when Tag =:= dns_rrdata_nsec3 orelse Tag =:= dns_rrdata_nsec3param -> binary:decode_hex(Value); decode_field(dns_opt_nsid, data, Value) -> binary:decode_hex(Value); decode_field(dns_opt_owner, _Field, Value) -> binary:decode_hex(Value); decode_field(dns_opt_ecs, _Field, Value) -> binary:decode_hex(Value); decode_field(dns_opt_unknown, bin, Value) -> binary:decode_hex(Value); decode_field(_Tag, _Field, Value) -> Value. %% Helper to decode DNSKEY public key (handles both binary and list formats) -spec from_map_dnskey_publickey(binary()) -> binary() | [integer()]. from_map_dnskey_publickey(<<>>) -> []; from_map_dnskey_publickey(<>) -> [I | from_map_dnskey_publickey(Rest)]; from_map_dnskey_publickey(PK) -> PK. %% Handle list of integers (RSA key components) -spec to_map_dnskey_publickey([integer()]) -> binary(). to_map_dnskey_publickey(Ints) -> lists:foldl( fun(I, Acc) -> BI = binary:encode_unsigned(I), <> end, <<>>, Ints ).