diff --git a/erts/emulator/beam/erl_bif_port.c b/erts/emulator/beam/erl_bif_port.c index cfa351a7550e..d08a44153d6a 100644 --- a/erts/emulator/beam/erl_bif_port.c +++ b/erts/emulator/beam/erl_bif_port.c @@ -51,6 +51,7 @@ #define HALLOC_EXTRA 200 static Port *open_port(Process* p, Eterm name, Eterm settings, int *err_typep, int *err_nump); +static int parse_packet_option(SysDriverOpts *opts, Eterm option); static int merge_global_environment(erts_osenv_t *env, Eterm key_value_pairs); static char **convert_args(Eterm); static void free_args(char **); @@ -782,6 +783,58 @@ Eterm erts_port_data_read(Port* prt) } +static int +parse_packet_option(SysDriverOpts *opts, Eterm option) +{ + Sint packet_bytes; + Eterm packet_endian = am_big; + + if (is_small(option)) { + packet_bytes = signed_val(option); + } else if (is_tuple_arity(option, 2)) { + Eterm *tp = tuple_val(option); + + if (is_not_small(tp[1])) { + return 0; + } + + packet_bytes = signed_val(tp[1]); + packet_endian = tp[2]; + + if (packet_bytes != 2 && packet_bytes != 4) { + return 0; + } + } else { + return 0; + } + + switch (packet_bytes) { + case 1: + case 2: + case 4: + break; + default: + return 0; + } + + if (packet_endian == am_big) { + opts->packet_endian = ERTS_SYS_DRIVER_PACKET_ENDIAN_BIG; + } else if (packet_endian == am_little) { + opts->packet_endian = ERTS_SYS_DRIVER_PACKET_ENDIAN_LITTLE; + } else if (packet_endian == am_native) { +#ifdef WORDS_BIGENDIAN + opts->packet_endian = ERTS_SYS_DRIVER_PACKET_ENDIAN_BIG; +#else + opts->packet_endian = ERTS_SYS_DRIVER_PACKET_ENDIAN_LITTLE; +#endif + } else { + return 0; + } + + opts->packet_bytes = packet_bytes; + return 1; +} + /* * Open a port. Most of the work is not done here but rather in * the file io.c. @@ -811,6 +864,7 @@ open_port(Process* p, Eterm name, Eterm settings, int *err_typep, int *err_nump) /* These are the defaults */ opts.packet_bytes = 0; + opts.packet_endian = ERTS_SYS_DRIVER_PACKET_ENDIAN_BIG; opts.use_stdio = 1; opts.redir_stderr = 0; opts.read_write = 0; @@ -847,18 +901,9 @@ open_port(Process* p, Eterm name, Eterm settings, int *err_typep, int *err_nump) arity = *tp++; option = *tp++; if (option == am_packet) { - if (is_not_small(*tp)) { + if (!parse_packet_option(&opts, *tp)) { goto bad_settings; } - opts.packet_bytes = signed_val(*tp); - switch (opts.packet_bytes) { - case 1: - case 2: - case 4: - break; - default: - goto bad_settings; - } } else if (option == am_line) { if (is_not_small(*tp)) { goto bad_settings; @@ -978,6 +1023,7 @@ open_port(Process* p, Eterm name, Eterm settings, int *err_typep, int *err_nump) } } else if (*nargs == am_stream) { opts.packet_bytes = 0; + opts.packet_endian = ERTS_SYS_DRIVER_PACKET_ENDIAN_BIG; } else if (*nargs == am_use_stdio) { opts.use_stdio = 1; } else if (*nargs == am_stderr_to_stdout) { @@ -1615,4 +1661,3 @@ BIF_RETTYPE decode_packet_3(BIF_ALIST_3) BIF_RET(res); } - diff --git a/erts/emulator/beam/erl_sys_driver.h b/erts/emulator/beam/erl_sys_driver.h index 6d84d77d0c95..6f16dab32ddb 100644 --- a/erts/emulator/beam/erl_sys_driver.h +++ b/erts/emulator/beam/erl_sys_driver.h @@ -39,6 +39,11 @@ typedef struct _SysDriverOpts SysDriverOpts; #include "erl_driver.h" +typedef enum { + ERTS_SYS_DRIVER_PACKET_ENDIAN_BIG, + ERTS_SYS_DRIVER_PACKET_ENDIAN_LITTLE +} ErtsSysDriverPacketEndian; + /* * This structure contains options to all built in drivers. * None of the drivers use all of the fields. @@ -48,6 +53,7 @@ struct _SysDriverOpts { Uint ifd; /* Input file descriptor (fd driver). */ Uint ofd; /* Outputfile descriptor (fd driver). */ int packet_bytes; /* Number of bytes in packet header. */ + ErtsSysDriverPacketEndian packet_endian; /* Byte order of packet header. */ int read_write; /* Read and write bits. */ int use_stdio; /* Use standard I/O: TRUE or FALSE. */ int redir_stderr; /* Redirect stderr to stdout: TRUE/FALSE. */ @@ -68,8 +74,83 @@ struct _SysDriverOpts { char msgq_watermarks_set; }; -#endif +ERTS_GLB_INLINE Uint32 +erts_sys_driver_get_packet_size(const char *header, + int packet_bytes, + ErtsSysDriverPacketEndian packet_endian); + +ERTS_GLB_INLINE int +erts_sys_driver_packet_size_fits(ErlDrvSizeT packet_size, int packet_bytes); + +ERTS_GLB_INLINE void +erts_sys_driver_put_packet_size(ErlDrvSizeT packet_size, + char *header, + int packet_bytes, + ErtsSysDriverPacketEndian packet_endian); + +#if ERTS_GLB_INLINE_INCL_FUNC_DEF + +ERTS_GLB_INLINE Uint32 +erts_sys_driver_get_packet_size(const char *header, + int packet_bytes, + ErtsSysDriverPacketEndian packet_endian) +{ + Uint32 packet_size = 0; + int i; + + for (i = 0; i < packet_bytes; i++) { + int index = packet_endian == ERTS_SYS_DRIVER_PACKET_ENDIAN_LITTLE + ? packet_bytes - i - 1 + : i; + packet_size = (packet_size << 8) | + ((const unsigned char *) header)[index]; + } + return packet_size; +} +ERTS_GLB_INLINE int +erts_sys_driver_packet_size_fits(ErlDrvSizeT packet_size, int packet_bytes) +{ + if (packet_bytes < 0 || + packet_size > ((ErlDrvSizeT) -1) - (ErlDrvSizeT) packet_bytes) { + return 0; + } + switch (packet_bytes) { + case 0: + return 1; + case 1: + return packet_size <= 0xff; + case 2: + return packet_size <= 0xffff; + case 4: + return (Uint64) packet_size <= ERTS_UINT32_MAX; + default: + return 0; + } +} + +ERTS_GLB_INLINE void +erts_sys_driver_put_packet_size(ErlDrvSizeT packet_size, + char *header, + int packet_bytes, + ErtsSysDriverPacketEndian packet_endian) +{ + unsigned char *bytes = (unsigned char *) header; + int i; + + for (i = 0; i < packet_bytes; i++) { + int index = packet_endian == ERTS_SYS_DRIVER_PACKET_ENDIAN_LITTLE + ? i + : packet_bytes - i - 1; + + bytes[index] = packet_size & 0xff; + packet_size >>= 8; + } +} + +#endif /* ERTS_GLB_INLINE_INCL_FUNC_DEF */ + +#endif diff --git a/erts/emulator/sys/unix/sys_drivers.c b/erts/emulator/sys/unix/sys_drivers.c index 8dedc8d82a9f..50519353f0b0 100644 --- a/erts/emulator/sys/unix/sys_drivers.c +++ b/erts/emulator/sys/unix/sys_drivers.c @@ -36,6 +36,7 @@ #include #include #include +#include #include #include #include @@ -110,6 +111,7 @@ typedef struct driver_data { ErtsSysFdData *ofd; ErtsSysFdData *ifd; int packet_bytes; + ErtsSysDriverPacketEndian packet_endian; int pid; int alive; int status; @@ -336,7 +338,6 @@ static ErtsSysDriverData * create_driver_data(ErlDrvPort port_num, int ifd, int ofd, - int packet_bytes, int read_write, int exit_status, int pid, @@ -365,7 +366,8 @@ create_driver_data(ErlDrvPort port_num, if (prt != ERTS_INVALID_ERL_DRV_PORT) prt->os_pid = pid; - driver_data->packet_bytes = packet_bytes; + driver_data->packet_bytes = opts->packet_bytes; + driver_data->packet_endian = opts->packet_endian; driver_data->port_num = port_num; driver_data->pid = pid; driver_data->alive = exit_status ? 1 : 0; @@ -691,8 +693,8 @@ static ErlDrvData spawn_start(ErlDrvPort port_num, char* name, erts_free(ERTS_ALC_T_TMP, (void *) cmd_line); - dd = create_driver_data(port_num, ifd[0], ofd[1], opts->packet_bytes, - DO_WRITE | DO_READ, opts->exit_status, + dd = create_driver_data(port_num, ifd[0], ofd[1], + DO_WRITE | DO_READ, opts->exit_status, 0, 0, opts); { @@ -974,7 +976,6 @@ static ErlDrvData fd_start(ErlDrvPort port_num, char* name, } } return (ErlDrvData)create_driver_data(port_num, opts->ifd, opts->ofd, - opts->packet_bytes, opts->read_write, 0, -1, !non_blocking, opts); } @@ -1060,6 +1061,7 @@ static void outputv(ErlDrvData e, ErlIOVec* ev) ErtsSysDriverData *dd = (ErtsSysDriverData*)e; ErlDrvPort ix = dd->port_num; int pb = dd->packet_bytes; + ErtsSysDriverPacketEndian packet_endian = dd->packet_endian; int ofd = dd->ofd ? dd->ofd->fd : -1; ssize_t n; char lb[4]; @@ -1067,15 +1069,13 @@ static void outputv(ErlDrvData e, ErlIOVec* ev) ErlDrvSizeT len = ev->size; ErlDrvSizeT qsz; - /* (len > ((unsigned long)-1 >> (4-pb)*8)) */ - /* if (pb >= 0 && (len & (((ErlDrvSizeT)1 << (pb*8))) - 1) != len) {*/ - if (((pb == 2) && (len > 0xffff)) || (pb == 1 && len > 0xff)) { + if (!erts_sys_driver_packet_size_fits(len, pb)) { driver_failure_posix(ix, EINVAL); return; /* -1; */ } /* Handles 0 <= pb <= 4 only */ - put_int32((Uint32) len, lb); - lbp = lb + (4-pb); + erts_sys_driver_put_packet_size(len, lb, pb, packet_endian); + lbp = lb; ev->iov[0].iov_base = lbp; ev->iov[0].iov_len = pb; @@ -1142,6 +1142,7 @@ static void output(ErlDrvData e, char* buf, ErlDrvSizeT len) ErtsSysDriverData *dd = (ErtsSysDriverData*)e; ErlDrvPort ix = dd->port_num; int pb = dd->packet_bytes; + ErtsSysDriverPacketEndian packet_endian = dd->packet_endian; int ofd = dd->ofd ? dd->ofd->fd : -1; ssize_t n; ErlDrvSizeT qsz; @@ -1149,15 +1150,13 @@ static void output(ErlDrvData e, char* buf, ErlDrvSizeT len) char* lbp; struct iovec iv[2]; - /* (len > ((unsigned long)-1 >> (4-pb)*8)) */ - if (((pb == 2) && (len > 0xffff)) - || (pb == 1 && len > 0xff) + if (!erts_sys_driver_packet_size_fits(len, pb) || dd->pid == 0 /* Attempt at output before port is ready */) { driver_failure_posix(ix, EINVAL); return; /* -1; */ } - put_int32(len, lb); - lbp = lb + (4-pb); + erts_sys_driver_put_packet_size(len, lb, pb, packet_endian); + lbp = lb; qsz = driver_sizeq(ix); if (qsz) { @@ -1405,11 +1404,13 @@ static void ready_input(ErlDrvData e, ErlDrvEvent ready_fd) } dd->ifd->psz = 0; - switch (packet_bytes) { - case 1: h = get_int8(dd->ifd->pbuf); break; - case 2: h = get_int16(dd->ifd->pbuf); break; - case 4: h = get_uint32(dd->ifd->pbuf); break; - default: ASSERT(0); return; /* -1; */ + h = erts_sys_driver_get_packet_size(dd->ifd->pbuf, + packet_bytes, + dd->packet_endian); + if (h > (Uint) INT_MAX) { + errno = EINVAL; + port_inp_failure(dd, -1); + break; } if (h <= (bytes_left)) { diff --git a/erts/emulator/sys/win32/sys.c b/erts/emulator/sys/win32/sys.c index 25c49b5c685f..7563370f791c 100644 --- a/erts/emulator/sys/win32/sys.c +++ b/erts/emulator/sys/win32/sys.c @@ -35,6 +35,7 @@ #include "global.h" #include "erl_threads.h" #include "erl_cpu_topology.h" +#include #include #if defined(__WIN32__) && !defined(WINDOWS_H_INCLUDES_WINSOCK2_H) @@ -507,6 +508,7 @@ struct driver_data { int packet_bytes; /* 0: continuous stream, 1, 2, or 4: the number * of bytes in the packet header. */ + ErtsSysDriverPacketEndian packet_endian; HANDLE port_pid; /* PID of the port process. */ AsyncIo in; /* Control block for overlapped reading. */ AsyncIo out; /* Control block for overlapped writing. */ @@ -620,12 +622,21 @@ unrefer_driver_data(DriverData *dp) * if the initialsation failed. */ +static void +set_packet_options(DriverData *dp, const SysDriverOpts *opts) +{ + dp->packet_bytes = opts->packet_bytes; + dp->packet_endian = opts->packet_endian; + dp->totalNeeded = opts->packet_bytes; +} + static DriverData* -new_driver_data(ErlDrvPort port_num, int packet_bytes, int wait_objs_required, int use_threads) +new_driver_data(ErlDrvPort port_num, const SysDriverOpts *opts, + int wait_objs_required, int use_threads) { DriverData* dp; - DEBUGF(("new_driver_data(%p, pb %d)\n", port_num, packet_bytes)); + DEBUGF(("new_driver_data(%p, pb %d)\n", port_num, opts->packet_bytes)); dp = driver_alloc(sizeof(DriverData)); if (!dp) @@ -638,7 +649,7 @@ new_driver_data(ErlDrvPort port_num, int packet_bytes, int wait_objs_required, i erts_atomic32_init_nob(&dp->refc, 1); dp->bytesInBuffer = 0; - dp->totalNeeded = packet_bytes; + set_packet_options(dp, opts); dp->inBufSize = PORT_BUFSIZ; dp->inbuf = DRV_BUF_ALLOC(dp->inBufSize); if (dp->inbuf == NULL) @@ -647,7 +658,6 @@ new_driver_data(ErlDrvPort port_num, int packet_bytes, int wait_objs_required, i dp->outBufSize = 0; dp->outbuf = NULL; dp->port_num = port_num; - dp->packet_bytes = packet_bytes; dp->port_pid = INVALID_HANDLE_VALUE; if (init_async_io(dp, &dp->in, use_threads) == -1) goto async_io_error1; @@ -879,22 +889,26 @@ set_driver_data(DriverData* dp, HANDLE ifd, HANDLE ofd, int read_write, int repo } static ErlDrvData -reuse_driver_data(DriverData *dp, HANDLE ifd, HANDLE ofd, int read_write, ErlDrvPort port_num) +reuse_driver_data(DriverData *dp, HANDLE ifd, HANDLE ofd, + ErlDrvPort port_num, SysDriverOpts *opts) { int result; + /* fd_stop() leaves input active. Keep bytesInBuffer in sync with the + * destination of that outstanding read when changing packet settings. */ + set_packet_options(dp, opts); dp->port_num = port_num; dp->in.fd = ifd; dp->out.fd = ofd; dp->report_exit = 0; - if (read_write & DO_READ) { + if (opts->read_write & DO_READ) { result = driver_select(dp->port_num, (ErlDrvEvent)dp->in.ov.hEvent, ERL_DRV_READ|ERL_DRV_USE, 1); ASSERT(result != -1); } - if (read_write & DO_WRITE) { + if (opts->read_write & DO_WRITE) { result = driver_select(dp->port_num, (ErlDrvEvent)dp->out.ov.hEvent, ERL_DRV_WRITE|ERL_DRV_USE, 1); ASSERT(result != -1); @@ -1191,7 +1205,7 @@ spawn_start(ErlDrvPort port_num, char* utf8_name, SysDriverOpts* opts) if (opts->read_write & DO_WRITE) neededSelects++; - if ((dp = new_driver_data(port_num, opts->packet_bytes, neededSelects, + if ((dp = new_driver_data(port_num, opts, neededSelects, !use_named_pipes)) == NULL) return ERL_DRV_ERROR_GENERAL; @@ -2130,12 +2144,14 @@ fd_start(ErlDrvPort port_num, char* name, SysDriverOpts* opts) opts->ofd = (Uint) translate_fd(out); if ( in == 0 && out == 1 && save_01_port != NULL) { dp = save_01_port; - return reuse_driver_data(dp, (HANDLE) opts->ifd, (HANDLE) opts->ofd, opts->read_write, port_num); + return reuse_driver_data(dp, (HANDLE) opts->ifd, (HANDLE) opts->ofd, + port_num, opts); } else if (in == 2 && out == 2 && save_22_port != NULL) { dp = save_22_port; - return reuse_driver_data(dp, (HANDLE) opts->ifd, (HANDLE) opts->ofd, opts->read_write, port_num); + return reuse_driver_data(dp, (HANDLE) opts->ifd, (HANDLE) opts->ofd, + port_num, opts); } else { - if ((dp = new_driver_data(port_num, opts->packet_bytes, 2, TRUE)) == NULL) + if ((dp = new_driver_data(port_num, opts, 2, TRUE)) == NULL) return ERL_DRV_ERROR_GENERAL; /** @@ -2402,14 +2418,15 @@ output(ErlDrvData drv_data, char* buf, ErlDrvSizeT len) pb = dp->packet_bytes; - if ((pb+len) == 0) + if (pb == 0 && len == 0) return ; /* 0; */ /* * Check that the message can be sent with given header length. */ - if ((pb == 2 && len > 65535) || (pb == 1 && len > 255)) { + if (!erts_sys_driver_packet_size_fits(len, pb) + || len > (ErlDrvSizeT) (INT_MAX - pb)) { driver_failure_posix(dp->port_num, EINVAL); return ; /* -1; */ } @@ -2430,16 +2447,8 @@ output(ErlDrvData drv_data, char* buf, ErlDrvSizeT len) */ current = bin->orig_bytes; - - switch (pb) { - case 4: - *current++ = (len >> 24) & 255; - *current++ = (len >> 16) & 255; - case 2: - *current++ = (len >> 8) & 255; - case 1: - *current++ = len & 255; - } + erts_sys_driver_put_packet_size(len, current, pb, dp->packet_endian); + current += pb; /* * Start the write. @@ -2559,12 +2568,14 @@ ready_input(ErlDrvData drv_data, ErlDrvEvent ready_event) if (error == NO_ERROR) { if (pb == 0) { /* Continuous stream. */ + dp->bytesInBuffer += bytesRead; #ifdef DEBUG - DEBUGF(("ready_input: %d: ", bytesRead)); - erl_bin_write(dp->inbuf, 16, bytesRead); + DEBUGF(("ready_input: %d: ", dp->bytesInBuffer)); + erl_bin_write(dp->inbuf, 16, dp->bytesInBuffer); DEBUGF(("\n")); #endif - driver_output(dp->port_num, dp->inbuf, bytesRead); + driver_output(dp->port_num, dp->inbuf, dp->bytesInBuffer); + dp->bytesInBuffer = 0; } else { /* Packet mode */ dp->bytesInBuffer += bytesRead; @@ -2588,20 +2599,17 @@ ready_input(ErlDrvData drv_data, ErlDrvEvent ready_event) * the packet size. */ - int packet_size = 0; - unsigned char *header = (unsigned char *) dp->inbuf; - - switch (pb) { - case 4: - packet_size = (packet_size << 8) | *header++; - packet_size = (packet_size << 8) | *header++; - case 2: - packet_size = (packet_size << 8) | *header++; - case 1: - packet_size = (packet_size << 8) | *header++; + Uint32 packet_size = + erts_sys_driver_get_packet_size((char *) dp->inbuf, + pb, + dp->packet_endian); + + if (packet_size > (Uint32) (INT_MAX - pb)) { + driver_failure_posix(dp->port_num, EINVAL); + return; } - dp->totalNeeded += packet_size; + dp->totalNeeded += (int) packet_size; /* * Make sure that the receive buffer is big enough. diff --git a/erts/emulator/test/port_SUITE.erl b/erts/emulator/test/port_SUITE.erl index f19e23d433e6..2b444e765125 100644 --- a/erts/emulator/test/port_SUITE.erl +++ b/erts/emulator/test/port_SUITE.erl @@ -39,13 +39,16 @@ %% [Spawn of external program is tested.] %% %% {fd, In, Out} -%% Open file descriptors In and Out. [Not tested] +%% Open file descriptors In and Out. [Packet framing tested.] %% %% PortSettings can be %% %% {packet, N} %% N is 1, 2 or 4. %% +%% {packet, {N, Endian}} +%% N is 2 or 4. Endian is big, little, or native. +%% %% stream (default) %% Without packet length. %% @@ -92,6 +95,7 @@ eof/1, exit_status/1, exit_status_multi_scheduling_block/1, + fd_packet_endian/1, huge_env/1, pipe_limit_env/1, input_only/1, @@ -119,6 +123,7 @@ otp_5119/1, otp_6224/1, output_only/1, + packet_endian/1, parallelism_option/1, parallell/1, port_program_with_path/1, @@ -159,7 +164,7 @@ %% Internal exports. -export([tps/3]). -export([otp_3906_forker/5, otp_3906_start_forker_starter/4]). --export([env_slave_main/1]). +-export([env_slave_main/1, fd_packet_endian_child/1]). -include_lib("common_test/include/ct.hrl"). -include_lib("kernel/include/file.hrl"). @@ -170,7 +175,8 @@ suite() -> {timetrap, {minutes, 1}}]. all() -> - [otp_6224, {group, stream}, basic_ping, slow_writes, + [otp_6224, {group, stream}, basic_ping, packet_endian, + fd_packet_endian, slow_writes, bad_packet, bad_port_messages, {group, options}, {group, multiple_packets}, parallell, dying_port, dropped_commands, port_program_with_path, name1, env, huge_env, bad_env, cd, @@ -315,6 +321,194 @@ basic_ping(Config) when is_list(Config) -> ping(Config, sizes(4), 4, "", []), ok. +%% Test packet headers with an explicit byte order in both directions. +packet_endian(Config) when is_list(Config) -> + ct:timetrap({minutes, 3}), + lists:foreach( + fun(Endian) -> + ping(Config, sizes(2), {2, Endian}, "", []), + ping(Config, sizes(4), {4, Endian}, "", []) + end, + [big, little, native]), + + %% Cover coalesced packets, zero-length packets, and fragmented headers. + expect_input(Config, [0, 10, 1600, 65535], {2, little}, "", []), + expect_input(Config, [0, 10, 70000], {4, little}, "", []), + ping(Config, [8], {4, little}, "-s1", []), + + packet_option_resets_endian(Config), + + %% The endian-bearing form is only meaningful for multi-byte headers. + bad_argument(Config, [{packet, {1, big}}]), + bad_argument(Config, [{packet, {3, little}}]), + bad_argument(Config, [{packet, {2, middle}}]), + bad_argument(Config, [{packet, {2, little, extra}}]), + bad_argument(Config, [{packet, {2, little}}, {line, 5}]), + ok. + +packet_option_resets_endian(Config) -> + PortTest = port_test(Config), + Port = open_port({spawn, PortTest ++ " -h2"}, + [{packet, {2, little}}, {packet, 2}]), + [$p | Packet] = random_packet(256, "ping"), + try + port_expect(Port, [{[$p | Packet], [[$P | Packet]]}], [{packet, 2}]) + after + catch port_close(Port) + end. + +%% Exercise packet framing through a real {fd,0,1} port in a child VM. The +%% outer port stays in stream mode so that encoder and decoder mistakes cannot +%% cancel each other out. +fd_packet_endian(Config) when is_list(Config) -> + ct:timetrap({minutes, 2}), + [Prog | ProgArgs] = string:split(ct:get_progname(), " ", all), + case os:find_executable(Prog) of + false -> + {skip, "Could not find the erl executable"}; + Erl -> + BeamDir = filename:dirname(code:which(?MODULE)), + lists:foreach( + fun({HeaderSize, Endian}) -> + Args = ProgArgs ++ + ["+S", "1:1", "-noshell", "-noinput", + "-pa", BeamDir, + "-run", ?MODULE_STRING, + "fd_packet_endian_child", + integer_to_list(HeaderSize), + atom_to_list(Endian)], + fd_packet_endian(Erl, Args, HeaderSize, Endian) + end, + [{HeaderSize, Endian} || HeaderSize <- [2, 4], + Endian <- [big, little, native]]), + ok + end. + +fd_packet_endian(Erl, Args, HeaderSize, Endian) -> + Port = open_port({spawn_executable, Erl}, + [binary, stream, eof, exit_status, hide, {args, Args}]), + {InitialHeaderSize, InitialEndian} = + fd_initial_packet_setting(HeaderSize, Endian), + InitialFrame = + fd_packet_frame(InitialHeaderSize, InitialEndian, <<"first">>), + ReopenFrame = + fd_packet_frame(InitialHeaderSize, InitialEndian, <<"reopen">>), + ReadyFrame = fd_packet_frame(HeaderSize, Endian, <<"ready">>), + Payload = binary:copy(<<$x>>, 256), + Frame = fd_packet_frame(HeaderSize, Endian, Payload), + try + true = port_command(Port, InitialFrame), + InitialFrame = + receive_fd_packet_bytes(Port, byte_size(InitialFrame), <<>>), + true = port_command(Port, ReopenFrame), + ReadyFrame = + receive_fd_packet_bytes(Port, byte_size(ReadyFrame), <<>>), + true = port_command(Port, Frame), + Frame = receive_fd_packet_bytes(Port, byte_size(Frame), <<>>), + Stop = fd_packet_frame(HeaderSize, Endian, <<"stop">>), + true = port_command(Port, Stop), + wait_fd_packet_child(Port, false, false) + after + catch port_close(Port) + end. + +fd_packet_endian_child([HeaderSize0, Endian0]) -> + HeaderSize = list_to_integer(HeaderSize0), + Endian = list_to_existing_atom(Endian0), + {InitialHeaderSize, InitialEndian} = + fd_initial_packet_setting(HeaderSize, Endian), + InitialPort = open_port({fd, 0, 1}, + [binary, + {packet, {InitialHeaderSize, InitialEndian}}]), + receive + {InitialPort, {data, <<"first">> = InitialData}} -> + true = port_command(InitialPort, InitialData) + after 10000 -> + halt(2) + end, + receive + {InitialPort, {data, <<"reopen">>}} -> + ok + after 10000 -> + halt(3) + end, + true = port_close(InitialPort), + Port = open_port({fd, 0, 1}, + [binary, {packet, {HeaderSize, Endian}}]), + true = port_command(Port, <<"ready">>), + receive + {Port, {data, EchoData}} -> + true = port_command(Port, EchoData) + after 10000 -> + halt(4) + end, + receive + {Port, {data, <<"stop">>}} -> + halt(0) + after 10000 -> + halt(5) + end. + +fd_initial_packet_setting(2, Endian) -> + {4, fd_opposite_endian(Endian)}; +fd_initial_packet_setting(4, Endian) -> + {2, fd_opposite_endian(Endian)}. + +fd_opposite_endian(big) -> + little; +fd_opposite_endian(little) -> + big; +fd_opposite_endian(native) -> + case erlang:system_info(endian) of + big -> little; + little -> big + end. + +fd_packet_frame(HeaderSize, Endian, Data) -> + Header = fd_packet_header(HeaderSize, Endian, byte_size(Data)), + <
>. + +fd_packet_header(2, big, Size) -> <>; +fd_packet_header(2, little, Size) -> <>; +fd_packet_header(2, native, Size) -> <>; +fd_packet_header(4, big, Size) -> <>; +fd_packet_header(4, little, Size) -> <>; +fd_packet_header(4, native, Size) -> <>. + +receive_fd_packet_bytes(_Port, Size, Data) when byte_size(Data) =:= Size -> + Data; +receive_fd_packet_bytes(Port, Size, Acc) -> + receive + {Port, {data, Data}} -> + Combined = <>, + case byte_size(Combined) =< Size of + true -> receive_fd_packet_bytes(Port, Size, Combined); + false -> ct:fail({unexpected_fd_packet_data, Combined}) + end; + {Port, eof} -> + ct:fail(fd_packet_child_eof); + {Port, {exit_status, Status}} -> + ct:fail({fd_packet_child_exit, Status}) + after 10000 -> + ct:fail({fd_packet_child_timeout, byte_size(Acc), Size}) + end. + +wait_fd_packet_child(_Port, true, true) -> + ok; +wait_fd_packet_child(Port, Eof, Exited) -> + receive + {Port, eof} -> + wait_fd_packet_child(Port, true, Exited); + {Port, {exit_status, 0}} -> + wait_fd_packet_child(Port, Eof, true); + {Port, {exit_status, Status}} -> + ct:fail({fd_packet_child_exit, Status}); + {Port, {data, Data}} -> + ct:fail({unexpected_fd_packet_data, Data}) + after 10000 -> + ct:fail({fd_packet_child_timeout, Eof, Exited}) + end. + %% Let the port program insert delays between characters sent back to %% Erlang, to test that the Erlang emulator can handle a packet coming in %% small chunks rather than all at once. @@ -334,10 +528,11 @@ bad_packet(Config) when is_list(Config) -> bad_packet(PortTest, 1, 257), bad_packet(PortTest, 2, 65536), bad_packet(PortTest, 2, 65537), + bad_packet(PortTest, {2, little}, 65536), ok. -bad_packet(PortTest, HeaderSize, PacketSize) -> - P = open_port({spawn, PortTest}, [{packet, HeaderSize}]), +bad_packet(PortTest, PacketSetting, PacketSize) -> + P = open_port({spawn, PortTest}, [{packet, PacketSetting}]), P ! {self(), {command, make_zero_packet(PacketSize)}}, receive {'EXIT', P, einval} -> ok; @@ -2167,7 +2362,7 @@ ping(Config, Sizes, HSize, CmdLine, Options) -> %% expect_input(Sizes, HSize, CmdLine, Options) %% %% Sizes = Size of packets to generated. -%% HSize = Header size: 1, 2, or 4 +%% HSize = Header size: 1, 2, 4, or {2 | 4, Endian} %% CmdLine = Additional command line options. %% Options = Additional port options. @@ -2195,10 +2390,10 @@ build_cmd_line(FixedCmdLine, [], Result) -> %% port_expect(Actions, HSize, CmdLine, Options) %% %% Actions = [{Send, ExpectList}|Rest] -%% HSize = 0 (stream), or 1, 2, 4 (header size aka "packet bytes") +%% HSize = 0 (stream), 1, 2, 4, or {2 | 4, Endian} %% CmdLine = Command line for port_test. Don't include -h. -%% Options = Options for open_port/2. Don't include {packet, Number} or -%% or stream. +%% Options = Options for open_port/2. Don't include a packet setting or +%% stream. %% %% Send = false | list() %% ExpectList = List of lists or binaries. @@ -2209,18 +2404,32 @@ port_expect(Config, Actions, HSize, CmdLine, Options0) -> % io:format("port_expect(~p, ~p, ~p, ~p)", % [Actions, HSize, CmdLine, Options0]), PortTest = port_test(Config), - Cmd = lists:concat([PortTest, " -h", HSize, " ", CmdLine]), - PortType = - case HSize of - 0 -> stream; - _ -> {packet, HSize} - end, + {HeaderSize, PortType, EndianArg} = port_packet_setting(HSize), + Cmd = lists:concat([PortTest, " -h", HeaderSize, EndianArg, + " ", CmdLine]), Options = [PortType|Options0], ct:log("open_port({spawn, ~p}, ~p)", [Cmd, Options]), Port = open_port({spawn, Cmd}, Options), port_expect(Port, Actions, Options), Port. +port_packet_setting(0) -> + {0, stream, ""}; +port_packet_setting(HeaderSize) when is_integer(HeaderSize) -> + {HeaderSize, {packet, HeaderSize}, ""}; +port_packet_setting({HeaderSize, Endian} = PacketSetting) -> + EndianArg = + case Endian of + big -> ""; + little -> " -L"; + native -> + case erlang:system_info(endian) of + big -> ""; + little -> " -L" + end + end, + {HeaderSize, {packet, PacketSetting}, EndianArg}. + port_expect(Port, [{Send, Expects}|Rest], Options) when is_list(Expects) -> port_send(Port, Send), IsBinaryPort = lists:member(binary, Options), diff --git a/erts/emulator/test/port_SUITE_data/port_test.c b/erts/emulator/test/port_SUITE_data/port_test.c index 2e00be7f4b79..26dbd89a4a94 100644 --- a/erts/emulator/test/port_SUITE_data/port_test.c +++ b/erts/emulator/test/port_SUITE_data/port_test.c @@ -64,6 +64,7 @@ typedef struct { char* progname; /* Name of this program (from argv[0]). */ int header_size; /* Number of bytes in each packet header: * 1, 2, or 4, or 0 for a continuous byte stream. */ + int little_endian; /* Use little-endian packet headers. */ int fd_from_erl; /* File descriptor from Erlang. */ int fd_to_erl; /* File descriptor to Erlang. */ unsigned char* io_buf; /* Buffer for file i/o. */ @@ -94,6 +95,8 @@ typedef struct { PORT_TEST_DATA* port_data; static int packet_loop(void); +static int decode_packet_length(const unsigned char *header); +static char *encode_packet_length(char *body, size_t size); static void reply(char *buf, int size); static void write_reply(char *buf, int size); static void ensure_buf_big_enough(int size); @@ -136,6 +139,7 @@ int main(int argc, char *argv[]) exit(1); } port_data->header_size = 0; + port_data->little_endian = 0; port_data->io_buf_size = 0; port_data->delay_mode = 0; port_data->fd_count = 0; @@ -189,6 +193,9 @@ int main(int argc, char *argv[]) case 'l': port_data->limited_bytecount = atoi(argv[1]+2); break; + case 'L': /* Little-endian packet headers. */ + port_data->little_endian = 1; + break; case 'n': /* No packet loop. */ port_data->no_packet_loop = 1; break; @@ -257,7 +264,6 @@ packet_loop(void) for (;;) { int packet_length; /* Length of current packet. */ - int i; int bytes_read; /* Number of bytes read. */ /* @@ -280,9 +286,7 @@ packet_loop(void) * Get the length of this packet. */ - packet_length = 0; - for (i = 0; i < port_data->header_size; i++) - packet_length = (packet_length << 8) | port_data->io_buf[i]; + packet_length = decode_packet_length(port_data->io_buf); } @@ -360,9 +364,42 @@ packet_loop(void) } } -/* - * Sends a packet back to Erlang. - */ +/* Packet header encoding and decoding. */ + +static int +decode_packet_length(const unsigned char *header) +{ + int packet_length = 0; + int i; + + if (port_data->little_endian) { + for (i = port_data->header_size - 1; i >= 0; i--) + packet_length = (packet_length << 8) | header[i]; + } else { + for (i = 0; i < port_data->header_size; i++) + packet_length = (packet_length << 8) | header[i]; + } + + return packet_length; +} + +static char * +encode_packet_length(char *body, size_t size) +{ + char *header = body - port_data->header_size; + int i; + + for (i = 0; i < port_data->header_size; i++) { + int index = port_data->little_endian + ? i + : port_data->header_size - i - 1; + + header[index] = (char) size; + size >>= 8; + } + + return header; +} static void reply(buf, size) @@ -372,19 +409,7 @@ reply(buf, size) */ int size; /* Size of buffer to send. */ { - int n; /* Temporary to hold size. */ - int i; /* Loop counter. */ - - /* - * Fill the header starting with the least significant byte - * (this will work even if there is no header). - */ - - n = size; - for (i = 0; i < port_data->header_size; i++) { - *--buf = (char) n; /* Store least significant byte. */ - n = n >> 8; - } + buf = encode_packet_length(buf, size); size += port_data->header_size; write_reply(buf, size); @@ -589,15 +614,9 @@ char* spec; /* Specification for reply. */ s = buf; for (cur = 0; cur < last; cur++) { int i; - size_t n; - n = items[cur].size; - s += port_data->header_size; - for (i = 0; i < port_data->header_size; i++) { - *--s = (char) n; /* Store least significant byte. */ - n = n >> 8; - } s += port_data->header_size; + (void) encode_packet_length(s, items[cur].size); c = items[cur].start; for (i = 0; i < items[cur].size; i++) { @@ -610,4 +629,3 @@ char* spec; /* Specification for reply. */ } write_reply(buf, s-buf); } - diff --git a/erts/preloaded/src/erlang.erl b/erts/preloaded/src/erlang.erl index 53424e1f9afb..7ac10136de6b 100644 --- a/erts/preloaded/src/erlang.erl +++ b/erts/preloaded/src/erlang.erl @@ -7754,6 +7754,12 @@ follows: with the most significant byte first. The valid values for `N` are 1, 2, and 4. +- **`{packet, {N, Endian}}`** - Messages are preceded by their length, sent in + `N` bytes using the specified byte order. The valid values for `N` are 2 and + 4. `Endian` can be `big`, `little`, or `native`, where `native` uses the byte + order of the platform. This setting applies to packet headers in both + directions. `{packet, {N, big}}` is equivalent to `{packet, N}`. + - **`stream`** - Output messages are sent without packet lengths. A user-defined protocol must be used between the Erlang process and the external object. @@ -7768,7 +7774,8 @@ follows: following a newline sequence, the last line is also delivered with `Flag` set to `noeol`. Otherwise lines are delivered with `Flag` set to `eol`. - The `{packet, N}` and `{line, L}` settings are mutually exclusive. + The `{packet, N}` and `{packet, {N, Endian}}` settings are mutually exclusive + with `{line, L}`. - **`{cd, Dir}`** - Only valid for `{spawn, Command}` and `{spawn_executable, FileName}`. The external program starts using `Dir` as its @@ -7967,6 +7974,7 @@ by passing command-line flag [`+Q`](erl_cmd.md#max_ports) to [erl](erl_cmd.md). {fd, In :: non_neg_integer(), Out :: non_neg_integer()}, PortSettings :: [Opt], Opt :: {packet, N :: 1 | 2 | 4} + | {packet, {N :: 2 | 4, Endian :: big | little | native}} | stream | {line, L :: non_neg_integer()} | {cd, Dir :: string() | binary()} diff --git a/system/doc/reference_manual/ports.md b/system/doc/reference_manual/ports.md index 7e31a3ba9f8d..af1a5502478e 100644 --- a/system/doc/reference_manual/ports.md +++ b/system/doc/reference_manual/ports.md @@ -76,9 +76,11 @@ is found, that driver is started. `PortSettings` is a list of settings (options) for the port. The list typically contains at least a tuple `{packet,N}`, which specifies that data sent between -the port and the external program are preceded by an N-byte length indicator. -Valid values for N are 1, 2, or 4. If binaries are to be used instead of lists -of bytes, the option `binary` must be included. +the port and the external program are preceded by an N-byte, big-endian length +indicator. Valid values for N are 1, 2, or 4. For 2- and 4-byte headers, +`{packet,{N,Endian}}` selects `big`, `little`, or the platform's `native` byte +order. If binaries are to be used instead of lists of bytes, the option +`binary` must be included. The port owner `Pid` can communicate with the port `Port` by sending and receiving messages. (In fact, any process can send messages to the port, but