// SPDX-License-Identifier: ISC /* * Copyright (C) 2025 Felix Fietkau */ #include #include #include #include #include #include #include #include #include "uline.h" static uc_value_t *registry; static uc_resource_type_t *state_type, *argp_type; enum { STATE_RES, STATE_CB, STATE_INPUT, STATE_OUTPUT, STATE_POLL_CB, }; struct uc_uline_state { struct uloop_fd fd; struct uline_state s; int registry_index; uc_vm_t *vm; uc_value_t *state, *cb, *res, *poll_cb; uc_value_t *line; uint32_t input_mask[256 / 32]; }; struct uc_arg_parser { char line_sep; }; static unsigned int registry_set(uc_vm_t *vm, uc_value_t *val) { uc_value_t *registry; size_t i, len; registry = uc_vm_registry_get(vm, "uline.registry"); len = ucv_array_length(registry); for (i = 0; i < len; i++) if (ucv_array_get(registry, i) == NULL) break; ucv_array_set(registry, i, ucv_get(val)); return i; } static uc_value_t * uc_uline_poll(uc_vm_t *vm, size_t nargs) { struct uc_uline_state *us = uc_fn_thisval("uline.state"); uc_value_t *val; if (!us) return NULL; uline_poll(&us->s); val = us->line; us->line = NULL; return val; } static uc_value_t * uc_uline_poll_key(uc_vm_t *vm, size_t nargs) { struct uc_uline_state *us = uc_fn_thisval("uline.state"); uc_value_t *timeout_arg = uc_fn_arg(0); struct pollfd pfd = {}; int timeout, len; char c; if (!us) return NULL; if (ucv_type(timeout_arg) == UC_INTEGER) timeout = ucv_int64_get(timeout_arg); else timeout = -1; pfd.fd = us->s.input; pfd.events = POLLIN; poll(&pfd, 1, timeout); if (!(pfd.revents & POLLIN)) return NULL; do { len = read(pfd.fd, &c, 1); } while (len < 0 && errno == EINTR); if (len != 1) return NULL; return ucv_string_new_length(&c, 1); } static uc_value_t * uc_uline_poll_stop(uc_vm_t *vm, size_t nargs) { struct uc_uline_state *us = uc_fn_thisval("uline.state"); if (!us) return NULL; us->s.stop = true; return NULL; } static uc_value_t * uc_uline_get_window(uc_vm_t *vm, size_t nargs) { struct uc_uline_state *us = uc_fn_thisval("uline.state"); uc_value_t *val; if (!us) return NULL; val = ucv_object_new(vm); ucv_object_add(val, "x", ucv_int64_new(us->s.cols)); ucv_object_add(val, "y", ucv_int64_new(us->s.rows)); return val; } static uc_value_t * uc_uline_get_line(uc_vm_t *vm, size_t nargs) { struct uc_uline_state *us = uc_fn_thisval("uline.state"); uc_value_t *line2 = uc_fn_arg(0); uc_value_t *state; const char *line; size_t len; if (!us) return NULL; state = ucv_object_new(vm); if (ucv_is_truish(line2)) uline_get_line2(&us->s, &line, &len); else uline_get_line(&us->s, &line, &len); ucv_object_add(state, "line", ucv_string_new_length(line, len)); ucv_object_add(state, "pos", ucv_int64_new(us->s.line.pos)); return state; } static uc_value_t * uc_uline_set_state(uc_vm_t *vm, size_t nargs) { struct uc_uline_state *us = uc_fn_thisval("uline.state"); uc_value_t *state = uc_fn_arg(0); uc_value_t *arg; bool found; if (!us || ucv_type(state) != UC_OBJECT) return NULL; if ((arg = ucv_object_get(state, "prompt", NULL)) != NULL) { if (ucv_type(arg) != UC_STRING) return NULL; uline_set_prompt(&us->s, ucv_string_get(arg)); } if ((arg = ucv_object_get(state, "line", NULL)) != NULL) { if (ucv_type(arg) != UC_STRING) return NULL; uline_set_line(&us->s, ucv_string_get(arg), ucv_string_length(arg)); } if ((arg = ucv_object_get(state, "pos", NULL)) != NULL) { if (ucv_type(arg) != UC_INTEGER) return NULL; uline_set_cursor(&us->s, ucv_int64_get(arg)); } arg = ucv_object_get(state, "line2_prompt", &found); if (found) { if (!arg) uline_set_line2_prompt(&us->s, NULL); else if (ucv_type(arg) == UC_STRING) uline_set_line2_prompt(&us->s, ucv_string_get(arg)); else return NULL; } if ((arg = ucv_object_get(state, "line2", NULL)) != NULL) { if (ucv_type(arg) != UC_STRING) return NULL; uline_set_line2(&us->s, ucv_string_get(arg), ucv_string_length(arg)); } if ((arg = ucv_object_get(state, "line2_pos", NULL)) != NULL) { if (ucv_type(arg) != UC_INTEGER) return NULL; uline_set_line2_cursor(&us->s, ucv_int64_get(arg)); } return ucv_boolean_new(true); } static uc_value_t * uc_uline_set_hint(uc_vm_t *vm, size_t nargs) { struct uc_uline_state *us = uc_fn_thisval("uline.state"); uc_value_t *arg = uc_fn_arg(0); if (!us || ucv_type(arg) != UC_STRING) return NULL; uline_set_hint(&us->s, ucv_string_get(arg), ucv_string_length(arg)); return ucv_boolean_new(true); } static uc_value_t * uc_uline_set_uloop(uc_vm_t *vm, size_t nargs) { struct uc_uline_state *us = uc_fn_thisval("uline.state"); uc_value_t *cb = uc_fn_arg(0); if (!us || (cb && !ucv_is_callable(cb))) return NULL; us->poll_cb = cb; ucv_array_set(us->state, STATE_POLL_CB, ucv_get(cb)); if (cb) { uloop_fd_add(&us->fd, ULOOP_READ); us->fd.cb(&us->fd, 0); } else { uloop_fd_delete(&us->fd); } return ucv_boolean_new(true); } static uc_value_t * uc_uline_reset_key_input(uc_vm_t *vm, size_t nargs) { struct uc_uline_state *us = uc_fn_thisval("uline.state"); us->s.repeat_char = 0; return ucv_boolean_new(true); } static uc_value_t * uc_uline_hide_prompt(uc_vm_t *vm, size_t nargs) { struct uc_uline_state *us = uc_fn_thisval("uline.state"); if (!us) return NULL; uline_hide_prompt(&us->s); return ucv_boolean_new(true); } static uc_value_t * uc_uline_refresh_prompt(uc_vm_t *vm, size_t nargs) { struct uc_uline_state *us = uc_fn_thisval("uline.state"); if (!us) return NULL; uline_refresh_prompt(&us->s); return ucv_boolean_new(true); } static bool cb_prepare(struct uc_uline_state *us, const char *name) { uc_value_t *func; func = ucv_object_get(us->cb, name, NULL); if (!func) return false; uc_vm_stack_push(us->vm, ucv_get(us->res)); uc_vm_stack_push(us->vm, ucv_get(func)); return true; } static uc_value_t * cb_call_ret(struct uc_uline_state *us, size_t args, ...) { uc_vm_t *vm = us->vm; va_list ap; va_start(ap, args); for (size_t i = 0; i < args; i++) uc_vm_stack_push(vm, ucv_get(va_arg(ap, void *))); va_end(ap); if (uc_vm_call(vm, true, args) == EXCEPTION_NONE) return uc_vm_stack_pop(vm); return NULL; } #define cb_call(...) ucv_put(cb_call_ret(__VA_ARGS__)) static bool uc_uline_cb_line(struct uline_state *s, const char *str, size_t len) { struct uc_uline_state *us = container_of(s, struct uc_uline_state, s); bool complete = true; uc_value_t *ret; if (cb_prepare(us, "line_check")) { ret = cb_call_ret(us, 1, ucv_string_new_length(str, len)); complete = ucv_is_truish(ret); ucv_put(ret); } s->stop = complete; if (complete) us->line = ucv_string_new_length(str, len); return complete; } static void uc_uline_cb_event(struct uline_state *s, enum uline_event ev) { struct uc_uline_state *us = container_of(s, struct uc_uline_state, s); static const char * const ev_types[] = { [EDITLINE_EV_CURSOR_UP] = "cursor_up", [EDITLINE_EV_CURSOR_DOWN] = "cursor_down", [EDITLINE_EV_WINDOW_CHANGED] = "window_changed", [EDITLINE_EV_EOF] = "eof", [EDITLINE_EV_INTERRUPT] = "interrupt", }; if (ev > ARRAY_SIZE(ev_types) || !ev_types[ev]) return; if (!cb_prepare(us, ev_types[ev])) return; if (ev == EDITLINE_EV_WINDOW_CHANGED) cb_call(us, 2, ucv_int64_new(s->cols), ucv_int64_new(s->rows)); else cb_call(us, 0); } static void uc_uline_poll_cb(struct uloop_fd *fd, unsigned int events) { struct uc_uline_state *us = container_of(fd, struct uc_uline_state, fd); uc_value_t *res = ucv_get(us->res); uc_value_t *val; while (!uloop_cancelled && ucv_resource_data(res, NULL) && us->poll_cb) { uline_poll(&us->s); val = us->line; if (!val) break; us->line = NULL; if (!ucv_is_callable(us->poll_cb)) break; uc_vm_stack_push(us->vm, ucv_get(res)); uc_vm_stack_push(us->vm, ucv_get(us->poll_cb)); cb_call(us, 1, val); } ucv_put(res); } static bool uc_uline_cb_key_input(struct uline_state *s, unsigned char c, unsigned int count) { struct uc_uline_state *us = container_of(s, struct uc_uline_state, s); uc_value_t *ret; bool retval; if (!(us->input_mask[c / 32] & (1 << (c % 32)))) return false; if (!cb_prepare(us, "key_input")) return false; ret = cb_call_ret(us, 2, ucv_string_new_length((char *)&c, 1), ucv_int64_new(count)); retval = ucv_is_truish(ret); ucv_put(ret); return retval; } static void uc_uline_cb_line2_update(struct uline_state *s, const char *str, size_t len) { struct uc_uline_state *us = container_of(s, struct uc_uline_state, s); if (cb_prepare(us, "line2_update")) cb_call(us, 1, ucv_string_new_length(str, len)); } static bool uc_uline_cb_line2_cursor(struct uline_state *s) { struct uc_uline_state *us = container_of(s, struct uc_uline_state, s); uc_value_t *retval; bool ret = true; if (cb_prepare(us, "line2_cursor")) { retval = cb_call_ret(us, 0); ret = ucv_is_truish(retval); ucv_put(retval); } return ret; } static bool uc_uline_cb_line2_newline(struct uline_state *s, const char *str, size_t len) { struct uc_uline_state *us = container_of(s, struct uc_uline_state, s); uc_value_t *retval; bool ret = false; if (cb_prepare(us, "line2_newline")) { retval = cb_call_ret(us, 1, ucv_string_new_length(str, len)); ret = ucv_is_truish(retval); ucv_put(retval); } return ret; } static uc_value_t * uc_uline_new(uc_vm_t *vm, size_t nargs) { static const struct uline_cb uline_cb = { #define _CB(_type) ._type = uc_uline_cb_##_type _CB(key_input), _CB(line), _CB(event), _CB(line2_update), _CB(line2_cursor), _CB(line2_newline), #undef _CB }; uc_value_t *data = uc_fn_arg(0); struct uc_uline_state *us; FILE *input, *output; uc_value_t *arg, *cb, *state, *res; if (ucv_type(data) != UC_OBJECT) return NULL; cb = ucv_object_get(data, "cb", NULL); if (ucv_type(cb) != UC_OBJECT) return NULL; state = ucv_array_new(vm); ucv_array_set(state, 0, ucv_get(cb)); if ((arg = ucv_object_get(data, "input", NULL)) != NULL) { input = ucv_resource_data(arg, "fs.file"); ucv_array_set(state, STATE_INPUT, ucv_get(arg)); } else { input = stdin; } if ((arg = ucv_object_get(data, "output", NULL)) != NULL) { output = ucv_resource_data(arg, "fs.file"); ucv_array_set(state, STATE_OUTPUT, ucv_get(arg)); } else { output = stdout; } if (!input || !output) { input = output = NULL; return NULL; } us = calloc(1, sizeof(*us)); us->vm = vm; us->state = ucv_array_new(vm); ucv_array_set(us->state, STATE_CB, ucv_get(cb)); us->cb = cb; us->registry_index = registry_set(vm, state); if ((arg = ucv_object_get(data, "key_input_list", NULL)) != NULL) { uc_value_t *val; size_t len; if (ucv_type(arg) != UC_ARRAY) goto free; len = ucv_array_length(arg); for (size_t i = 0; i < len; i++) { unsigned char c; val = ucv_array_get(arg, i); if (ucv_type(val) != UC_STRING || ucv_string_length(val) != 1) goto free; c = ucv_string_get(val)[0]; us->input_mask[c / 32] |= 1 << (c % 32); } } res = ucv_resource_new(state_type, us); ucv_array_set(us->state, STATE_RES, ucv_get(res)); us->res = res; us->fd.fd = fileno(input); us->fd.cb = uc_uline_poll_cb; uline_init(&us->s, &uline_cb, us->fd.fd, output, true); return res; free: free(us); return NULL; } static void free_state(void *ptr) { struct uc_uline_state *us = ptr; uc_value_t *registry; if (!us) return; uloop_fd_delete(&us->fd); registry = uc_vm_registry_get(us->vm, "uline.registry"); ucv_array_set(registry, us->registry_index, NULL); uline_free(&us->s); free(us); } static uc_value_t * uc_uline_close(uc_vm_t *vm, size_t nargs) { struct uline_state **s = uc_fn_this("uline.state"); if (!s || !*s) return NULL; free_state(*s); *s = NULL; return NULL; } static bool skip_space(const char **str, const char *end) { while (*str < end && isspace(**str)) (*str)++; return *str < end; } static void add_str(uc_stringbuf_t **buf, const char *str, const char *next) { if (str == next) return; if (!*buf) *buf = ucv_stringbuf_new(); ucv_stringbuf_addstr(*buf, str, next - str); } static void uc_uline_add_pos(uc_vm_t *vm, uc_value_t *list, ssize_t start, ssize_t end) { uc_value_t *val = ucv_array_new(vm); ucv_array_push(val, ucv_int64_new(start)); ucv_array_push(val, ucv_int64_new(end)); ucv_array_push(list, val); } static uc_value_t * uc_uline_parse_args(uc_vm_t *vm, size_t nargs, bool check) { struct uc_arg_parser *argp = uc_fn_thisval("uline.argp"); uc_value_t *list = NULL, *pos_list = NULL; uc_value_t *args = NULL, *pos_args = NULL; uc_value_t *str_arg = uc_fn_arg(0); uc_stringbuf_t *buf = NULL; uc_value_t *missing = NULL; uc_value_t *ret; const char *start, *str, *end; ssize_t start_idx = -1, end_idx = 0; enum { UNQUOTED, BACKSLASH, SINGLE_QUOTE, DOUBLE_QUOTE, DOUBLE_QUOTE_BACKSLASH, } state = UNQUOTED; static const char * const state_str[] = { [BACKSLASH] = "\\", [SINGLE_QUOTE] = "'", [DOUBLE_QUOTE] = "\"", [DOUBLE_QUOTE_BACKSLASH] = "\\\"", }; #define UNQUOTE_TOKENS " \t\r\n'\"\\" char unquote_tok[] = UNQUOTE_TOKENS "\x00"; unquote_tok[strlen(UNQUOTE_TOKENS)] = argp->line_sep; if (!argp || ucv_type(str_arg) != UC_STRING) return NULL; if (!check) { list = ucv_array_new(vm); pos_list = ucv_array_new(vm); if (argp->line_sep) { args = ucv_array_new(vm); pos_args = ucv_array_new(vm); ucv_array_push(args, list); ucv_array_push(pos_args, pos_list); } else { args = list; pos_args = pos_list; } } start = str = ucv_string_get(str_arg); end = str + ucv_string_length(str_arg); skip_space(&str, end); while (*str && str < end) { const char *next; switch (state) { case UNQUOTED: if (isspace(*str)) { skip_space(&str, end); if (!buf) continue; ucv_array_push(list, ucv_stringbuf_finish(buf)); uc_uline_add_pos(vm, pos_list, start_idx, end_idx); start_idx = -1; buf = NULL; continue; } if (start_idx < 0) start_idx = str - start; next = str + strcspn(str, unquote_tok); if (list) add_str(&buf, str, next); str = next; end_idx = str - start; switch (*str) { case 0: continue; case '\'': state = SINGLE_QUOTE; break; case '"': state = DOUBLE_QUOTE; break; case '\\': state = BACKSLASH; break; default: if (argp->line_sep && *str == argp->line_sep) { str++; if (list) { if (buf) { ucv_array_push(list, ucv_stringbuf_finish(buf)); uc_uline_add_pos(vm, pos_list, start_idx, end_idx); start_idx = -1; } buf = NULL; list = ucv_array_new(vm); ucv_array_push(args, list); pos_list = ucv_array_new(vm); ucv_array_push(pos_args, pos_list); } } continue; } if (!buf) buf = ucv_stringbuf_new(); str++; break; case BACKSLASH: case DOUBLE_QUOTE_BACKSLASH: if (start_idx < 0) start_idx = str - start; if (list && *str != '\n') add_str(&buf, str, str + 1); str++; state--; end_idx = str - start; break; case SINGLE_QUOTE: if (start_idx < 0) start_idx = str - start; next = str + strcspn(str, "'"); if (list) add_str(&buf, str, next); str = next; if (*str == '\'') { state = UNQUOTED; str++; } end_idx = str - start; break; case DOUBLE_QUOTE: if (start_idx < 0) start_idx = str - start; next = str + strcspn(str, "\"\\"); if (list) add_str(&buf, str, next); str = next; if (*str == '"') { state = UNQUOTED; str++; } else if (*str == '\\') { state = DOUBLE_QUOTE_BACKSLASH; str++; } end_idx = str - start; } } if (buf) { ucv_array_push(list, ucv_stringbuf_finish(buf)); uc_uline_add_pos(vm, pos_list, start_idx, end_idx); } if (state_str[state]) missing = ucv_string_new(state_str[state]); if (!list) return missing; ret = ucv_object_new(vm); ucv_object_add(ret, "args", args); ucv_object_add(ret, "pos", pos_args); if (missing) ucv_object_add(ret, "missing", missing); return ret; } static uc_value_t * uc_uline_arg_parser(uc_vm_t *vm, size_t nargs) { uc_value_t *opts = uc_fn_arg(0); struct uc_arg_parser *argp; uc_value_t *a; char sep = 0; if ((a = ucv_object_get(opts, "line_separator", NULL)) != NULL) { if (ucv_type(a) != UC_STRING || ucv_string_length(a) != 1) return NULL; sep = ucv_string_get(a)[0]; } argp = calloc(1, sizeof(*argp)); argp->line_sep = sep; return ucv_resource_new(argp_type, argp); } static uc_value_t * uc_uline_argp_parse(uc_vm_t *vm, size_t nargs) { return uc_uline_parse_args(vm, nargs, false); } static uc_value_t * uc_uline_argp_check(uc_vm_t *vm, size_t nargs) { return uc_uline_parse_args(vm, nargs, true); } static uc_value_t * uc_uline_argp_escape(uc_vm_t *vm, size_t nargs) { uc_value_t *arg = uc_fn_arg(0); uc_value_t *ref_arg = uc_fn_arg(1); const char *str, *next; uc_stringbuf_t *buf; char ref = 0; if (ucv_type(arg) != UC_STRING) return NULL; if (ucv_type(ref_arg) == UC_STRING) ref = ucv_string_get(ref_arg)[0]; str = ucv_string_get(arg); if (ref != '"' && ref != '\'') { next = str + strcspn(str, "\n\t '\""); if (*next) ref = '"'; } if (ref != '"' && ref != '\'') return ucv_string_new(str); buf = ucv_stringbuf_new(); ucv_stringbuf_addstr(buf, &ref, 1); while (*str) { next = strchr(str, ref); if (!next) { ucv_stringbuf_addstr(buf, str, strlen(str)); break; } if (next - str) ucv_stringbuf_addstr(buf, str, next - str); if (ref == '\'') ucv_stringbuf_addstr(buf, "'\\''", 4); else ucv_stringbuf_addstr(buf, "\\\"", 2); str = next + 1; } ucv_stringbuf_addstr(buf, &ref, 1); return ucv_stringbuf_finish(buf); } static uc_value_t * uc_uline_getpass(uc_vm_t *vm, size_t nargs) { uc_value_t *prompt = uc_fn_arg(0); char *pw; if (ucv_type(prompt) != UC_STRING) return NULL; pw = getpass(ucv_string_get(prompt)); if (!pw) return NULL; return ucv_string_new(pw); } static const uc_function_list_t argp_fns[] = { { "parse", uc_uline_argp_parse }, { "check", uc_uline_argp_check }, { "escape", uc_uline_argp_escape }, }; static const uc_function_list_t state_fns[] = { { "close", uc_uline_close }, { "poll", uc_uline_poll }, { "poll_stop", uc_uline_poll_stop }, { "poll_key", uc_uline_poll_key }, { "reset_key_input", uc_uline_reset_key_input }, { "get_line", uc_uline_get_line }, { "get_window", uc_uline_get_window }, { "set_hint", uc_uline_set_hint }, { "set_state", uc_uline_set_state }, { "set_uloop", uc_uline_set_uloop }, { "hide_prompt", uc_uline_hide_prompt }, { "refresh_prompt", uc_uline_refresh_prompt }, }; static const uc_function_list_t global_fns[] = { { "new", uc_uline_new }, { "arg_parser", uc_uline_arg_parser }, { "getpass", uc_uline_getpass }, }; void uc_module_init(uc_vm_t *vm, uc_value_t *scope) { uc_function_list_register(scope, global_fns); state_type = uc_type_declare(vm, "uline.state", state_fns, free_state); argp_type = uc_type_declare(vm, "uline.argp", argp_fns, free); registry = ucv_array_new(vm); uc_vm_registry_set(vm, "uline.registry", registry); }