diff --git a/embed/rayforce_q.c b/embed/rayforce_q.c index b96e1ee..9a4be92 100644 --- a/embed/rayforce_q.c +++ b/embed/rayforce_q.c @@ -31,9 +31,22 @@ #include "lang/env.h" /* ray_env_bind, ray_env_bind_flat */ #include "lang/eval.h" /* ray_fn_*, RAY_FN_NONE */ -#include /* atoi */ +#include +#include #include +static int q_parse_port(const char *s, int *out) { + if (s == NULL || *s == '\0') + return 0; + errno = 0; + char *end = NULL; + long v = strtol(s, &end, 10); + if (errno != 0 || end == s || *end != '\0' || v < 1 || v > 65535) + return 0; + *out = (int)v; + return 1; +} + static int64_t q_atom_i64(ray_t *a, int *ok) { *ok = 1; if (a == NULL) { @@ -81,6 +94,8 @@ static ray_t *qb_connect(ray_t **args, int64_t n) { int64_t port = q_atom_i64(args[1], &ok); if (!ok) return ray_error("type", ".q.connect: port must be an integer"); + if (port < 1 || port > 65535) + return ray_error("range", ".q.connect: port must be in 1..65535"); char host[256]; size_t hn = ray_str_len(args[0]); @@ -172,8 +187,14 @@ void q_env_register(void) { int64_t q_serve_from_args(ray_poll_t *poll, int argc, char **argv) { for (int i = 1; i < argc; i++) { if ((strcmp(argv[i], "-q") == 0 || strcmp(argv[i], "--q-serve") == 0) && - i + 1 < argc) - return q_serve(poll, atoi(argv[++i])); + i + 1 < argc) { + int port = 0; + if (!q_parse_port(argv[++i], &port)) { + fprintf(stderr, "q: invalid port %s (expected 1..65535)\n", argv[i]); + return -1; + } + return q_serve(poll, port); + } } return -1; } diff --git a/q.c b/q.c index 3b9cc1f..6a122f8 100644 --- a/q.c +++ b/q.c @@ -696,7 +696,6 @@ static ray_t *q_des_vec_i(uint8_t **buf, int64_t *len, int8_t ray_type, return vec; } - static inline int64_t q_kz_days_to_nanos(double days) { if (isnan(days)) return NULL_I64; @@ -980,6 +979,8 @@ static ray_t *q_des_obj(uint8_t **buf, int64_t *len) { i++; if (i == *len) return ray_error("q: malformed error frame", NULL); + *buf += i + 1; + *len -= i + 1; /* Put the q error text in both the code (short, shown by ray_fmt) and the * message (full), so bindings reading the message field get the whole * string even though the displayed code is length-capped. */ @@ -1067,6 +1068,9 @@ static int q_decompress(const uint8_t *src, int64_t src_len, uint8_t **out_buf, /* Public API */ int q_connect(const char *host, int port, const char *user, const char *password, int timeout_ms) { + if (host == NULL || host[0] == '\0' || port < 1 || port > 65535) + return Q_ERR_SOCKET; + int timed_out = 0; int fd = q_open_socket(host, port, timeout_ms, &timed_out); if (fd < 0) @@ -1228,6 +1232,11 @@ ray_t *q_decode(uint8_t *resp, int64_t resp_len, int compressed, char *err, free(decompressed); if (result == NULL) q_set_err(err, errlen, "q: deserialization returned null"); + else if (remaining != 0) { + ray_release(result); + q_set_err(err, errlen, "q: trailing bytes after object"); + return NULL; + } return result; } diff --git a/q_server.c b/q_server.c index 36bcc94..087d37d 100644 --- a/q_server.c +++ b/q_server.c @@ -242,6 +242,10 @@ static void q_on_close(ray_poll_t *poll, ray_selector_t *sel) { int64_t q_serve(ray_poll_t *poll, int port) { if (poll == NULL) return -1; + if (port < 1 || port > 65535) { + fprintf(stderr, "q: invalid port %d (expected 1..65535)\n", port); + return -1; + } ray_sock_t fd = ray_sock_listen((uint16_t)port); if (fd == RAY_INVALID_SOCK) { fprintf(stderr, "q: cannot listen on port %d (in use?)\n", port); diff --git a/test/driver.c b/test/driver.c index 8063242..8a68ec0 100644 --- a/test/driver.c +++ b/test/driver.c @@ -25,6 +25,7 @@ #include "core/poll.h" /* ray_poll_create / run / destroy */ #include "core/runtime.h" /* ray_runtime_set_poll */ +#include "q.h" /* q_decode / q_connect */ #include "q_server.h" /* q_serve */ #include @@ -275,7 +276,68 @@ static void inject_server(const char *host, const char *port, eval_setup("(set qpass \"%s\")", pass); } +static void release_any(ray_t *r) { + if (r == NULL) + return; + if (RAY_IS_ERR(r)) + ray_error_free(r); + else + ray_release(r); +} + +static int run_codec_selftest(void) { + int failures = 0; + ray_runtime_t *rt = ray_runtime_create(0, NULL); + if (rt == NULL) { + fprintf(stderr, "codec selftest: failed to create rayforce runtime\n"); + return 1; + } + + char err[128] = {0}; + uint8_t int_with_tail[] = {250, 42, 0, 0, 0, 0xff}; + ray_t *r = q_decode(int_with_tail, (int64_t)sizeof int_with_tail, 0, err, + sizeof err); + if (r != NULL || strstr(err, "trailing bytes") == NULL) { + fprintf(stderr, "codec selftest: trailing body bytes were not rejected\n"); + failures++; + } + release_any(r); + + err[0] = '\0'; + uint8_t qerr[] = {128, 'b', 'a', 'd', 0}; + r = q_decode(qerr, (int64_t)sizeof qerr, 0, err, sizeof err); + if (r == NULL || !RAY_IS_ERR(r)) { + fprintf(stderr, "codec selftest: Q error frame did not decode as error\n"); + failures++; + } + release_any(r); + + if (q_connect("127.0.0.1", 70000, "", "", 1) != Q_ERR_SOCKET) { + fprintf(stderr, "codec selftest: client accepted out-of-range port\n"); + failures++; + } + + ray_poll_t *poll = ray_poll_create(); + if (poll == NULL) { + fprintf(stderr, "codec selftest: failed to create poll\n"); + failures++; + } else { + if (q_serve(poll, -1) >= 0 || q_serve(poll, 70000) >= 0) { + fprintf(stderr, "codec selftest: server accepted out-of-range port\n"); + failures++; + } + ray_poll_destroy(poll); + } + + ray_runtime_destroy(rt); + printf("codec selftest: %s\n", failures ? "FAIL" : "ok"); + return failures ? 1 : 0; +} + int main(int argc, char **argv) { + if (argc >= 2 && strcmp(argv[1], "--codec-selftest") == 0) + return run_codec_selftest(); + /* Server role: `driver --serve PORT`. */ if (argc >= 3 && strcmp(argv[1], "--serve") == 0) return run_server(atoi(argv[2])); diff --git a/test/run.sh b/test/run.sh index 2ad15ec..80dd17e 100755 --- a/test/run.sh +++ b/test/run.sh @@ -85,6 +85,9 @@ cleanup() { } trap cleanup EXIT +echo "running codec selftest..." +"$DRIVER" --codec-selftest + # ---- Leg 1: Rayforce server SERVERPORT="${SERVERPORT:-$(free_port)}" "$DRIVER" --serve "$SERVERPORT" &