diff options
Diffstat (limited to 'lib')
23 files changed, 608 insertions, 61 deletions
diff --git a/lib/crypto/c_src/crypto.c b/lib/crypto/c_src/crypto.c index 6e113ef39e..2c69dbb5ff 100644 --- a/lib/crypto/c_src/crypto.c +++ b/lib/crypto/c_src/crypto.c @@ -3071,10 +3071,11 @@ static ERL_NIF_TERM dh_generate_key_nif(ErlNifEnv* env, int argc, const ERL_NIF_ if ((dhkey = EVP_PKEY_new()) && (params = EVP_PKEY_new()) && EVP_PKEY_set1_DH(params, dh_params) /* set the key referenced by params to dh_params. - dh_params (and params) must be freed by us*/ + dh_params (and params) must be freed */ && (ctx = EVP_PKEY_CTX_new(params, NULL)) && EVP_PKEY_keygen_init(ctx) - && EVP_PKEY_keygen(ctx, &dhkey) + && EVP_PKEY_keygen(ctx, &dhkey) /* "performs a key generation operation, the + generated key is written to ppkey." (=last arg) */ && (dh_params = EVP_PKEY_get1_DH(dhkey)) /* return the referenced key. dh_params and dhkey must be freed */ ) { #else @@ -3108,8 +3109,8 @@ static ERL_NIF_TERM dh_generate_key_nif(ErlNifEnv* env, int argc, const ERL_NIF_ DH_free(dh_params); #ifdef HAS_EVP_PKEY_CTX if (ctx) EVP_PKEY_CTX_free(ctx); - /* if (dhkey) EVP_PKEY_free(dhkey); */ - /* if (params) EVP_PKEY_free(params); */ + if (dhkey) EVP_PKEY_free(dhkey); + if (params) EVP_PKEY_free(params); #endif return ret; } @@ -3207,7 +3208,7 @@ static ERL_NIF_TERM dh_compute_key_nif(ErlNifEnv* env, int argc, const ERL_NIF_T if (dh_pub) DH_free(dh_pub); #ifdef HAS_EVP_PKEY_CTX if (ctx) EVP_PKEY_CTX_free(ctx); - /* if (my_priv_key) EVP_PKEY_free(my_priv_key); */ + if (my_priv_key) EVP_PKEY_free(my_priv_key); /* if (peer_pub_key) EVP_PKEY_free(peer_pub_key); */ #endif return ret; diff --git a/lib/public_key/doc/src/public_key.xml b/lib/public_key/doc/src/public_key.xml index 7284da0499..5d57109140 100644 --- a/lib/public_key/doc/src/public_key.xml +++ b/lib/public_key/doc/src/public_key.xml @@ -986,10 +986,38 @@ fun(#'DistributionPoint'{}, #'CertificateList'{}, <p>The <c>ip</c> Reference ID takes an <seealso marker="inet:inet#type-ip_address">inet:ip_address()</seealso> or an ip address in string format (E.g "10.0.1.1" or "1234::5678:9012") as second element. </p> + <p>See <seealso marker="#pkix_verify_hostname_match_fun-1">pkix_verify_hostname_match_fun/1</seealso> for a + function that return a fun suitable for this option. + </p> </desc> </func> <func> + <name>pkix_verify_hostname_match_fun(Alg) -> fun(RefId | FQDN::string(), PresentedID) -> boolean() | default</name> + <fsummary>Returns a fun that is intendended as argument to the match_fun option in pkix_verify_hostname/3. + </fsummary> + <type> + <v>Alg = https</v> + <d>The algorithm for wich the fun should implement the special matching rules</d> + <v>RefId</v> + <d>See <seealso marker="#pkix_verify_hostname-3">pkix_verify_hostname/3</seealso>.</d> + <v>FQDN</v> + <d>See <seealso marker="#pkix_verify_hostname-3">pkix_verify_hostname/3</seealso>.</d> + <v>PresentedID</v> + <d>See <seealso marker="#pkix_verify_hostname-3">pkix_verify_hostname/3</seealso>.</d> + </type> + <desc> + <p>The return value of calling this function is intended to be used in the <c>match_fun</c> option in + <seealso marker="#pkix_verify_hostname-3">pkix_verify_hostname/3</seealso>. + </p> + <p>The returned fun augments the verify hostname matching according to the specific rules for + the protocol in the argument. + </p> + </desc> + </func> + + + <func> <name>sign(Msg, DigestType, Key) -> binary()</name> <name>sign(Msg, DigestType, Key, Options) -> binary()</name> <fsummary>Creates a digital signature.</fsummary> diff --git a/lib/public_key/src/public_key.erl b/lib/public_key/src/public_key.erl index 1c4acc9e1a..f2b57fd330 100644 --- a/lib/public_key/src/public_key.erl +++ b/lib/public_key/src/public_key.erl @@ -49,6 +49,7 @@ pkix_normalize_name/1, pkix_path_validation/3, pkix_verify_hostname/2, pkix_verify_hostname/3, + pkix_verify_hostname_match_fun/1, ssh_decode/2, ssh_encode/2, ssh_hostkey_fingerprint/1, ssh_hostkey_fingerprint/2, ssh_curvename2oid/1, oid2ssh_curvename/1, @@ -883,12 +884,23 @@ pkix_crls_validate(OtpCert, DPAndCRLs0, Options) -> Options, pubkey_crl:init_revokation_state()). %-------------------------------------------------------------------- --spec pkix_verify_hostname(Cert :: #'OTPCertificate'{} | binary(), - ReferenceIDs :: [{uri_id | dns_id | ip | srv_id | oid(), string()}]) -> boolean(). +-spec pkix_verify_hostname(#'OTPCertificate'{} | binary(), + referenceIDs() + ) -> boolean(). --spec pkix_verify_hostname(Cert :: #'OTPCertificate'{} | binary(), - ReferenceIDs :: [{uri_id | dns_id | ip | srv_id | oid(), string()}], - Options :: proplists:proplist()) -> boolean(). +-spec pkix_verify_hostname(#'OTPCertificate'{} | binary(), + referenceIDs(), + proplists:proplist()) -> boolean(). + +-type referenceIDs() :: [referenceID()] . +-type referenceID() :: {uri_id | dns_id | ip | srv_id | oid(), string()} . + +-spec pkix_verify_hostname_match_fun(high_level_alg()) -> match_fun() . + +-type high_level_alg() :: https . +-type match_fun() :: fun((ReferenceID::referenceID() | string(), + PresentedID::{atom()|oid(),string()}) -> match_fun_result() ) . +-type match_fun_result() :: boolean() | default . %% Description: Validates a hostname to RFC 6125 %%-------------------------------------------------------------------- @@ -953,6 +965,11 @@ pkix_verify_hostname(Cert = #'OTPCertificate'{tbsCertificate = TbsCert}, Referen end end. +pkix_verify_hostname_match_fun(https) -> + fun({dns_id,FQDN=[_|_]}, {dNSName,Name=[_|_]}) -> verify_hostname_match_wildcard(FQDN, Name); + (_, _) -> default + end. + %%-------------------------------------------------------------------- -spec ssh_decode(binary(), public_key | ssh_file()) -> [{public_key(), Attributes::list()}] ; (binary(), ssh2_pubkey) -> public_key() @@ -1516,9 +1533,7 @@ verify_hostname_match_default(Ref, Pres) -> verify_hostname_match_default0(FQDN=[_|_], {cn,FQDN}) -> not lists:member($*, FQDN); verify_hostname_match_default0(FQDN=[_|_], {cn,Name=[_|_]}) -> - [F1|Fs] = string:tokens(FQDN, "."), - [N1|Ns] = string:tokens(Name, "."), - match_wild(F1,N1) andalso Fs==Ns; + verify_hostname_match_wildcard(FQDN, Name); verify_hostname_match_default0({dns_id,R}, {dNSName,P}) -> R==P; verify_hostname_match_default0({uri_id,R}, {uniformResourceIdentifier,P}) -> @@ -1553,6 +1568,13 @@ verify_hostname_match_default0({srv_id,R}, {?srvName_OID,P}) -> verify_hostname_match_default0(_, _) -> false. + +verify_hostname_match_wildcard(FQDN, Name) -> + [F1|Fs] = string:tokens(FQDN, "."), + [N1|Ns] = string:tokens(Name, "."), + match_wild(F1,N1) andalso Fs==Ns. + + ok({ok,X}) -> X. l16_to_tup(L) -> list_to_tuple(l16_to_tup(L, [])). diff --git a/lib/public_key/test/public_key_SUITE.erl b/lib/public_key/test/public_key_SUITE.erl index 572748edc9..fcc9bdc080 100644 --- a/lib/public_key/test/public_key_SUITE.erl +++ b/lib/public_key/test/public_key_SUITE.erl @@ -310,7 +310,7 @@ init_ec_pem_encode_generated(Config) -> ec_pem_encode_generated() -> [{doc, "PEM-encode generated EC key"}]. -ec_pem_encode_generated(Config) -> +ec_pem_encode_generated(_Config) -> Key1 = public_key:generate_key({namedCurve, 'secp384r1'}), public_key:pem_entry_encode('ECPrivateKey', Key1), @@ -965,7 +965,7 @@ pkix_verify_hostname_cn(Config) -> %% openssl req -x509 -nodes -newkey rsa:1024 -keyout /dev/null -extensions SAN -config public_key_SUITE_data/verify_hostname.conf 2>/dev/null > public_key_SUITE_data/pkix_verify_hostname_subjAltName.pem %% %% Subject: C=SE, CN=example.com -%% Subject Alternative Name: DNS:kb.example.org, URI:http://www.example.org, URI:https://wws.example.org +%% Subject Alternative Name: DNS:kb.example.org, DNS:*.example.org, URI:http://www.example.org, URI:https://wws.example.org pkix_verify_hostname_subjAltName(Config) -> DataDir = proplists:get_value(data_dir, Config), @@ -984,7 +984,16 @@ pkix_verify_hostname_subjAltName(Config) -> {dns_id,"wws.example.org"}]), %% Check that a dns_id matches a DNS subjAltName: - true = public_key:pkix_verify_hostname(Cert, [{dns_id,"kb.example.org"}]). + true = public_key:pkix_verify_hostname(Cert, [{dns_id,"kb.example.org"}]), + + %% Check that a dns_id does not match a DNS subjAltName wiht wildcard + false = public_key:pkix_verify_hostname(Cert, [{dns_id,"other.example.org"}]), + + %% Check that a dns_id does nmatches a DNS subjAltName wiht wildcard with matchfun + true = public_key:pkix_verify_hostname(Cert, [{dns_id,"other.example.org"}], + [{match_fun, public_key:pkix_verify_hostname_match_fun(https)} + ] + ). %%-------------------------------------------------------------------- %% Uses the pem-file for pkix_verify_hostname_cn @@ -1351,7 +1360,7 @@ do_gen_ec_param(File) -> ct:fail({key_gen_fail, File}) end. -init_per_testcase_gen_ec_param(TC, Curve, Config) -> +init_per_testcase_gen_ec_param(_TC, Curve, Config) -> case crypto:ec_curves() of [] -> {skip, missing_ec_support}; @@ -1367,7 +1376,7 @@ init_per_testcase_gen_ec_param(TC, Curve, Config) -> end. -crypto_supported_curve(Curve, Curves) -> +crypto_supported_curve(Curve, _Curves) -> try crypto:generate_key(ecdh, Curve) of {error,_} -> false; % Just in case crypto is changed in the future... _-> true diff --git a/lib/public_key/test/public_key_SUITE_data/pkix_verify_hostname_subjAltName.pem b/lib/public_key/test/public_key_SUITE_data/pkix_verify_hostname_subjAltName.pem index 83e1ad37b3..7ab9ed7b96 100644 --- a/lib/public_key/test/public_key_SUITE_data/pkix_verify_hostname_subjAltName.pem +++ b/lib/public_key/test/public_key_SUITE_data/pkix_verify_hostname_subjAltName.pem @@ -1,14 +1,14 @@ -----BEGIN CERTIFICATE----- -MIICEjCCAXugAwIBAgIJANwliLph5EiAMA0GCSqGSIb3DQEBCwUAMCMxCzAJBgNV -BAYTAlNFMRQwEgYDVQQDEwtleGFtcGxlLmNvbTAeFw0xNjEyMjAxNTEyMjRaFw0x -NzAxMTkxNTEyMjRaMCMxCzAJBgNVBAYTAlNFMRQwEgYDVQQDEwtleGFtcGxlLmNv -bTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAydstIN157w8QxkVaOl3wm81j -fgZ8gqO3BXkECPF6bw5ewLlmePL6Qs4RypsaRe7cKJ9rHFlwhpdcYkxWSWEt2N7Z -Ry3N4SjuU04ohWbYgy3ijTt7bJg7jOV1Dh56BnI4hwhQj0oNFizNZOeRRfEzdMnS -+uk03t/Qre2NS7KbwnUCAwEAAaNOMEwwSgYDVR0RBEMwQYIOa2IuZXhhbXBsZS5v -cmeGFmh0dHA6Ly93d3cuZXhhbXBsZS5vcmeGF2h0dHBzOi8vd3dzLmV4YW1wbGUu -b3JnMA0GCSqGSIb3DQEBCwUAA4GBAKqFqW5gCso422bXriCBJoygokOTTOw1Rzpq -K8Mm0B8W9rrW9OTkoLEcjekllZcUCZFin2HovHC5HlHZz+mQvBI1M6sN2HVQbSzS -EgL66U9gwJVnn9/U1hXhJ0LO28aGbyE29DxnewNR741dWN3oFxCdlNaO6eMWaEsO -gduJ5sDl +MIICITCCAYqgAwIBAgIJAP31suf/Fi4oMA0GCSqGSIb3DQEBCwUAMCMxCzAJBgNV +BAYTAlNFMRQwEgYDVQQDEwtleGFtcGxlLmNvbTAeFw0xODA1MTcxMDIzNDBaFw0x +ODA2MTYxMDIzNDBaMCMxCzAJBgNVBAYTAlNFMRQwEgYDVQQDEwtleGFtcGxlLmNv +bTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAsUVMXSM4Q6vYp7H4Svsfv4QQ +dmUD3IdTbtumlyAqLZuc6Z0HU9IOE0wpF97+5AE3moHluwN/MtSX/fb9oxCjh3L6 +iDla770uUoIgiWkA9lyzuYXt7zGsqc0EmGMJRAHp4jOxI26U/C8wdXoyZsGD8GPr +hYAI2Me4CkdDqCoRuUUCAwEAAaNdMFswWQYDVR0RBFIwUIIOa2IuZXhhbXBsZS5v +cmeCDSouZXhhbXBsZS5vcmeGFmh0dHA6Ly93d3cuZXhhbXBsZS5vcmeGF2h0dHBz +Oi8vd3dzLmV4YW1wbGUub3JnMA0GCSqGSIb3DQEBCwUAA4GBAKs8vWMqpXiuFhcq +6W1dMrVB4tuDjt1Ctr3g2USXBLgm8NxsZzslFyDnrvtZY0hbjcAkGKMMhy8lFD5t ++GjBbyp7MKII6vJaVvc+wbrsbNdvioB1puGwbgVhgD3Kb79do9h6JrNncjMvBN7j +VK6BUB8TUofFmztMjoPlxFOs/7qK -----END CERTIFICATE----- diff --git a/lib/public_key/test/public_key_SUITE_data/verify_hostname.conf b/lib/public_key/test/public_key_SUITE_data/verify_hostname.conf index a28864dc78..6b4e4f284e 100644 --- a/lib/public_key/test/public_key_SUITE_data/verify_hostname.conf +++ b/lib/public_key/test/public_key_SUITE_data/verify_hostname.conf @@ -10,7 +10,8 @@ CN=example.com subjectAltName = @alt_names [alt_names] -DNS = kb.example.org +DNS.1 = kb.example.org +DNS.2 = *.example.org URI.1 = http://www.example.org URI.2 = https://wws.example.org diff --git a/lib/ssh/src/ssh_client_channel.erl b/lib/ssh/src/ssh_client_channel.erl index 134d3f08bd..8b5e196412 100644 --- a/lib/ssh/src/ssh_client_channel.erl +++ b/lib/ssh/src/ssh_client_channel.erl @@ -180,6 +180,8 @@ init([Options]) -> {stop, Why} -> {stop, Why} catch + _:undef -> + {stop, {bad_channel_callback_module,Cb}}; _:Reason -> {stop, Reason} end. diff --git a/lib/ssh/src/ssh_connection.erl b/lib/ssh/src/ssh_connection.erl index ed03b4e2ed..dad7636e3f 100644 --- a/lib/ssh/src/ssh_connection.erl +++ b/lib/ssh/src/ssh_connection.erl @@ -498,25 +498,24 @@ handle_msg(#ssh_msg_channel_request{recipient_channel = ChannelId, data = Data}, #connection{channel_cache = Cache} = Connection, server) -> <<?DEC_BIN(SsName,_SsLen)>> = Data, - - #channel{remote_id = RemoteId} = Channel0 = + #channel{remote_id=RemoteId} = Channel = ssh_client_channel:cache_lookup(Cache, ChannelId), - - ReplyMsg = {subsystem, ChannelId, WantReply, binary_to_list(SsName)}, - - try - {ok, Pid} = start_subsystem(SsName, Connection, Channel0, ReplyMsg), - erlang:monitor(process, Pid), - Channel = Channel0#channel{user = Pid}, - ssh_client_channel:cache_update(Cache, Channel), - Reply = {connection_reply, - channel_success_msg(RemoteId)}, - {[Reply], Connection} - catch - _:_ -> - ErrorReply = {connection_reply, channel_failure_msg(RemoteId)}, - {[ErrorReply], Connection} - end; + Reply = + try + start_subsystem(SsName, Connection, Channel, + {subsystem, ChannelId, WantReply, binary_to_list(SsName)}) + of + {ok, Pid} -> + erlang:monitor(process, Pid), + ssh_client_channel:cache_update(Cache, Channel#channel{user=Pid}), + channel_success_msg(RemoteId); + {error,_Error} -> + channel_failure_msg(RemoteId) + catch + _:_ -> + channel_failure_msg(RemoteId) + end, + {[{connection_reply,Reply}], Connection}; handle_msg(#ssh_msg_channel_request{request_type = "subsystem"}, Connection, client) -> @@ -822,7 +821,12 @@ start_channel(Cb, Id, Args, SubSysSup, Exec, Opts) -> ChannelSup = ssh_subsystem_sup:channel_supervisor(SubSysSup), case max_num_channels_not_exceeded(ChannelSup, Opts) of true -> - ssh_server_channel_sup:start_child(ChannelSup, Cb, Id, Args, Exec); + case ssh_server_channel_sup:start_child(ChannelSup, Cb, Id, Args, Exec) of + {error,{Error,_Info}} -> + throw(Error); + Others -> + Others + end; false -> throw(max_num_channels_exceeded) end. diff --git a/lib/ssh/src/ssh_connection_handler.erl b/lib/ssh/src/ssh_connection_handler.erl index f1ff3a70e2..3e224fe13f 100644 --- a/lib/ssh/src/ssh_connection_handler.erl +++ b/lib/ssh/src/ssh_connection_handler.erl @@ -1345,11 +1345,11 @@ handle_event(info, {Proto, Sock, NewData}, StateName, D0 = #data{socket = Sock, {next_event, internal, Msg} ]} catch - C:E -> + C:E:ST -> {Shutdown, D} = ?send_disconnect(?SSH_DISCONNECT_PROTOCOL_ERROR, io_lib:format("Bad packet: Decrypted, but can't decode~n~p:~p~n~p", - [C,E,erlang:get_stacktrace()]), + [C,E,ST]), StateName, D1), {stop, Shutdown, D} end; @@ -1378,10 +1378,10 @@ handle_event(info, {Proto, Sock, NewData}, StateName, D0 = #data{socket = Sock, StateName, D0), {stop, Shutdown, D} catch - C:E -> + C:E:ST -> {Shutdown, D} = ?send_disconnect(?SSH_DISCONNECT_PROTOCOL_ERROR, - io_lib:format("Bad packet: Couldn't decrypt~n~p:~p~n~p",[C,E,erlang:get_stacktrace()]), + io_lib:format("Bad packet: Couldn't decrypt~n~p:~p~n~p",[C,E,ST]), StateName, D0), {stop, Shutdown, D} end; diff --git a/lib/ssh/test/Makefile b/lib/ssh/test/Makefile index 0a99d31a63..9832a9b210 100644 --- a/lib/ssh/test/Makefile +++ b/lib/ssh/test/Makefile @@ -36,6 +36,7 @@ MODULES= \ ssh_options_SUITE \ ssh_basic_SUITE \ ssh_bench_SUITE \ + ssh_chan_behaviours_SUITE \ ssh_compat_SUITE \ ssh_connection_SUITE \ ssh_dbg_SUITE \ @@ -53,6 +54,8 @@ MODULES= \ ssh_key_cb_options \ ssh_key_cb_engine_keys \ ssh_trpt_test_lib \ + ssh_chan_behaviours_client \ + ssh_chan_behaviours_server \ ssh_echo_server \ ssh_bench_dev_null \ ssh_peername_sockname_server \ diff --git a/lib/ssh/test/ssh_chan_behaviours_SUITE.erl b/lib/ssh/test/ssh_chan_behaviours_SUITE.erl new file mode 100644 index 0000000000..16ed152bcd --- /dev/null +++ b/lib/ssh/test/ssh_chan_behaviours_SUITE.erl @@ -0,0 +1,152 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2018-2018. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%% +%% %CopyrightEnd% +%% + +%% + +-module(ssh_chan_behaviours_SUITE). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("ssh/src/ssh.hrl"). +-include("ssh_test_lib.hrl"). + +%% Note: This directive should only be used in test suites. +-compile(export_all). + +%%-------------------------------------------------------------------- +%% Common Test interface functions ----------------------------------- +%%-------------------------------------------------------------------- + +suite() -> + [{ct_hooks,[ts_install_cth]}, + {timetrap,{seconds,60}}]. + +all() -> + [ + noexist_subsystem, + undefined_subsystem, + defined_subsystem, + subsystem_client + ]. + +%%-------------------------------------------------------------------- +init_per_suite(Config) -> + ?CHECK_CRYPTO( + begin + ssh:start(), + Config + end). + +end_per_suite(_Config) -> + {Time,R} = timer:tc(ssh, stop, []), + ct:log("Stop ssh: ~p ms",[(100*(Time div 1000)) / 100]), + R. + +init_per_testcase(_TC, Config) -> + SubSystems = [ + {"bad_cb", {ssh_chan_behaviours_undefined, []}}, % A non-existing file + {"ch1", {ssh_chan_behaviours_server, [self(),true]}} + ], + {Pid, Host, Port} = ssh_test_lib:std_daemon(Config, [{subsystems,SubSystems}]), + C = ssh_test_lib:std_connect(Config, Host, Port, []), + [{connref,C}, {daemon_pid,Pid}| Config]. + +end_per_testcase(_TC, Config) -> + {Time,_} = timer:tc(ssh, stop_daemon, [proplists:get_value(daemon_pid,Config)]), + ct:log("Stop daemon: ~p ms",[(100*(Time div 1000)) / 100]), + case flush() of + [] -> ok; + Msgs -> ct:pal("Unhandled messages:~n~p", [Msgs]) + end. + + +-define(EXPECT(What, Bind), + Bind = + (fun() -> + receive What -> + ct:log("~p:~p ~p got ~p",[?MODULE,?LINE,self(),What]), + Bind + after 5000 -> + ct:log("~p:~p ~p Flushed:~n~p",[?MODULE,?LINE,self(),flush()]), + ct:fail("Timeout!",[]) + end + end)() + ). + +%%-------------------------------------------------------------------- +%% Test Cases -------------------------------------------------------- +%%-------------------------------------------------------------------- +%% Try start a subsystem whos name is not known by the server +noexist_subsystem(Config) -> + C = proplists:get_value(connref, Config), + {ok, Ch} = ssh_connection:session_channel(C, infinity), + failure = ssh_connection:subsystem(C, Ch, "noexist", infinity), + ok = ssh_connection:close(C, Ch), + ?EXPECT({ssh_cm,C,{closed,Ch}},[]), + ok. + +%% Try to start a subsystem with a known name, but without any callback file +undefined_subsystem(Config) -> + C = proplists:get_value(connref, Config), + {ok, Ch} = ssh_connection:session_channel(C, infinity), + failure = ssh_connection:subsystem(C, Ch, "bad_cb", infinity), + ok = ssh_connection:close(C, Ch), + ?EXPECT({ssh_cm,C,{closed,Ch}},[]), % self() is instead of a proper channel handler + ok. + +%% Try to start and stop a subsystem with known name and defined callback file +defined_subsystem(Config) -> + C = proplists:get_value(connref, Config), + {ok, Ch1} = ssh_connection:session_channel(C, infinity), + + success = ssh_connection:subsystem(C, Ch1, "ch1", infinity), + IDsrv = ?EXPECT({{_Csrv,_Ch1srv}, {ssh_channel_up,_Ch1srv,_Csrv}}, {_Csrv,_Ch1srv}), + + ok = ssh_connection:close(C, Ch1), + ?EXPECT({IDsrv, {terminate,normal}}, []), + ?EXPECT({ssh_cm, C, {closed,Ch1}}, []), % self() is instead of a proper channel handler + ok. + +%% Try to start and stop a subsystem from a ssh_client_channel behviour +subsystem_client(Config) -> + C = proplists:get_value(connref, Config), + + {ok,ChRef} = ssh_chan_behaviours_client:start_link(C), + IDclt = ?EXPECT({{C,Ch1clt}, {ssh_channel_up,Ch1clt,C}}, {C,Ch1clt}), + IDsrv = ?EXPECT({{_Csrv,Ch1srv}, {ssh_channel_up,Ch1srv,_Csrv}}, {_Csrv,Ch1srv}), + + ok = ssh_chan_behaviours_client:stop(ChRef), + ?EXPECT({IDclt, {terminate,normal}}, []), % From the proper channel handler + ?EXPECT({IDsrv, {terminate,normal}}, []), + ok. + +%%%================================================================ +%%% +%%% + +flush() -> lists:reverse(flush([])). + +flush(Acc) -> + receive + M -> + flush([M|Acc]) + after 0 -> + Acc + end. + diff --git a/lib/ssh/test/ssh_chan_behaviours_SUITE_data/ssh_host_dsa_key b/lib/ssh/test/ssh_chan_behaviours_SUITE_data/ssh_host_dsa_key new file mode 100644 index 0000000000..51ab6fbd88 --- /dev/null +++ b/lib/ssh/test/ssh_chan_behaviours_SUITE_data/ssh_host_dsa_key @@ -0,0 +1,13 @@ +-----BEGIN DSA PRIVATE KEY----- +MIIBuwIBAAKBgQCClaHzE2ul0gKSUxah5W0W8UiJLy4hXngKEqpaUq9SSdVdY2LK +wVfKH1gt5iuaf1FfzOhsIC9G/GLnjYttXZc92cv/Gfe3gR+s0ni2++MX+T++mE/Q +diltXv/Hp27PybS67SmiFW7I+RWnT2OKlMPtw2oUuKeztCe5UWjaj/y5FQIVAPLA +l9RpiU30Z87NRAHY3NTRaqtrAoGANMRxw8UfdtNVR0CrQj3AgPaXOGE4d+G4Gp4X +skvnCHycSVAjtYxebUkzUzt5Q6f/IabuLUdge3gXrc8BetvrcKbp+XZgM0/Vj2CF +Ymmy3in6kzGZq7Fw1sZaku6AOU8vLa5woBT2vAcHLLT1bLAzj7viL048T6MfjrOP +ef8nHvACgYBhDWFQJ1mf99sg92LalVq1dHLmVXb3PTJDfCO/Gz5NFmj9EZbAtdah +/XcF3DeRF+eEoz48wQF/ExVxSMIhLdL+o+ElpVhlM7Yii+T7dPhkQfEul6zZXu+U +ykSTXYUbtsfTNRFQGBW2/GfnEc0mnIxfn9v10NEWMzlq5z9wT9P0CgIVAN4wtL5W +Lv62jKcdskxNyz2NQoBx +-----END DSA PRIVATE KEY----- + diff --git a/lib/ssh/test/ssh_chan_behaviours_SUITE_data/ssh_host_dsa_key.pub b/lib/ssh/test/ssh_chan_behaviours_SUITE_data/ssh_host_dsa_key.pub new file mode 100644 index 0000000000..4dbb1305b0 --- /dev/null +++ b/lib/ssh/test/ssh_chan_behaviours_SUITE_data/ssh_host_dsa_key.pub @@ -0,0 +1,11 @@ +---- BEGIN SSH2 PUBLIC KEY ---- +AAAAB3NzaC1kc3MAAACBAIKVofMTa6XSApJTFqHlbRbxSIkvLiFeeAoSqlpSr1JJ1V1j +YsrBV8ofWC3mK5p/UV/M6GwgL0b8YueNi21dlz3Zy/8Z97eBH6zSeLb74xf5P76YT9B2 +KW1e/8enbs/JtLrtKaIVbsj5FadPY4qUw+3DahS4p7O0J7lRaNqP/LkVAAAAFQDywJfU +aYlN9GfOzUQB2NzU0WqrawAAAIA0xHHDxR9201VHQKtCPcCA9pc4YTh34bganheyS+cI +fJxJUCO1jF5tSTNTO3lDp/8hpu4tR2B7eBetzwF62+twpun5dmAzT9WPYIViabLeKfqT +MZmrsXDWxlqS7oA5Ty8trnCgFPa8BwcstPVssDOPu+IvTjxPox+Os495/yce8AAAAIBh +DWFQJ1mf99sg92LalVq1dHLmVXb3PTJDfCO/Gz5NFmj9EZbAtdah/XcF3DeRF+eEoz48 +wQF/ExVxSMIhLdL+o+ElpVhlM7Yii+T7dPhkQfEul6zZXu+UykSTXYUbtsfTNRFQGBW2 +/GfnEc0mnIxfn9v10NEWMzlq5z9wT9P0Cg== +---- END SSH2 PUBLIC KEY ---- diff --git a/lib/ssh/test/ssh_chan_behaviours_SUITE_data/ssh_host_ecdsa_key b/lib/ssh/test/ssh_chan_behaviours_SUITE_data/ssh_host_ecdsa_key new file mode 100644 index 0000000000..fb1a862ded --- /dev/null +++ b/lib/ssh/test/ssh_chan_behaviours_SUITE_data/ssh_host_ecdsa_key @@ -0,0 +1,6 @@ +-----BEGIN EC PRIVATE KEY----- +MIGkAgEBBDArxbDfh3p1okrD9wQw6jJ4d4DdlBPD5GqXE8bIeRJiK41Sh40LgvPw +mkqEDSXK++CgBwYFK4EEACKhZANiAAScl43Ih2lWTDKrSox5ve5uiTXil4smsup3 +CfS1XPjKxgBAmlfBim8izbdrT0BFdQzz2joduNMtpt61wO4rGs6jm0UP7Kim9PC7 +Hneb/99fIYopdMH5NMnk60zGO1uZ2vc= +-----END EC PRIVATE KEY----- diff --git a/lib/ssh/test/ssh_chan_behaviours_SUITE_data/ssh_host_ecdsa_key.pub b/lib/ssh/test/ssh_chan_behaviours_SUITE_data/ssh_host_ecdsa_key.pub new file mode 100644 index 0000000000..428d5fb7d7 --- /dev/null +++ b/lib/ssh/test/ssh_chan_behaviours_SUITE_data/ssh_host_ecdsa_key.pub @@ -0,0 +1 @@ +ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBJyXjciHaVZMMqtKjHm97m6JNeKXiyay6ncJ9LVc+MrGAECaV8GKbyLNt2tPQEV1DPPaOh240y2m3rXA7isazqObRQ/sqKb08Lsed5v/318hiil0wfk0yeTrTMY7W5na9w== uabhnil@elxadlj3q32 diff --git a/lib/ssh/test/ssh_chan_behaviours_SUITE_data/ssh_host_rsa_key b/lib/ssh/test/ssh_chan_behaviours_SUITE_data/ssh_host_rsa_key new file mode 100644 index 0000000000..79968bdd7d --- /dev/null +++ b/lib/ssh/test/ssh_chan_behaviours_SUITE_data/ssh_host_rsa_key @@ -0,0 +1,16 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXQIBAAKBgQDCZX+4FBDwZIh9y/Uxee1VJnEXlowpz2yDKwj8semM4q843337 +zbNfxHmladB1lpz2NqyxI175xMIJuDxogyZdsOxGnFAzAnthR4dqL/RWRWzjaxSB +6IAO9SPYVVlrpZ+1hsjLW79fwXK/yc8VdhRuWTeQiRgYY2ek8+OKbOqz4QIDAQAB +AoGANmvJzJO5hkLuvyDZHKfAnGTtpifcR1wtSa9DjdKUyn8vhKF0mIimnbnYQEmW +NUUb3gXCZLi9PvkpRSVRrASDOZwcjoU/Kvww163vBUVb2cOZfFhyn6o2Sk88Tt++ +udH3hdjpf9i7jTtUkUe+QYPsia+wgvvrmn4QrahLAH86+kECQQDx5gFeXTME3cnW +WMpFz3PPumduzjqgqMMWEccX4FtQkMX/gyGa5UC7OHFyh0N/gSWvPbRHa8A6YgIt +n8DO+fh5AkEAzbqX4DOn8NY6xJIi42q7l/2jIA0RkB6P7YugW5NblhqBZ0XDnpA5 +sMt+rz+K07u9XZtxgh1xi7mNfwY6lEAMqQJBAJBEauCKmRj35Z6OyeQku59SPsnY ++SJEREVvSNw2lH9SOKQQ4wPsYlTGbvKtNVZgAcen91L5MmYfeckYE/fdIZECQQCt +64zxsTnM1I8iFxj/gP/OYlJBikrKt8udWmjaghzvLMEw+T2DExJyb9ZNeT53+UMB +m6O+B/4xzU/djvp+0hbhAkAemIt+rA5kTmYlFndhpvzkSSM8a2EXsO4XIPgGWCTT +tQKS/tTly0ADMjN/TVy11+9d6zcqadNVuHXHGtR4W0GR +-----END RSA PRIVATE KEY----- + diff --git a/lib/ssh/test/ssh_chan_behaviours_SUITE_data/ssh_host_rsa_key.pub b/lib/ssh/test/ssh_chan_behaviours_SUITE_data/ssh_host_rsa_key.pub new file mode 100644 index 0000000000..75d2025c71 --- /dev/null +++ b/lib/ssh/test/ssh_chan_behaviours_SUITE_data/ssh_host_rsa_key.pub @@ -0,0 +1,5 @@ +---- BEGIN SSH2 PUBLIC KEY ---- +AAAAB3NzaC1yc2EAAAADAQABAAAAgQDCZX+4FBDwZIh9y/Uxee1VJnEXlowpz2yDKwj8 +semM4q843337zbNfxHmladB1lpz2NqyxI175xMIJuDxogyZdsOxGnFAzAnthR4dqL/RW +RWzjaxSB6IAO9SPYVVlrpZ+1hsjLW79fwXK/yc8VdhRuWTeQiRgYY2ek8+OKbOqz4Q== +---- END SSH2 PUBLIC KEY ---- diff --git a/lib/ssh/test/ssh_chan_behaviours_client.erl b/lib/ssh/test/ssh_chan_behaviours_client.erl new file mode 100644 index 0000000000..07ac21ba97 --- /dev/null +++ b/lib/ssh/test/ssh_chan_behaviours_client.erl @@ -0,0 +1,143 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2005-2016. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%% +%% %CopyrightEnd% +%% + +%% + +%%% Description: Example ssh client +-module(ssh_chan_behaviours_client). +-behaviour(ssh_client_channel). +-record(state, { + parent, + cm, + ch, + dbg + }). +-export([start_link/1, start/1, + stop/1, send_eof/1, + init/1, handle_msg/2, handle_ssh_msg/2, terminate/2, + code_change/3, handle_call/3, handle_cast/2 + ]). + +-define(DBG(State,Fmt,Args), + case State#state.dbg of + true -> ct:log("~p:~p ~p C=~p Ch=~p "++Fmt, + [?MODULE,?LINE,self(),State#state.cm,State#state.ch|Args]); + false -> ok + end). + + +start_link(C) -> + {ok, Ch} = ssh_connection:session_channel(C, infinity), + ssh_client_channel:start_link(C, Ch, ssh_chan_behaviours_client, [C, Ch, self(), true]). + +start(C) -> + {ok, Ch} = ssh_connection:session_channel(C, infinity), + ssh_client_channel:start(C, Ch, ssh_chan_behaviours_client, [C, Ch, self(), true]). + +send_eof(ChRef) -> + ssh_client_channel:call(ChRef, send_eof). + +stop(ChRef) -> + ssh_client_channel:call(ChRef, stop). + + +init([C, Ch, Parent, Dbg|_Exec]) -> + case ssh_connection:subsystem(C, Ch, "ch1", infinity) of + success -> + State = #state{cm = C, + ch = Ch, + parent=Parent, + dbg=Dbg}, + ?DBG(State, "callback spawned, parent = ~p", [Parent]), + {ok, State}; + + Other -> + {stop, Other} + end. + +handle_msg({ssh_channel_up, ChannelId, ConnectionManager}=M, State0) -> + State = State0#state{cm = ConnectionManager, + ch = ChannelId}, + tell_parent(M, State), + ?DBG(State, "ssh_channel_up",[]), + {ok, State}. + +handle_ssh_msg({ssh_cm, C, {data, Ch, 0, Data}}=M, #state{ch=Ch,cm=C} = State) -> + tell_parent(M, State), + ?DBG(State, "ssh_cm data size(Data)=~p",[size(Data)]), + {ok, State}; + +handle_ssh_msg({ssh_cm, C, {data, Ch, Type, Data}}=M, #state{ch=Ch,cm=C} = State) -> + tell_parent(M, State), + ?DBG(State, "ssh_cm data Type=~p : ~p",[Type,Data]), + {ok, State}; + +handle_ssh_msg({ssh_cm, C, {eof, Ch}}=M, #state{ch=Ch,cm=C} = State) -> + tell_parent(M, State), + ?DBG(State, "eof",[]), + {ok, State}; + +handle_ssh_msg({ssh_cm, C, {signal, _Ch, _SigNameStr}=Sig} = M, #state{ch=Ch,cm=C} = State) -> + %% Ignore signals according to RFC 4254 section 6.9. + tell_parent(M, State), + ?DBG(State, "~p",[Sig]), + {ok, State}; + +handle_ssh_msg({ssh_cm, C, {exit_signal, Ch, _, _Error, _}=Sig}=M, #state{ch=Ch,cm=C} = State) -> + tell_parent(M, State), + ?DBG(State, "~p",[Sig]), + {stop, Ch, State}; + +handle_ssh_msg({ssh_cm, C, {exit_status, Ch, _Status}=Sig}=M, #state{ch=Ch,cm=C} = State) -> + tell_parent(M, State), + ?DBG(State, "~p",[Sig]), + {stop, Ch, State}. + + +handle_call(send_eof, _From,#state{ch=Ch,cm=C} = State) -> + {reply, ssh_connection:send_eof(C,Ch), State}; + +handle_call(stop, _From, State) -> + {stop, normal, ok, State}; + +handle_call(Msg, _From, State) -> + ?DBG(State, "Unknown call ~p", [Msg]), + {reply, {unknown_call,Msg}, State}. + + +terminate(Reason, State) -> + tell_parent({terminate,Reason}, State), + ?DBG(State, "terminate Reason = ~p",[Reason]). + + +handle_cast(Msg, State) -> + ?DBG(State, "Unknown cast ~p", [Msg]), + {noreply, State}. + +code_change(_OldVsn, State, _Extra) -> {ok, State}. + +%%%================================================================ +%%% +%%% + +tell_parent(Msg, #state{parent = Parent, + cm = C, + ch = Ch}) -> Parent ! {{C,Ch}, Msg}. + diff --git a/lib/ssh/test/ssh_chan_behaviours_server.erl b/lib/ssh/test/ssh_chan_behaviours_server.erl new file mode 100644 index 0000000000..a5ec19e0cf --- /dev/null +++ b/lib/ssh/test/ssh_chan_behaviours_server.erl @@ -0,0 +1,96 @@ +%% +%% %CopyrightBegin% +%% +%% Copyright Ericsson AB 2005-2016. All Rights Reserved. +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License. +%% +%% %CopyrightEnd% +%% + +%% + +%%% Description: Example ssh server +-module(ssh_chan_behaviours_server). +-behaviour(ssh_server_channel). +-record(state, { + parent, + cm, + ch, + dbg + }). +-export([init/1, handle_msg/2, handle_ssh_msg/2, terminate/2]). + +-define(DBG(State,Fmt,Args), + case State#state.dbg of + true -> ct:log("~p:~p ~p C=~p Ch=~p "++Fmt, + [?MODULE,?LINE,self(),State#state.cm,State#state.ch|Args]); + false -> ok + end). + + +init([Pid,Dbg|_Exec]) -> + {ok, #state{parent=Pid, + dbg=Dbg}}. + +handle_msg({ssh_channel_up, ChannelId, ConnectionManager}=M, State0) -> + State = State0#state{cm = ConnectionManager, + ch = ChannelId}, + tell_parent(M, State), + ?DBG(State, "ssh_channel_up",[]), + {ok, State}. + +handle_ssh_msg({ssh_cm, C, {data, Ch, 0, Data}}=M, #state{ch=Ch,cm=C} = State) -> + tell_parent(M, State), + ?DBG(State, "ssh_cm data size(Data)=~p",[size(Data)]), + {ok, State}; + +handle_ssh_msg({ssh_cm, C, {data, Ch, Type, Data}}=M, #state{ch=Ch,cm=C} = State) -> + tell_parent(M, State), + ?DBG(State, "ssh_cm data Type=~p : ~p",[Type,Data]), + {ok, State}; + +handle_ssh_msg({ssh_cm, C, {eof, Ch}}=M, #state{ch=Ch,cm=C} = State) -> + tell_parent(M, State), + ?DBG(State, "eof",[]), + {ok, State}; + +handle_ssh_msg({ssh_cm, C, {signal, _Ch, _SigNameStr}=Sig} = M, #state{ch=Ch,cm=C} = State) -> + %% Ignore signals according to RFC 4254 section 6.9. + tell_parent(M, State), + ?DBG(State, "~p",[Sig]), + {ok, State}; + +handle_ssh_msg({ssh_cm, C, {exit_signal, Ch, _, _Error, _}=Sig}=M, #state{ch=Ch,cm=C} = State) -> + tell_parent(M, State), + ?DBG(State, "~p",[Sig]), + {stop, Ch, State}; + +handle_ssh_msg({ssh_cm, C, {exit_status, Ch, _Status}=Sig}=M, #state{ch=Ch,cm=C} = State) -> + tell_parent(M, State), + ?DBG(State, "~p",[Sig]), + {stop, Ch, State}. + +terminate(Reason, State) -> + tell_parent({terminate,Reason}, State), + ?DBG(State, "terminate Reason = ~p",[Reason]), + ok. + +%%%================================================================ +%%% +%%% + +tell_parent(Msg, #state{parent = Parent, + cm = C, + ch = Ch}) -> Parent ! {{C,Ch}, Msg}. + diff --git a/lib/stdlib/doc/src/calendar.xml b/lib/stdlib/doc/src/calendar.xml index 8f2b6b747a..6b4fa7f98a 100644 --- a/lib/stdlib/doc/src/calendar.xml +++ b/lib/stdlib/doc/src/calendar.xml @@ -323,7 +323,9 @@ <type name="rfc3339_string"/> <type name="rfc3339_time_unit"/> <desc> - <p>Converts an RFC 3339 timestamp into system time.</p> + <p>Converts an RFC 3339 timestamp into system time. The data format + of RFC 3339 timestamps is described by + <url href="https://www.ietf.org/rfc/rfc3339.txt">RFC 3339</url>.</p> <p>Valid option:</p> <taglist> <tag><c>{unit, Unit}</c></tag> @@ -378,7 +380,10 @@ <type name="rfc3339_string"/> <type name="rfc3339_time_unit"/> <desc> - <p>Converts a system time into RFC 3339 timestamp.</p> + <p>Converts a system time into an RFC 3339 timestamp. The data format + of RFC 3339 timestamps is described by + <url href="https://www.ietf.org/rfc/rfc3339.txt">RFC 3339</url>. + The data format of offsets is also described by RFC 3339.</p> <p>Valid options:</p> <taglist> <tag><c>{offset, Offset}</c></tag> diff --git a/lib/stdlib/src/io.erl b/lib/stdlib/src/io.erl index f510f61e9f..5d5773c80c 100644 --- a/lib/stdlib/src/io.erl +++ b/lib/stdlib/src/io.erl @@ -86,7 +86,16 @@ put_chars(Chars) -> CharData :: unicode:chardata(). put_chars(Io, Chars) -> - o_request(Io, {put_chars,unicode,Chars}, put_chars). + put_chars(Io, unicode, Chars). + +%% This function is here to make the erlang:raise in o_request actually raise to +%% a valid function. +-spec put_chars(IoDevice, Encoding, CharData) -> 'ok' when + IoDevice :: device(), + Encoding :: unicode, + CharData :: unicode:chardata(). +put_chars(Io, Encoding, Chars) -> + o_request(Io, {put_chars,Encoding,Chars}, put_chars). -spec nl() -> 'ok'. diff --git a/lib/syntax_tools/src/erl_syntax.erl b/lib/syntax_tools/src/erl_syntax.erl index b816c0699c..029f1e88ac 100644 --- a/lib/syntax_tools/src/erl_syntax.erl +++ b/lib/syntax_tools/src/erl_syntax.erl @@ -5328,7 +5328,7 @@ revert_map_type_assoc(Node) -> Pos = get_pos(Node), Name = map_type_assoc_name(Node), Value = map_type_assoc_value(Node), - {type, Pos, map_type_assoc, [Name, Value]}. + {type, Pos, map_field_assoc, [Name, Value]}. %% ===================================================================== @@ -5386,7 +5386,7 @@ revert_map_type_exact(Node) -> Pos = get_pos(Node), Name = map_type_exact_name(Node), Value = map_type_exact_value(Node), - {type, Pos, map_type_exact, [Name, Value]}. + {type, Pos, map_field_exact, [Name, Value]}. %% ===================================================================== diff --git a/lib/syntax_tools/test/syntax_tools_SUITE.erl b/lib/syntax_tools/test/syntax_tools_SUITE.erl index ae2c67c03e..c8e6448d37 100644 --- a/lib/syntax_tools/test/syntax_tools_SUITE.erl +++ b/lib/syntax_tools/test/syntax_tools_SUITE.erl @@ -24,13 +24,14 @@ %% Test cases -export([app_test/1,appup_test/1,smoke_test/1,revert/1,revert_map/1, + revert_map_type/1, t_abstract_type/1,t_erl_parse_type/1,t_epp_dodger/1, t_comment_scan/1,t_igor/1,t_erl_tidy/1]). suite() -> [{ct_hooks,[ts_install_cth]}]. all() -> - [app_test,appup_test,smoke_test,revert,revert_map, + [app_test,appup_test,smoke_test,revert,revert_map,revert_map_type, t_abstract_type,t_erl_parse_type,t_epp_dodger, t_comment_scan,t_igor,t_erl_tidy]. @@ -121,7 +122,26 @@ revert_map(Config) when is_list(Config) -> {map_field_assoc,{atom,17,name},{var,18,'Value'}}}]), ?t:timetrap_cancel(Dog). - +%% Testing bug fix for reverting map_field_assoc in types +revert_map_type(Config) when is_list(Config) -> + Dog = ?t:timetrap(?t:minutes(1)), + Form1 = {attribute,4,record, + {state, + [{typed_record_field, + {record_field,5,{atom,5,x}}, + {type,5,map, + [{type,5,map_field_exact,[{atom,5,y},{atom,5,z}]}]}}]}}, + Mapped1 = erl_syntax_lib:map(fun(X) -> X end, Form1), + Form1 = erl_syntax:revert(Mapped1), + Form2 = {attribute,4,record, + {state, + [{typed_record_field, + {record_field,5,{atom,5,x}}, + {type,5,map, + [{type,5,map_field_assoc,[{atom,5,y},{atom,5,z}]}]}}]}}, + Mapped2 = erl_syntax_lib:map(fun(X) -> X end, Form2), + Form2 = erl_syntax:revert(Mapped2), + ?t:timetrap_cancel(Dog). %% api tests |