/* * Copyright (C) 2020-2021 Jo-Philipp Wich * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ /** * # Builtin functions * * The core namespace is not an actual module but refers to the set of * builtin functions and properties available to `ucode` scripts. * * @module core */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "json-c-compat.h" #include "ucode/lexer.h" #include "ucode/compiler.h" #include "ucode/vm.h" #include "ucode/lib.h" #include "ucode/source.h" #include "ucode/program.h" #include "ucode/platform.h" static void format_context_line(uc_stringbuf_t *buf, const char *line, size_t off, bool compact) { unsigned padlen, i; const char *p; for (p = line, padlen = 0; *p != '\n' && *p != '\0'; p++) { if (compact && (p - line) == (ptrdiff_t)off) ucv_stringbuf_append(buf, "\033[22m"); switch (*p) { case '\t': ucv_stringbuf_append(buf, " "); if (p < line + off) padlen += 4; break; case '\r': case '\v': ucv_stringbuf_append(buf, " "); if (p < line + off) padlen++; break; default: ucv_stringbuf_addstr(buf, p, 1); if (p < line + off) padlen++; } } if (compact) { ucv_stringbuf_append(buf, "\033[m\n"); return; } ucv_stringbuf_append(buf, "`\n "); if (padlen < strlen("Near here ^")) { for (i = 0; i < padlen; i++) ucv_stringbuf_append(buf, " "); ucv_stringbuf_append(buf, "^-- Near here\n"); } else { ucv_stringbuf_append(buf, "Near here "); for (i = strlen("Near here "); i < padlen; i++) ucv_stringbuf_append(buf, "-"); ucv_stringbuf_append(buf, "^\n"); } } static char * source_filename(uc_source_t *src, uint32_t line) { const char *name = src->filename ? basename(src->filename) : "[?]"; static char buf[sizeof("xxxxxxxxx.uc:0000000000")]; size_t len = strlen(name); if (len > 12) snprintf(buf, sizeof(buf), "...%s:%u", name + (len - 9), line); else snprintf(buf, sizeof(buf), "%12s:%u", name, line); return buf; } bool uc_source_context_format(uc_stringbuf_t *buf, uc_source_t *src, size_t off, bool compact) { size_t len, rlen; bool truncated; char line[256]; long srcpos; int eline; srcpos = ftell(src->fp); if (srcpos == -1) return false; fseek(src->fp, 0, SEEK_SET); truncated = false; eline = 1; rlen = 0; while (fgets(line, sizeof(line), src->fp)) { len = strlen(line); rlen += len; if (rlen >= off) { if (compact) ucv_stringbuf_printf(buf, "\033[2;40;97m%17s %s", source_filename(src, eline), truncated ? "..." : ""); else ucv_stringbuf_printf(buf, "\n `%s", truncated ? "..." : ""); format_context_line(buf, line, len - (rlen - off) + (truncated ? 3 : 0), compact); break; } truncated = (len > 0 && line[len-1] != '\n'); eline += !truncated; } fseek(src->fp, srcpos, SEEK_SET); return true; } bool uc_error_context_format(uc_stringbuf_t *buf, uc_source_t *src, uc_value_t *stacktrace, size_t off) { uc_value_t *e, *fn, *file, *line, *byte; const char *path; size_t idx; for (idx = 0; idx < (stacktrace ? ucv_array_length(stacktrace) : 0); idx++) { e = ucv_array_get(stacktrace, idx); fn = ucv_object_get(e, "function", NULL); file = ucv_object_get(e, "filename", NULL); if (idx == 0) { path = (file && strcmp(ucv_string_get(file), "[stdin]")) ? ucv_string_get(file) : NULL; if (path && fn) ucv_stringbuf_printf(buf, "In %s(), file %s, ", ucv_string_get(fn), path); else if (fn) ucv_stringbuf_printf(buf, "In %s(), ", ucv_string_get(fn)); else if (path) ucv_stringbuf_printf(buf, "In %s, ", path); else ucv_stringbuf_append(buf, "In "); ucv_stringbuf_printf(buf, "line %" PRId64 ", byte %" PRId64 ":\n", ucv_int64_get(ucv_object_get(e, "line", NULL)), ucv_int64_get(ucv_object_get(e, "byte", NULL))); } else { line = ucv_object_get(e, "line", NULL); byte = ucv_object_get(e, "byte", NULL); ucv_stringbuf_printf(buf, " called from %s%s (%s", fn ? "function " : "anonymous function", fn ? ucv_string_get(fn) : "", file ? ucv_string_get(file) : ""); if (line && byte) ucv_stringbuf_printf(buf, ":%" PRId64 ":%" PRId64 ")\n", ucv_int64_get(line), ucv_int64_get(byte)); else ucv_stringbuf_append(buf, "[C])\n"); } } return uc_source_context_format(buf, src, off, false); } void uc_error_message_indent(char **msg) { uc_stringbuf_t *buf = xprintbuf_new(); char *s, *p, *nl; size_t len; if (!msg || !*msg) return; s = *msg; len = strlen(s); while (len > 0 && s[len-1] == '\n') s[--len] = 0; for (p = s, nl = strchr(p, '\n'); p != NULL; p = nl ? nl + 1 : NULL, nl = p ? strchr(p, '\n') : NULL) { if (!nl) ucv_stringbuf_printf(buf, " | %s", p); else if (nl != p) ucv_stringbuf_printf(buf, " | %.*s\n", (int)(nl - p), p); else ucv_stringbuf_append(buf, " |\n"); } ucv_stringbuf_append(buf, "\n"); *msg = buf->buf; free(buf); free(s); } static char *uc_cast_string(uc_vm_t *vm, uc_value_t **v, bool *freeable) { if (ucv_type(*v) == UC_STRING) { *freeable = false; return _ucv_string_get(v); } *freeable = true; return ucv_to_string(vm, *v); } static void uc_vm_ctx_push(uc_vm_t *vm) { uc_value_t *ctx = NULL; if (vm->callframes.count >= 2) ctx = vm->callframes.entries[vm->callframes.count - 2].ctx; uc_vm_stack_push(vm, ucv_get(ctx)); } static uc_value_t * uc_print_common(uc_vm_t *vm, size_t nargs, FILE *fh) { uc_value_t *item; size_t reslen = 0; size_t len = 0; size_t arridx; char *p; for (arridx = 0; arridx < nargs; arridx++) { item = uc_fn_arg(arridx); if (ucv_type(item) == UC_STRING) { len = ucv_string_length(item); reslen += fwrite(ucv_string_get(item), 1, len, fh); } else if (item != NULL) { p = ucv_to_string(vm, item); len = strlen(p); reslen += fwrite(p, 1, len, fh); free(p); } } return ucv_int64_new(reslen); } /** * Print any of the given values to stdout. * * The `print()` function writes a string representation of each given argument * to stdout and returns the amount of bytes written. * * String values are printed as-is, integer and double values are printed in * decimal notation, boolean values are printed as `true` or `false` while * arrays and objects are converted to their JSON representation before being * written to the standard output. The `null` value is represented by an empty * string so `print(null)` would print nothing. Resource values are printed in * the form ``, e.g. ``. * * If resource, array or object values contain a `tostring()` function in their * prototypes, then this function is invoked to obtain an alternative string * representation of the value. * * Examples: * * ```javascript * print(1 != 2); // Will print 'true' * print(0xff); // Will print '255' * print(2e3); // Will print '2000' * print(null); // Will print nothing * print({ hello: true, world: 123 }); // Will print '{ "hello": true, "world": 123 }' * print([1,2,3]); // Will print '[ 1, 2, 3 ]' * * print(proto({ foo: "bar" }, // Will print 'MyObj' * { tostring: () => "MyObj" })); // instead of '{ "foo": "bar" }' * * ``` * * Returns the amount of bytes printed. * * @function module:core#print * * @param {...*} values * Arbitrary values to print * * @returns {number} */ static uc_value_t * uc_print(uc_vm_t *vm, size_t nargs) { return uc_print_common(vm, nargs, vm->output); } /** * Determine the length of the given object, array or string. * * Returns the length of the given value. * * - For strings, the length is the amount of bytes within the string * - For arrays, the length is the amount of array elements * - For objects, the length is defined as the amount of keys * * Returns `null` if the given argument is not an object, array or string. * * @function module:core#length * * @param {Object|Array|string} x - The input object, array, or string. * * @returns {?number} - The length of the input. * * @example * length("test") // 4 * length([true, false, null, 123, "test"]) // 5 * length({foo: true, bar: 123, baz: "test"}) // 3 * length({}) // 0 * length(true) // null * length(10.0) // null */ static uc_value_t * uc_length(uc_vm_t *vm, size_t nargs) { uc_value_t *arg = uc_fn_arg(0); switch (ucv_type(arg)) { case UC_OBJECT: return ucv_int64_new(ucv_object_length(arg)); case UC_ARRAY: return ucv_int64_new(ucv_array_length(arg)); case UC_STRING: return ucv_int64_new(ucv_string_length(arg)); default: return NULL; } } static int uc_uniq_ucv_equal(const void *k1, const void *k2); static uc_value_t * uc_index(uc_vm_t *vm, size_t nargs, bool right) { uc_value_t *stack = uc_fn_arg(0); uc_value_t *needle = uc_fn_arg(1); const char *sstr, *nstr, *p; size_t arridx, slen, nlen; ssize_t ret = -1; switch (ucv_type(stack)) { case UC_ARRAY: if (right) { for (arridx = ucv_array_length(stack); arridx > 0; arridx--) { if (uc_uniq_ucv_equal(ucv_array_get(stack, arridx - 1), needle)) { ret = (ssize_t)(arridx - 1); break; } } } else { for (arridx = 0, slen = ucv_array_length(stack); arridx < slen; arridx++) { if (uc_uniq_ucv_equal(ucv_array_get(stack, arridx), needle)) { ret = (ssize_t)arridx; break; } } } return ucv_int64_new(ret); case UC_STRING: if (ucv_type(needle) == UC_STRING) { sstr = ucv_string_get(stack); slen = ucv_string_length(stack); nstr = ucv_string_get(needle); nlen = ucv_string_length(needle); if (slen == nlen) { if (memcmp(sstr, nstr, nlen) == 0) ret = 0; } else if (slen > nlen) { if (right) { p = sstr + slen - nlen; do { if (memcmp(p, nstr, nlen) == 0) { ret = (ssize_t)(p - sstr); break; } } while (--p != sstr); } else if (nlen > 0) { p = (const char *)memmem(sstr, slen, nstr, nlen); if (p) ret = (ssize_t)(p - sstr); } else { ret = 0; } } } return ucv_int64_new(ret); default: return NULL; } } /** * Finds the given value passed as the second argument within the array or * string specified in the first argument. * * Returns the first matching array index or first matching string offset or * `-1` if the value was not found. * * Returns `null` if the first argument was neither an array nor a string. * * @function module:core#index * * @param {Array|string} arr_or_str * The array or string to search for the value. * * @param {*} needle * The value to find within the array or string. * * @returns {?number} * * @example * index("Hello hello hello", "ll") // 2 * index([ 1, 2, 3, 1, 2, 3, 1, 2, 3 ], 2) // 1 * index("foo", "bar") // -1 * index(["Red", "Blue", "Green"], "Brown") // -1 * index(123, 2) // null */ static uc_value_t * uc_lindex(uc_vm_t *vm, size_t nargs) { return uc_index(vm, nargs, false); } /** * Finds the given value passed as the second argument within the array or * string specified in the first argument. * * Returns the last matching array index or last matching string offset or * `-1` if the value was not found. * * Returns `null` if the first argument was neither an array nor a string. * * @function module:core#rindex * * @param {Array|string} arr_or_str * The array or string to search for the value. * * @param {*} needle * The value to find within the array or string. * * @returns {?number} * * @example * rindex("Hello hello hello", "ll") // 14 * rindex([ 1, 2, 3, 1, 2, 3, 1, 2, 3 ], 2) // 7 * rindex("foo", "bar") // -1 * rindex(["Red", "Blue", "Green"], "Brown") // -1 * rindex(123, 2) // null */ static uc_value_t * uc_rindex(uc_vm_t *vm, size_t nargs) { return uc_index(vm, nargs, true); } static bool assert_mutable(uc_vm_t *vm, uc_value_t *val) { if (ucv_is_constant(val)) { uc_vm_raise_exception(vm, EXCEPTION_TYPE, "%s value is immutable", ucv_typename(val)); return false; } return true; } static bool assert_mutable_array(uc_vm_t *vm, uc_value_t *val) { if (ucv_type(val) != UC_ARRAY) return false; return assert_mutable(vm, val); } /** * Pushes the given argument(s) to the given array. * * Returns the last pushed value. * * @function module:core#push * * @param {Array} arr * The array to push values to. * * @param {...*} [values] * The values to push. * * @returns {*} * * @example * let x = [ 1, 2, 3 ]; * push(x, 4, 5, 6); // 6 * print(x, "\n"); // [ 1, 2, 3, 4, 5, 6 ] */ static uc_value_t * uc_push(uc_vm_t *vm, size_t nargs) { uc_value_t *arr = uc_fn_arg(0); uc_value_t *item = NULL; size_t arridx; if (!assert_mutable_array(vm, arr)) return NULL; for (arridx = 1; arridx < nargs; arridx++) { item = uc_fn_arg(arridx); ucv_array_push(arr, ucv_get(item)); } return ucv_get(item); } /** * Pops the last item from the given array and returns it. * * Returns `null` if the array was empty or if a non-array argument was passed. * * @function module:core#pop * * @param {Array} arr * The input array. * * @returns {*} * * @example * let x = [ 1, 2, 3 ]; * pop(x); // 3 * print(x, "\n"); // [ 1, 2 ] */ static uc_value_t * uc_pop(uc_vm_t *vm, size_t nargs) { uc_value_t *arr = uc_fn_arg(0); if (!assert_mutable_array(vm, arr)) return NULL; return ucv_array_pop(arr); } /** * Pops the first item from the given array and returns it. * * Returns `null` if the array was empty or if a non-array argument was passed. * * @function module:core#shift * * @param {Array} arr * The array from which to pop the first item. * * @returns {*} * * @example * let x = [ 1, 2, 3 ]; * shift(x); // 1 * print(x, "\n"); // [ 2, 3 ] */ static uc_value_t * uc_shift(uc_vm_t *vm, size_t nargs) { uc_value_t *arr = uc_fn_arg(0); if (!assert_mutable_array(vm, arr)) return NULL; return ucv_array_shift(arr); } /** * Add the given values to the beginning of the array passed via first argument. * * Returns the last value added to the array. * * @function module:core#unshift * * @param {Array} arr * The array to which the values will be added. * * @param {...*} * Values to add. * * @returns {*} * * @example * let x = [ 3, 4, 5 ]; * unshift(x, 1, 2); // 2 * print(x, "\n"); // [ 1, 2, 3, 4, 5 ] */ static uc_value_t * uc_unshift(uc_vm_t *vm, size_t nargs) { uc_value_t *arr = uc_fn_arg(0); uc_value_t *item; size_t i; if (!assert_mutable_array(vm, arr)) return NULL; for (i = 1; i < nargs; i++) { item = uc_fn_arg(nargs - i); ucv_array_unshift(arr, ucv_get(item)); } return (nargs > 1) ? ucv_get(uc_fn_arg(nargs - 1)) : NULL; } /** * Converts each given numeric value to a byte and return the resulting string. * Invalid numeric values or values < 0 result in `\0` bytes, values larger than * 255 are truncated to 255. * * Returns a new strings consisting of the given byte values. * * @function module:core#chr * * @param {...number} n1 * The numeric values. * * @returns {string} * * @example * chr(65, 98, 99); // "Abc" * chr(-1, 300); // string consisting of an `0x0` and a `0xff` byte */ static uc_value_t * uc_chr(uc_vm_t *vm, size_t nargs) { uc_value_t *rv = NULL; size_t idx; int64_t n; char *str; if (!nargs) return ucv_string_new_length("", 0); str = xalloc(nargs); for (idx = 0; idx < nargs; idx++) { n = ucv_to_integer(uc_fn_arg(idx)); if (n < 0) n = 0; else if (n > 255) n = 255; str[idx] = (char)n; } rv = ucv_string_new_length(str, nargs); free(str); return rv; } /** * Raise an exception with the given message and abort execution. * * @function module:core#die * * @param {string} msg * The error message. * * @throws {Error} * The error with the given message. * * @example * die(msg); */ static uc_value_t * uc_die(uc_vm_t *vm, size_t nargs) { uc_value_t *msg = uc_fn_arg(0); bool freeable = false; char *s; s = msg ? uc_cast_string(vm, &msg, &freeable) : "Died"; uc_vm_raise_exception(vm, EXCEPTION_USER, "%s", s); if (freeable) free(s); return NULL; } /** * Check whether the given key exists within the given object value. * * Returns `true` if the given key is present within the object passed as the * first argument, otherwise `false`. * * @function module:core#exists * * @param {Object} obj * The input object. * * @param {string} key * The key to check for existence. * * @returns {boolean} * * @example * let x = { foo: true, bar: false, qrx: null }; * exists(x, 'foo'); // true * exists(x, 'qrx'); // true * exists(x, 'baz'); // false */ static uc_value_t * uc_exists(uc_vm_t *vm, size_t nargs) { uc_value_t *obj = uc_fn_arg(0); uc_value_t *key = uc_fn_arg(1); bool found, freeable; char *k; if (ucv_type(obj) != UC_OBJECT) return ucv_boolean_new(false); k = uc_cast_string(vm, &key, &freeable); ucv_object_get(obj, k, &found); if (freeable) free(k); return ucv_boolean_new(found); } /** * Terminate the interpreter with the given exit code. * * This function does not return. * * @function module:core#exit * * @param {number} n * The exit code. * * @example * exit(); * exit(5); */ static uc_value_t * uc_exit(uc_vm_t *vm, size_t nargs) { int64_t n = ucv_to_integer(uc_fn_arg(0)); vm->arg.s32 = (int32_t)n; uc_vm_raise_exception(vm, EXCEPTION_EXIT, "Terminated"); return NULL; } /** * Query an environment variable or then entire environment. * * Returns the value of the given environment variable, or - if omitted - a * dictionary containing all environment variables. * * @function module:core#getenv * * @param {string} [name] * The name of the environment variable. * * @returns {string|Object} */ static uc_value_t * uc_getenv(uc_vm_t *vm, size_t nargs) { uc_value_t *key = uc_fn_arg(0), *rv = NULL; extern char **environ; char **env = environ; char *k, *v; if (!key) { rv = ucv_object_new(vm); while (*env) { v = strchr(*env, '='); if (v) { xasprintf(&k, "%.*s", (int)(v - *env), *env); ucv_object_add(rv, k, ucv_string_new(v + 1)); free(k); } env++; } } else if (ucv_type(key) == UC_STRING) { k = ucv_string_get(key); v = getenv(k); if (v) rv = ucv_string_new(v); } return rv; } /** * Filter the array passed as the first argument by invoking the function * specified in the second argument for each array item. * * If the invoked function returns a truthy result, the item is retained, * otherwise, it is dropped. The filter function is invoked with three * arguments: * * 1. The array value * 2. The current index * 3. The array being filtered * * (Note that the `map` function behaves similarly to `filter` with respect * to its `fn` parameters.) * * Returns a new array containing only retained items, in the same order as * the input array. * * @function module:core#filter * * @param {Array} arr * The input array. * * @param {Function} fn * The filter function. * * @returns {Array} * * @example * // filter out any empty string: * a = filter(["foo", "", "bar", "", "baz"], length) * // a = ["foo", "bar", "baz"] * * // filter out any non-number type: * a = filter(["foo", 1, true, null, 2.2], function(v) { * return (type(v) == "int" || type(v) == "double"); * }); * // a = [1, 2.2] */ static uc_value_t * uc_filter(uc_vm_t *vm, size_t nargs) { uc_value_t *obj = uc_fn_arg(0); uc_value_t *func = uc_fn_arg(1); uc_value_t *rv, *arr; size_t arridx, arrlen; if (ucv_type(obj) != UC_ARRAY) return NULL; arr = ucv_array_new(vm); for (arrlen = ucv_array_length(obj), arridx = 0; arridx < arrlen; arridx++) { uc_vm_ctx_push(vm); uc_vm_stack_push(vm, ucv_get(func)); uc_vm_stack_push(vm, ucv_get(ucv_array_get(obj, arridx))); uc_vm_stack_push(vm, ucv_int64_new(arridx)); uc_vm_stack_push(vm, ucv_get(obj)); if (uc_vm_call(vm, true, 3)) { ucv_put(arr); return NULL; } rv = uc_vm_stack_pop(vm); if (ucv_is_truish(rv)) ucv_array_push(arr, ucv_get(ucv_array_get(obj, arridx))); ucv_put(rv); } return arr; } /** * Converts the given hexadecimal string into a number. * * Returns the resulting integer value or `NaN` if the input value cannot be * interpreted as hexadecimal number. * * @function module:core#hex * * @param {*} x * The hexadecimal string to be converted. * * @returns {number} */ static uc_value_t * uc_hex(uc_vm_t *vm, size_t nargs) { uc_value_t *val = uc_fn_arg(0); char *e, *v; int64_t n; v = ucv_string_get(val); if (!v || !isxdigit(*v)) return ucv_double_new(NAN); n = strtoll(v, &e, 16); if (e == v || *e) return ucv_double_new(NAN); return ucv_int64_new(n); } /** * Converts the given value to an integer, using an optional base. * * Returns `NaN` if the value is not convertible. * * @function module:core#int * * @param {*} x * The value to be converted to an integer. * * @param {int} [base] * The base into which the value is to be converted, the default is 10. * Note that the base parameter is ignored if the `x` value is already numeric. * * @returns {number} * * @example * int("123") // Returns 123 * int("123", 10) // 123 * int("10 or more") // 10 * int("12.3") // 12 * int("123", 7) // 66 * int("abc", 16) // 2748 * int("xyz", 36) // 44027 * int(10.10, "2") // 10, the invalid base is ignored * int("xyz", 16) // NaN, bad value * int("1010", "2") // NaN, bad base */ static uc_value_t * uc_int(uc_vm_t *vm, size_t nargs) { uc_value_t *val = uc_fn_arg(0); uc_value_t *base = uc_fn_arg(1); char *e, *v; int64_t n; if (ucv_type(val) == UC_STRING) { errno = 0; v = ucv_string_get(val); n = strtoll(v, &e, base ? ucv_int64_get(base) : 10); if (e == v) return ucv_double_new(NAN); } else { n = ucv_to_integer(val); } if (errno == EINVAL || errno == ERANGE) return ucv_double_new(NAN); return ucv_int64_new(n); } /** * Joins the array passed as the second argument into a string, using the * separator passed in the first argument as glue. * * Returns `null` if the second argument is not an array. * * @function module:core#join * * @param {string} sep * The separator to be used in joining the array elements. * * @param {Array} arr * The array to be joined into a string. * * @returns {?string} */ static uc_value_t * uc_join(uc_vm_t *vm, size_t nargs) { uc_value_t *sep = uc_fn_arg(0); uc_value_t *arr = uc_fn_arg(1); size_t arrlen, arridx; uc_stringbuf_t *buf; if (ucv_type(arr) != UC_ARRAY) return NULL; buf = ucv_stringbuf_new(); for (arrlen = ucv_array_length(arr), arridx = 0; arridx < arrlen; arridx++) { if (arridx > 0) ucv_to_stringbuf(vm, buf, sep, false); ucv_to_stringbuf(vm, buf, ucv_array_get(arr, arridx), false); } return ucv_stringbuf_finish(buf); } /** * Enumerates all object key names. * * Returns an array of all key names present in the passed object. * Returns `null` if the given argument is not an object. * * @function module:core#keys * * @param {object} obj * The object from which to retrieve the key names. * * @returns {?Array} */ static uc_value_t * uc_keys(uc_vm_t *vm, size_t nargs) { uc_value_t *obj = uc_fn_arg(0); uc_value_t *arr = NULL; if (ucv_type(obj) != UC_OBJECT) return NULL; arr = ucv_array_new(vm); ucv_object_foreach(obj, key, val) { (void)val; ucv_array_push(arr, ucv_string_new(key)); } return arr; } /** * Convert the given string to lowercase and return the resulting string. * * Returns `null` if the given argument could not be converted to a string. * * @function module:core#lc * * @param {string} s * The input string. * * @returns {?string} * The lowercase string. * * @example * lc("HeLLo WoRLd!"); // "hello world!" */ static uc_value_t * uc_lc(uc_vm_t *vm, size_t nargs) { char *str = ucv_to_string(vm, uc_fn_arg(0)); uc_value_t *rv = NULL; char *p; if (!str) return NULL; for (p = str; *p; p++) if (*p >= 'A' && *p <= 'Z') *p |= 32; rv = ucv_string_new(str); free(str); return rv; } /** * Transform the array passed as the first argument by invoking the function * specified in the second argument for each array item. * * The mapping function is invoked with three arguments (see examples, below, * for some possibly counterintuitive usage): * * 1. The array value * 2. The current index * 3. The array being filtered * * (Note that the `filter` function behaves similarly to `map` with respect * to its `fn` parameters.) * * Returns a new array of the same length as the input array containing the * transformed values. * * @function module:core#map * * @param {Array} arr * The input array. * * @param {Function} fn * The mapping function. * * @returns {Array} * * @example * // turn into an array of string lengths: * a = map(["Apple", "Banana", "Bean"], length); * // a = [5, 6, 4] * * // map to type names: * a = map(["foo", 1, true, null, 2.2], type); * // a = ["string", "int", "bool", null, "double"] * * // attempt to naively use built-in 'int' to map an array: * a = map(["x", "2", "11", "7"], int) * // a = [NaN, NaN, 3, NaN] * // * // This is a direct result of 'int' being provided the second, index parameter * // for its base value in the conversion. * // * // The resulting calls to 'int' are as follows: * // int("x", 0, [...]) - convert "x" to base 0, 'int' ignores the third value * // int("2", 1, [...]) - convert "2" to base 1, digit out of range, so NaN * // int("11", 2, [...]) - convert "11" to base 2, produced unexpected 3 * // int("7", 3, [...]) - convert "7" to base 3, digit out of range, NaN again * * // remedy this by using an arrow function to ensure the proper base value * // (in this case, the default of 10) is passed to 'int': * a = map(["x", "2", "1", "7"], (x) => int(x)) * // a = [NaN, 2, 1, 7] * * // convert base-2 values: * a = map(["22", "1010", "0001", "0101"], (x) => int(x, 2)) * // a = [NaN, 10, 1, 5] */ static uc_value_t * uc_map(uc_vm_t *vm, size_t nargs) { uc_value_t *obj = uc_fn_arg(0); uc_value_t *func = uc_fn_arg(1); uc_value_t *arr, *rv; size_t arridx, arrlen; if (ucv_type(obj) != UC_ARRAY) return NULL; arr = ucv_array_new(vm); for (arrlen = ucv_array_length(obj), arridx = 0; arridx < arrlen; arridx++) { uc_vm_ctx_push(vm); uc_vm_stack_push(vm, ucv_get(func)); uc_vm_stack_push(vm, ucv_get(ucv_array_get(obj, arridx))); uc_vm_stack_push(vm, ucv_int64_new(arridx)); uc_vm_stack_push(vm, ucv_get(obj)); if (uc_vm_call(vm, true, 3)) { ucv_put(arr); return NULL; } rv = uc_vm_stack_pop(vm); ucv_array_push(arr, rv); } return arr; } /** * Without further arguments, this function returns the byte value of the first * character in the given string. * * If an offset argument is supplied, the byte value of the character at this * position is returned. If an invalid index is supplied, the function will * return `null`. Negative index entries are counted towards the end of the * string, e.g. `-2` will return the value of the second last character. * * Returns the byte value of the character. * Returns `null` if the offset is invalid or if the input is not a string. * * @function module:core#ord * * @param {string} s * The input string. * * @param {number} [offset] * The offset of the character. * * @returns {?number} * * @example * ord("Abc"); // 65 * ord("Abc", 0); // 65 * ord("Abc", 1); // 98 * ord("Abc", 2); // 99 * ord("Abc", 10); // null * ord("Abc", -10); // null * ord("Abc", "nan"); // null */ static uc_value_t * uc_ord(uc_vm_t *vm, size_t nargs) { uc_value_t *obj = uc_fn_arg(0); const char *str; int64_t n = 0; size_t len; if (ucv_type(obj) != UC_STRING) return NULL; str = ucv_string_get(obj); len = ucv_string_length(obj); if (nargs > 1) { n = ucv_int64_get(uc_fn_arg(1)); if (errno == EINVAL) return NULL; if (n < 0) n += len; } if (n < 0 || (uint64_t)n >= len) return NULL; return ucv_int64_new((uint8_t)str[n]); } /** * Query the type of the given value. * * Returns the type of the given value as a string which might be one of * `"function"`, `"object"`, `"array"`, `"double"`, `"int"`, or `"bool"`. * * Returns `null` when no value or `null` is passed. * * @function module:core#type * * @param {*} x * The value to determine the type of. * * @returns {?string} */ static uc_value_t * uc_type(uc_vm_t *vm, size_t nargs) { uc_value_t *v = uc_fn_arg(0); uc_type_t t = ucv_type(v); switch (t) { case UC_CFUNCTION: case UC_CLOSURE: return ucv_string_new("function"); case UC_INTEGER: return ucv_string_new("int"); case UC_BOOLEAN: return ucv_string_new("bool"); case UC_NULL: return NULL; default: return ucv_string_new(ucv_typename(v)); } } /** * Reverse the order of the given input array or string. * * If an array is passed, returns the array in reverse order. * If a string is passed, returns the string with the sequence of the characters * reversed. * * Returns the reversed array or string. * Returns `null` if neither an array nor a string were passed. * * @function module:core#reverse * * @param {Array|string} arr_or_str * The input array or string. * * @returns {?(Array|string)} * * @example * reverse([1, 2, 3]); // [ 3, 2, 1 ] * reverse("Abc"); // "cbA" */ static uc_value_t * uc_reverse(uc_vm_t *vm, size_t nargs) { uc_value_t *obj = uc_fn_arg(0); uc_value_t *rv = NULL; size_t len, arridx; const char *str; char *dup, *p; if (ucv_type(obj) == UC_ARRAY) { if (!assert_mutable_array(vm, obj)) return NULL; rv = ucv_array_new(vm); for (arridx = ucv_array_length(obj); arridx > 0; arridx--) ucv_array_push(rv, ucv_get(ucv_array_get(obj, arridx - 1))); } else if (ucv_type(obj) == UC_STRING) { len = ucv_string_length(obj); str = ucv_string_get(obj); p = dup = xalloc(len + 1); while (len > 0) *p++ = str[--len]; rv = ucv_string_new(dup); free(dup); } return rv; } static struct { uc_vm_t *vm; bool ex; uc_value_t *fn; } sort_ctx; static int default_cmp(uc_value_t *v1, uc_value_t *v2) { char *s1, *s2; bool f1, f2; int res; /* when both operands are numeric then compare numerically */ if ((ucv_type(v1) == UC_INTEGER || ucv_type(v1) == UC_DOUBLE) && (ucv_type(v2) == UC_INTEGER || ucv_type(v2) == UC_DOUBLE)) { ucv_compare(0, v1, v2, &res); return res; } /* otherwise convert both operands to strings and compare lexically */ s1 = uc_cast_string(sort_ctx.vm, &v1, &f1); s2 = uc_cast_string(sort_ctx.vm, &v2, &f2); res = strcmp(s1, s2); if (f1) free(s1); if (f2) free(s2); return res; } static int array_sort_fn(const void *k1, const void *k2) { uc_value_t *rv, *null = ucv_int64_new(0); uc_value_t * const *v1 = k1; uc_value_t * const *v2 = k2; int res; if (!sort_ctx.fn) return default_cmp(*v1, *v2); if (sort_ctx.ex) return 0; uc_vm_ctx_push(sort_ctx.vm); uc_vm_stack_push(sort_ctx.vm, ucv_get(sort_ctx.fn)); uc_vm_stack_push(sort_ctx.vm, ucv_get(*v1)); uc_vm_stack_push(sort_ctx.vm, ucv_get(*v2)); if (uc_vm_call(sort_ctx.vm, true, 2)) { sort_ctx.ex = true; return 0; } rv = uc_vm_stack_pop(sort_ctx.vm); ucv_compare(0, rv, null, &res); ucv_put(null); ucv_put(rv); return res; } static int object_sort_fn(const void *k1, const void *k2) { uc_value_t *rv, *null = ucv_int64_new(0); struct lh_entry * const *e1 = k1; struct lh_entry * const *e2 = k2; int res; if (!sort_ctx.fn) return strcmp((char *)lh_entry_k(*e1), (char *)lh_entry_k(*e2)); if (sort_ctx.ex) return 0; uc_vm_ctx_push(sort_ctx.vm); uc_vm_stack_push(sort_ctx.vm, ucv_get(sort_ctx.fn)); uc_vm_stack_push(sort_ctx.vm, ucv_string_new((char *)lh_entry_k(*e1))); uc_vm_stack_push(sort_ctx.vm, ucv_string_new((char *)lh_entry_k(*e2))); uc_vm_stack_push(sort_ctx.vm, ucv_get((uc_value_t *)lh_entry_v(*e1))); uc_vm_stack_push(sort_ctx.vm, ucv_get((uc_value_t *)lh_entry_v(*e2))); if (uc_vm_call(sort_ctx.vm, true, 4)) { sort_ctx.ex = true; return 0; } rv = uc_vm_stack_pop(sort_ctx.vm); ucv_compare(0, rv, null, &res); ucv_put(null); ucv_put(rv); return res; } /** * Sort the given array according to the given sort function. * If no sort function is provided, a default ascending sort order is applied. * * The input array is sorted in-place, no copy is made. * * The custom sort function is repeatedly called until the entire array is * sorted. It will receive two values as arguments and should return a value * lower than, larger than or equal to zero depending on whether the first * argument is smaller, larger or equal to the second argument respectively. * * Returns the sorted input array. * * @function module:core#sort * * @param {Array} arr * The input array to be sorted. * * @param {Function} [fn] * The sort function. * * @returns {Array} * * @example * sort([8, 1, 5, 9]) // [1, 5, 8, 9] * sort(["Bean", "Orange", "Apple"], function(a, b) { * return length(a) - length(b); * }) // ["Bean", "Apple", "Orange"] */ static uc_value_t * uc_sort(uc_vm_t *vm, size_t nargs) { uc_value_t *val = uc_fn_arg(0); uc_value_t *fn = uc_fn_arg(1); if (!assert_mutable(vm, val)) return NULL; sort_ctx.vm = vm; sort_ctx.fn = fn; switch (ucv_type(val)) { case UC_ARRAY: ucv_array_sort(val, array_sort_fn); break; case UC_OBJECT: ucv_object_sort(val, object_sort_fn); break; default: return NULL; } return sort_ctx.ex ? NULL : ucv_get(val); } /** * Removes the elements designated by `off` and `len` from the given array, * and replaces them with the additional arguments passed, if any. * * The array grows or shrinks as necessary. * * Returns the modified input array. * * @function module:core#splice * * @param {Array} arr * The input array to be modified. * * @param {number} off * The index to start removing elements. * * @param {number} [len] * The number of elements to remove. * * @param {...*} [elements] * The elements to insert. * * @returns {*} * * @example * let x = [ 1, 2, 3, 4 ]; * splice(x, 1, 2, "a", "b", "c"); // [ 1, "a", "b", "c", 4 ] * print(x, "\n"); // [ 1, "a", "b", "c", 4 ] */ static uc_value_t * uc_splice(uc_vm_t *vm, size_t nargs) { uc_value_t *arr = uc_fn_arg(0); int64_t ofs = ucv_to_integer(uc_fn_arg(1)); int64_t remlen = ucv_to_integer(uc_fn_arg(2)); size_t arrlen, addlen, idx; if (!assert_mutable_array(vm, arr)) return NULL; arrlen = ucv_array_length(arr); addlen = nargs; if (addlen == 1) { ofs = 0; addlen = 0; remlen = arrlen; } else if (addlen == 2) { if (ofs < 0) { ofs = arrlen + ofs; if (ofs < 0) ofs = 0; } else if ((uint64_t)ofs > arrlen) { ofs = arrlen; } addlen = 0; remlen = arrlen - ofs; } else { if (ofs < 0) { ofs = arrlen + ofs; if (ofs < 0) ofs = 0; } else if ((uint64_t)ofs > arrlen) { ofs = arrlen; } if (remlen < 0) { remlen = arrlen - ofs + remlen; if (remlen < 0) remlen = 0; } else if ((uint64_t)remlen > arrlen - (uint64_t)ofs) { remlen = arrlen - ofs; } addlen -= 3; } if (addlen < (uint64_t)remlen) { ucv_array_delete(arr, ofs, remlen - addlen); } else if (addlen > (uint64_t)remlen) { for (idx = arrlen; idx > (uint64_t)ofs; idx--) ucv_array_set(arr, idx + addlen - remlen - 1, ucv_get(ucv_array_get(arr, idx - 1))); } for (idx = 0; idx < addlen; idx++) ucv_array_set(arr, ofs + idx, ucv_get(uc_fn_arg(3 + idx))); return ucv_get(arr); } /** * Performs a shallow copy of a portion of the source array, as specified by * the start and end offsets. The original array is not modified. * * Returns a new array containing the copied elements, if any. * Returns `null` if the given source argument is not an array value. * * @function module:core#slice * * @param {Array} arr * The source array to be copied. * * @param {number} [off] * The index of the first element to copy. * * @param {number} [end] * The index of the first element to exclude from the returned array. * * @returns {Array} * * @example * slice([1, 2, 3]) // [1, 2, 3] * slice([1, 2, 3], 1) // [2, 3] * slice([1, 2, 3], -1) // [3] * slice([1, 2, 3], -3, -1) // [1, 2] * slice([1, 2, 3], 10) // [] * slice([1, 2, 3], 2, 1) // [] * slice("invalid", 1, 2) // null */ static uc_value_t * uc_slice(uc_vm_t *vm, size_t nargs) { uc_value_t *arr = uc_fn_arg(0); uc_value_t *sv = uc_fn_arg(1); uc_value_t *ev = uc_fn_arg(2); uc_value_t *res = NULL; int64_t off, end; size_t len; if (ucv_type(arr) != UC_ARRAY) return NULL; len = ucv_array_length(arr); off = sv ? ucv_to_integer(sv) : 0; end = ev ? ucv_to_integer(ev) : (int64_t)len; if (off < 0) { off = len + off; if (off < 0) off = 0; } else if ((uint64_t)off > len) { off = len; } if (end < 0) { end = len + end; if (end < 0) end = 0; } else if ((uint64_t)end > len) { end = len; } res = ucv_array_new(vm); while (off < end) ucv_array_push(res, ucv_get(ucv_array_get(arr, off++))); return res; } /** * Split the given string using the separator passed as the second argument * and return an array containing the resulting pieces. * * If a limit argument is supplied, the resulting array contains no more than * the given amount of entries, that means the string is split at most * `limit - 1` times total. * * The separator may either be a plain string or a regular expression. * * Returns a new array containing the resulting pieces. * * @function module:core#split * * @param {string} str * The input string to be split. * * @param {string|RegExp} sep * The separator. * * @param {number} [limit] * The limit on the number of splits. * * @returns {Array} * * @example * split("foo,bar,baz", ",") // ["foo", "bar", "baz"] * split("foobar", "") // ["f", "o", "o", "b", "a", "r"] * split("foo,bar,baz", /[ao]/) // ["f", "", ",b", "r,b", "z"] * split("foo=bar=baz", "=", 2) // ["foo", "bar=baz"] */ static uc_value_t * uc_split(uc_vm_t *vm, size_t nargs) { uc_value_t *str = uc_fn_arg(0); uc_value_t *sep = uc_fn_arg(1); uc_value_t *lim = uc_fn_arg(2); uc_value_t *arr = NULL; const char *p, *sepstr, *splitstr; size_t seplen, splitlen, limit; int eflags = 0, res; regmatch_t pmatch; uc_regexp_t *re; if (!sep || ucv_type(str) != UC_STRING) return NULL; arr = ucv_array_new(vm); splitlen = ucv_string_length(str); p = splitstr = ucv_string_get(str); limit = lim ? ucv_uint64_get(lim) : SIZE_MAX; if (limit == 0) goto out; if (ucv_type(sep) == UC_REGEXP) { re = (uc_regexp_t *)sep; while (limit > 1) { res = regexec(&re->regexp, splitstr, 1, &pmatch, eflags); if (res == REG_NOMATCH) break; if (pmatch.rm_so != pmatch.rm_eo) { ucv_array_push(arr, ucv_string_new_length(splitstr, pmatch.rm_so)); splitstr += pmatch.rm_eo; } else if (*splitstr) { ucv_array_push(arr, ucv_string_new_length(splitstr, 1)); splitstr++; } else { goto out; } eflags |= REG_NOTBOL; limit--; } ucv_array_push(arr, ucv_string_new(splitstr)); } else if (ucv_type(sep) == UC_STRING) { sepstr = ucv_string_get(sep); seplen = ucv_string_length(sep); if (splitlen == 0) { ucv_array_push(arr, ucv_string_new_length("", 0)); } else if (seplen == 0) { while (limit > 1 && splitlen > 0) { ucv_array_push(arr, ucv_string_new_length(p, 1)); limit--; splitlen--; p++; } if (splitlen > 0) ucv_array_push(arr, ucv_string_new_length(p, splitlen)); } else { while (limit > 1 && splitlen >= seplen) { if (!memcmp(p, sepstr, seplen)) { ucv_array_push(arr, ucv_string_new_length(splitstr, p - splitstr)); p = splitstr = p + seplen; splitlen -= seplen; limit--; continue; } splitlen--; p++; } ucv_array_push(arr, ucv_string_new_length(splitstr, p - splitstr + splitlen)); } } else { ucv_put(arr); return NULL; } out: return arr; } /** * Extracts a substring out of `str` and returns it. First character is at * offset zero. * * - If `off` is negative, starts that far back from the end of the string. * - If `len` is omitted, returns everything through the end of the string. * - If `len` is negative, leaves that many characters off the string end. * * Returns the extracted substring. * * @function module:core#substr * * @param {string} str * The input string. * * @param {number} off * The starting offset. * * @param {number} [len] * The length of the substring. * * @returns {string} * * @example * s = "The black cat climbed the green tree"; * substr(s, 4, 5); // black * substr(s, 4, -11); // black cat climbed the * substr(s, 14); // climbed the green tree * substr(s, -4); // tree * substr(s, -4, 2); // tr */ static uc_value_t * uc_substr(uc_vm_t *vm, size_t nargs) { uc_value_t *str = uc_fn_arg(0); int64_t ofs = ucv_to_integer(uc_fn_arg(1)); int64_t sublen = ucv_to_integer(uc_fn_arg(2)); const char *p; size_t len; if (ucv_type(str) != UC_STRING) return NULL; p = ucv_string_get(str); len = ucv_string_length(str); switch (nargs) { case 1: ofs = 0; sublen = len; break; case 2: if (ofs < 0) { ofs = len + ofs; if (ofs < 0) ofs = 0; } else if ((uint64_t)ofs > len) { ofs = len; } sublen = len - ofs; break; default: if (ofs < 0) { ofs = len + ofs; if (ofs < 0) ofs = 0; } else if ((uint64_t)ofs > len) { ofs = len; } if (sublen < 0) { sublen = len - ofs + sublen; if (sublen < 0) sublen = 0; } else if ((uint64_t)sublen > len - (uint64_t)ofs) { sublen = len - ofs; } break; } return ucv_string_new_length(p + ofs, sublen); } /** * Returns the current UNIX epoch. * * @function module:core#time * * @returns {number} * * @example * time(); // 1598043054 */ static uc_value_t * uc_time(uc_vm_t *vm, size_t nargs) { time_t t = time(NULL); return ucv_int64_new((int64_t)t); } /** * Converts the given string to uppercase and returns the resulting string. * * Returns null if the given argument could not be converted to a string. * * @function module:core#uc * * @param {*} str * The string to be converted to uppercase. * * @returns {?string} * * @example * uc("hello"); // "HELLO" * uc(123); // null */ static uc_value_t * uc_uc(uc_vm_t *vm, size_t nargs) { char *str = ucv_to_string(vm, uc_fn_arg(0)); uc_value_t *rv = NULL; char *p; if (!str) return NULL; for (p = str; *p; p++) if (*p >= 'a' && *p <= 'z') *p &= ~32; rv = ucv_string_new(str); free(str); return rv; } /** * Converts each given numeric value to an UTF-8 multibyte sequence and returns * the resulting string. * * Invalid numeric values or values outside the range `0`..`0x10FFFF` are * represented by the unicode replacement character `0xFFFD`. * * Returns a new UTF-8 encoded string consisting of unicode characters * corresponding to the given numeric codepoints. * * @function module:core#uchr * * @param {...number} * Numeric values to convert. * * @returns {string} * * @example * uchr(0x2600, 0x26C6, 0x2601); // "☀⛆☁" * uchr(-1, 0x20ffff, "foo"); // "���" */ static uc_value_t * uc_uchr(uc_vm_t *vm, size_t nargs) { uc_value_t *rv = NULL; size_t idx, ulen; char *p, *str; int64_t n; int rem; for (idx = 0, ulen = 0; idx < nargs; idx++) { n = ucv_to_integer(uc_fn_arg(idx)); if (errno == EINVAL || errno == ERANGE || n < 0 || n > 0x10FFFF) ulen += 3; else if (n <= 0x7F) ulen++; else if (n <= 0x7FF) ulen += 2; else if (n <= 0xFFFF) ulen += 3; else ulen += 4; } str = xalloc(ulen); for (idx = 0, p = str, rem = ulen; idx < nargs; idx++) { n = ucv_to_integer(uc_fn_arg(idx)); if (errno == EINVAL || errno == ERANGE || n < 0 || n > 0x10FFFF) n = 0xFFFD; if (!utf8enc(&p, &rem, n)) break; } rv = ucv_string_new_length(str, ulen); free(str); return rv; } /** * Returns an array containing all values of the given object. * * Returns null if no object was passed. * * @function module:core#values * * @param {*} obj * The object from which to extract values. * * @returns {?Array} * * @example * values({ foo: true, bar: false }); // [true, false] */ static uc_value_t * uc_values(uc_vm_t *vm, size_t nargs) { uc_value_t *obj = uc_fn_arg(0); uc_value_t *arr; if (ucv_type(obj) != UC_OBJECT) return NULL; arr = ucv_array_new(vm); ucv_object_foreach(obj, key, val) { (void)key; ucv_array_push(arr, ucv_get(val)); } return arr; } static uc_value_t * uc_trim_common(uc_vm_t *vm, size_t nargs, bool start, bool end) { uc_value_t *str = uc_fn_arg(0); uc_value_t *chr = uc_fn_arg(1); const char *p, *c; size_t len; if (ucv_type(str) != UC_STRING || (chr != NULL && ucv_type(chr) != UC_STRING)) return NULL; c = ucv_string_get(chr); c = c ? c : " \t\r\n"; p = ucv_string_get(str); len = ucv_string_length(str); if (start) { while (*p) { if (!strchr(c, *p)) break; p++; len--; } } if (end) { while (len > 0) { if (!strchr(c, p[len - 1])) break; len--; } } return ucv_string_new_length(p, len); } /** * Trim any of the specified characters in `c` from the start and end of `str`. * If the second argument is omitted, trims the characters, ` ` (space), `\t`, * `\r`, and `\n`. * * Returns the trimmed string. * * @function module:core#trim * * @param {string} str * The string to be trimmed. * * @param {string} [c] * The characters to be trimmed from the start and end of the string. * * @returns {string} */ static uc_value_t * uc_trim(uc_vm_t *vm, size_t nargs) { return uc_trim_common(vm, nargs, true, true); } /** * Trim any of the specified characters from the start of the string. * If the second argument is omitted, trims the characters ` ` (space), '\t', * '\r', and '\n'. * * Returns the left trimmed string. * * @function module:core#ltrim * * @param {string} s * The input string. * * @param {string} [c] * The characters to trim. * * @returns {string} * * @example * ltrim(" foo \n") // "foo \n" * ltrim("--bar--", "-") // "bar--" */ static uc_value_t * uc_ltrim(uc_vm_t *vm, size_t nargs) { return uc_trim_common(vm, nargs, true, false); } /** * Trim any of the specified characters from the end of the string. * If the second argument is omitted, trims the characters ` ` (space), '\t', * '\r', and '\n'. * * Returns the right trimmed string. * * @function module:core#rtrim * * @param {string} str * The input string. * * @param {string} [c] * The characters to trim. * * @returns {string} * * @example * rtrim(" foo \n") // " foo" * rtrim("--bar--", "-") // "--bar" */ static uc_value_t * uc_rtrim(uc_vm_t *vm, size_t nargs) { return uc_trim_common(vm, nargs, false, true); } enum { FMT_F_ALT = (1 << 0), FMT_F_ZERO = (1 << 1), FMT_F_LEFT = (1 << 2), FMT_F_SPACE = (1 << 3), FMT_F_SIGN = (1 << 4), FMT_F_WIDTH = (1 << 5), FMT_F_PREC = (1 << 6), }; enum { FMT_C_NONE = (1 << 0), FMT_C_INT = (1 << 1), FMT_C_UINT = (1 << 2), FMT_C_DBL = (1 << 3), FMT_C_CHR = (1 << 4), FMT_C_STR = (1 << 5), FMT_C_JSON = (1 << 6), }; static void uc_printf_common(uc_vm_t *vm, size_t nargs, uc_stringbuf_t *buf) { char *s, sfmt[sizeof("%#0- +0123456789.0123456789%")]; uint32_t conv, flags, width, precision; uc_value_t *fmt = uc_fn_arg(0), *arg; const char *fstr, *last, *p, *cfmt; size_t argidx = 1, argpos, sfmtlen; uint64_t u; int64_t n; double d; if (ucv_type(fmt) == UC_STRING) fstr = ucv_string_get(fmt); else fstr = ""; for (last = p = fstr; *p; p++) { if (*p == '%') { ucv_stringbuf_addstr(buf, last, p - last); last = p++; flags = 0; width = 0; precision = 0; argpos = argidx; if (*p >= '1' && *p <= '9') { while (isdigit(*p)) width = width * 10 + (*p++ - '0'); /* if a dollar sign follows, this is an argument index */ if (*p == '$') { argpos = width; width = 0; p++; } /* otherwise skip to parsing precision, flags can't possibly follow */ else { flags |= FMT_F_WIDTH; goto parse_precision; } } while (*p != '\0' && strchr("#0- +", *p)) { switch (*p++) { case '#': flags |= FMT_F_ALT; break; case '0': flags |= FMT_F_ZERO; break; case '-': flags |= FMT_F_LEFT; break; case ' ': flags |= FMT_F_SPACE; break; case '+': flags |= FMT_F_SIGN; break; } } if (*p >= '1' && *p <= '9') { while (isdigit(*p)) width = width * 10 + (*p++ - '0'); flags |= FMT_F_WIDTH; } parse_precision: if (*p == '.') { p++; if (*p == '-') { p++; while (isdigit(*p)) p++; } else { while (isdigit(*p)) precision = precision * 10 + (*p++ - '0'); } flags |= FMT_F_PREC; } switch (*p) { case 'd': case 'i': conv = FMT_C_INT; flags &= ~FMT_F_PREC; cfmt = PRId64; break; case 'o': conv = FMT_C_UINT; flags &= ~FMT_F_PREC; cfmt = PRIo64; break; case 'u': conv = FMT_C_UINT; flags &= ~FMT_F_PREC; cfmt = PRIu64; break; case 'x': conv = FMT_C_UINT; flags &= ~FMT_F_PREC; cfmt = PRIx64; break; case 'X': conv = FMT_C_UINT; flags &= ~FMT_F_PREC; cfmt = PRIX64; break; case 'e': conv = FMT_C_DBL; cfmt = "e"; break; case 'E': conv = FMT_C_DBL; cfmt = "E"; break; case 'f': conv = FMT_C_DBL; cfmt = "f"; break; case 'F': conv = FMT_C_DBL; cfmt = "F"; break; case 'g': conv = FMT_C_DBL; cfmt = "g"; break; case 'G': conv = FMT_C_DBL; cfmt = "G"; break; case 'c': conv = FMT_C_CHR; flags &= ~FMT_F_PREC; cfmt = "c"; break; case 's': conv = FMT_C_STR; flags &= ~FMT_F_ZERO; cfmt = "s"; break; case 'J': conv = FMT_C_JSON; if (flags & FMT_F_PREC) { flags &= ~FMT_F_PREC; precision++; } cfmt = "s"; break; case '%': conv = FMT_C_NONE; flags = 0; cfmt = "%"; break; case '\0': p--; /* fall through */ default: continue; } sfmtlen = 0; sfmt[sfmtlen++] = '%'; if (flags & FMT_F_ALT) sfmt[sfmtlen++] = '#'; if (flags & FMT_F_ZERO) sfmt[sfmtlen++] = '0'; if (flags & FMT_F_LEFT) sfmt[sfmtlen++] = '-'; if (flags & FMT_F_SPACE) sfmt[sfmtlen++] = ' '; if (flags & FMT_F_SIGN) sfmt[sfmtlen++] = '+'; if (flags & FMT_F_WIDTH) sfmtlen += snprintf(&sfmt[sfmtlen], sizeof(sfmt) - sfmtlen, "%" PRIu32, width); if (flags & FMT_F_PREC) sfmtlen += snprintf(&sfmt[sfmtlen], sizeof(sfmt) - sfmtlen, ".%" PRIu32, precision); snprintf(&sfmt[sfmtlen], sizeof(sfmt) - sfmtlen, "%s", cfmt); switch (conv) { case FMT_C_NONE: ucv_stringbuf_addstr(buf, cfmt, strlen(cfmt)); break; case FMT_C_INT: argidx++; arg = uc_fn_arg(argpos); n = ucv_to_integer(arg); if (errno == ERANGE) n = (int64_t)ucv_to_unsigned(arg); ucv_stringbuf_printf(buf, sfmt, n); break; case FMT_C_UINT: argidx++; arg = uc_fn_arg(argpos); u = ucv_to_unsigned(arg); if (errno == ERANGE) u = (uint64_t)ucv_to_integer(arg); ucv_stringbuf_printf(buf, sfmt, u); break; case FMT_C_DBL: argidx++; d = ucv_to_double(uc_fn_arg(argpos)); ucv_stringbuf_printf(buf, sfmt, d); break; case FMT_C_CHR: argidx++; n = ucv_to_integer(uc_fn_arg(argpos)); ucv_stringbuf_printf(buf, sfmt, (int)n); break; case FMT_C_STR: argidx++; arg = uc_fn_arg(argpos); switch (ucv_type(arg)) { case UC_STRING: ucv_stringbuf_printf(buf, sfmt, ucv_string_get(arg)); break; case UC_NULL: ucv_stringbuf_append(buf, "(null)"); break; default: s = ucv_to_string(vm, arg); ucv_stringbuf_printf(buf, sfmt, s ? s : "(null)"); free(s); } break; case FMT_C_JSON: argidx++; s = ucv_to_jsonstring_formatted(vm, uc_fn_arg(argpos), precision > 0 ? (precision > 1 ? ' ' : '\t') : '\0', precision > 0 ? (precision > 1 ? precision - 1 : 1) : 0); ucv_stringbuf_printf(buf, sfmt, s ? s : "null"); free(s); break; } last = p + 1; } } ucv_stringbuf_addstr(buf, last, p - last); } /** * Formats the given arguments according to the given format string. * * See `printf()` for details. * * Returns the formatted string. * * @function module:core#sprintf * * @param {string} fmt * The format string. * * @param {...*} * Arguments to be formatted. * * @returns {string} * * @example * sprintf("Hello %s", "world"); // "Hello world" * sprintf("%08x", 123); // "0000007b" * sprintf("%c%c%c", 65, 98, 99); // "Abc" * sprintf("%g", 10 / 3.0); // "3.33333" * sprintf("%2$d %1$d", 12, 34); // "34 12" * sprintf("%J", [1,2,3]); // "[1,2,3]" */ static uc_value_t * uc_sprintf(uc_vm_t *vm, size_t nargs) { uc_stringbuf_t *buf = ucv_stringbuf_new(); uc_printf_common(vm, nargs, buf); return ucv_stringbuf_finish(buf); } /** * Formats the given arguments according to the given format string and outputs * the result to stdout. * * Ucode supports a restricted subset of the formats allowed by the underlying * libc's `printf()` implementation, namely it allows the `d`, `i`, `o`, `u`, * `x`, `X`, `e`, `E`, `f`, `F`, `g`, `G`, `c` and `s` conversions. * * Additionally, an ucode specific `J` format is implemented, which causes the * corresponding value to be formatted as JSON string. By prefixing the `J` * format letter with a precision specifier, the resulting JSON output will be * pretty printed. A precision of `0` will use tabs for indentation, any other * positive precision will use that many spaces for indentation while a negative * or omitted precision specifier will turn off pretty printing. * * Other format specifiers such as `n` or `z` are not accepted and returned * verbatim. Format specifiers including `*` directives are rejected as well. * * Returns the number of bytes written to the standard output. * * @function module:core#printf * * @param {string} fmt * The format string. * * @param {...*} * Arguments to be formatted. * * @returns {number} * * @example * {% * printf("Hello %s\n", "world"); // Hello world * printf("%08x\n", 123); // 0000007b * printf("%c%c%c\n", 65, 98, 99); // Abc * printf("%g\n", 10 / 3.0); // 3.33333 * printf("%2$d %1$d\n", 12, 34); // 34 12 * printf("%J", [1,2,3]); // [ 1, 2, 3 ] * * printf("%.J", [1,2,3]); * // [ * // 1, * // 2, * // 3 * // ] * * printf("%.2J", [1,2,3]); * // [ * // 1, * // 2, * // 3 * // ] * %} */ static uc_value_t * uc_printf(uc_vm_t *vm, size_t nargs) { uc_stringbuf_t *buf = xprintbuf_new(); size_t len; uc_printf_common(vm, nargs, buf); len = fwrite(buf->buf, 1, printbuf_length(buf), vm->output); printbuf_free(buf); return ucv_int64_new(len); } static bool uc_require_so(uc_vm_t *vm, const char *path, uc_value_t **res) { void (*init)(uc_vm_t *, uc_value_t *); uc_value_t *scope; struct stat st; void *dlh; if (stat(path, &st)) return false; dlerror(); dlh = dlopen(path, RTLD_LAZY|RTLD_LOCAL); if (!dlh) { uc_vm_raise_exception(vm, EXCEPTION_RUNTIME, "Unable to dlopen file '%s': %s", path, dlerror()); return true; } *(void **)(&init) = dlsym(dlh, "uc_module_entry"); if (!init) { uc_vm_raise_exception(vm, EXCEPTION_RUNTIME, "Module '%s' provides no 'uc_module_entry' function", path); return true; } scope = ucv_object_new(vm); init(vm, scope); *res = scope; return true; } static uc_value_t * uc_loadfile(uc_vm_t *vm, size_t nargs); static uc_value_t * uc_callfunc(uc_vm_t *vm, size_t nargs); static bool uc_require_ucode(uc_vm_t *vm, const char *path, uc_value_t *scope, uc_value_t **res, bool raw_mode) { uc_parse_config_t config = *vm->config, *prev_config = vm->config; uc_value_t *closure; struct stat st; if (stat(path, &st)) return false; config.raw_mode = raw_mode; vm->config = &config; uc_vm_stack_push(vm, ucv_string_new(path)); closure = uc_loadfile(vm, 1); ucv_put(uc_vm_stack_pop(vm)); if (closure) { uc_vm_stack_push(vm, closure); uc_vm_stack_push(vm, NULL); uc_vm_stack_push(vm, scope); *res = uc_callfunc(vm, 3); uc_vm_stack_pop(vm); uc_vm_stack_pop(vm); uc_vm_stack_pop(vm); } vm->config = prev_config; return true; } static bool uc_require_path(uc_vm_t *vm, const char *path_template, const char *name, uc_value_t **res, bool so_only) { uc_stringbuf_t *buf = xprintbuf_new(); const char *p, *q, *last; uc_value_t *modtable; bool rv; modtable = ucv_property_get(uc_vm_scope_get(vm), "modules"); *res = ucv_get(ucv_object_get(modtable, name, &rv)); if (rv) goto out; p = strchr(path_template, '*'); if (!p) goto out; ucv_stringbuf_addstr(buf, path_template, p - path_template); for (q = last = name;; q++) { if (*q == '.' || *q == '\0') { ucv_stringbuf_addstr(buf, last, q - last); if (*q) ucv_stringbuf_append(buf, "/"); else ucv_stringbuf_addstr(buf, p + 1, strlen(p + 1)); if (*q == '\0') break; last = q + 1; } else if (!isalnum(*q) && *q != '_') { goto out; } } if (!strcmp(p + 1, ".so")) rv = uc_require_so(vm, buf->buf, res); else if (!strcmp(p + 1, ".uc") && !so_only) rv = uc_require_ucode(vm, buf->buf, NULL, res, true); if (rv) ucv_object_add(modtable, name, ucv_get(*res)); out: printbuf_free(buf); return rv; } uc_value_t * uc_require_library(uc_vm_t *vm, uc_value_t *nameval, bool so_only) { uc_value_t *search, *se, *res; size_t arridx, arrlen; const char *name; if (ucv_type(nameval) != UC_STRING) return NULL; name = ucv_string_get(nameval); search = ucv_property_get(uc_vm_scope_get(vm), "REQUIRE_SEARCH_PATH"); if (ucv_type(search) != UC_ARRAY) { uc_vm_raise_exception(vm, EXCEPTION_RUNTIME, "Global require search path not set"); return NULL; } for (arridx = 0, arrlen = ucv_array_length(search); arridx < arrlen; arridx++) { se = ucv_array_get(search, arridx); if (ucv_type(se) != UC_STRING) continue; if (uc_require_path(vm, ucv_string_get(se), name, &res, so_only)) return res; } uc_vm_raise_exception(vm, EXCEPTION_RUNTIME, "No module named '%s' could be found", name); return NULL; } /** * Load and evaluate ucode scripts or shared library extensions. * * The `require()` function expands each member of the global * `REQUIRE_SEARCH_PATH` array to a filesystem path by replacing the `*` * placeholder with a slash-separated version of the given dotted module name * and subsequently tries to load a file at the resulting location. * * If a file is found at one of the search path locations, it is compiled and * evaluated or loaded via the C runtime's `dlopen()` function, depending on * whether the found file is a ucode script or a compiled dynamic library. * * The resulting program function of the compiled/loaded module is then * subsequently executed with the current global environment, without a `this` * context and without arguments. * * Finally, the return value of the invoked program function is returned back * by `require()` to the caller. * * By default, modules are cached in the global `modules` dictionary and * subsequent attempts to require the same module will return the cached module * dictionary entry without re-evaluating the module. * * To force reloading a module, the corresponding entry from the global * `modules` dictionary can be deleted. * * To preload a module or to provide a "virtual" module without a corresponding * filesystem resource, an entry can be manually added to the global `modules` * dictionary. * * Summarized, the `require()` function can be roughly described by the * following code: * * ``` * function require(name) { * if (exists(modules, name)) * return modules[name]; * * for (const item in REQUIRE_SEARCH_PATH) { * const modpath = replace(item, '*', replace(name, '.', '/')); * const entryfunc = loadfile(modpath, { raw_mode: true }); * * if (entryfunc) { * const modval = entryfunc(); * modules[name] = modval; * * return modval; * } * } * * die(`Module ${name} not found`); * } * ``` * * Due to the fact that `require()` is a runtime operation, module source code * is only lazily evaluated/loaded upon invoking the first require invocation, * which might lead to situations where errors in module sources are only * reported much later throughout the program execution. Unless runtime loading * of modules is absolutely required, e.g. to conditionally load extensions, the * compile time * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#named_import|`import` syntax} * should be preferred. * * Returns the module value (typically an object) on success. * * Throws an exception if the module function threw an exception. * * Throws an exception if no matching module could be found, if the module * contains syntax errors or upon other I/O related problems. * * @function module:core#require * * @param {string} name * The name of the module to require in dotted notation. * * @returns {*} * * @example * // Require the `example/acme.uc` or `example/acme.so` module * const acme = require('example.acme'); * * // Requiring the same name again will yield the cached instance * const acme2 = require('example.acme'); * assert(acme === acme2); * * // Deleting the module dictionary entry will force a reload * delete modules['example.acme']; * const acme3 = require('example.acme'); * assert(acme !== acme3); * * // Preloading a "virtual" module * modules['example.test'] = { * hello: function() { print("This is the example module\n"); } * }; * * const test = require('example.test'); * test.hello(); // will print "This is the example module" */ static uc_value_t * uc_require(uc_vm_t *vm, size_t nargs) { return uc_require_library(vm, uc_fn_arg(0), false); } /** * Convert the given IP address string to an array of byte values. * * IPv4 addresses result in arrays of 4 integers while IPv6 ones in arrays * containing 16 integers. The resulting array can be turned back into IP * address strings using the inverse `arrtoip()` function. * * Returns an array containing the address byte values. * Returns `null` if the given argument is not a string or an invalid IP. * * @function module:core#iptoarr * * @param {string} address * The IP address string to convert. * * @returns {?number[]} * * @example * iptoarr("192.168.1.1") // [ 192, 168, 1, 1 ] * iptoarr("fe80::fc54:ff:fe82:abbd") // [ 254, 128, 0, 0, 0, 0, 0, 0, 252, 84, * // 0, 255, 254, 130, 171, 189 ]) * iptoarr("foo") // null (invalid address) * iptoarr(123) // null (not a string) */ static uc_value_t * uc_iptoarr(uc_vm_t *vm, size_t nargs) { uc_value_t *ip = uc_fn_arg(0); uc_value_t *res; union { uint8_t u8[4]; struct in_addr in; struct in6_addr in6; } a; int i; if (ucv_type(ip) != UC_STRING) return NULL; if (inet_pton(AF_INET6, ucv_string_get(ip), &a)) { res = ucv_array_new(vm); for (i = 0; i < 16; i++) ucv_array_push(res, ucv_int64_new(a.in6.s6_addr[i])); return res; } else if (inet_pton(AF_INET, ucv_string_get(ip), &a)) { res = ucv_array_new(vm); ucv_array_push(res, ucv_int64_new(a.u8[0])); ucv_array_push(res, ucv_int64_new(a.u8[1])); ucv_array_push(res, ucv_int64_new(a.u8[2])); ucv_array_push(res, ucv_int64_new(a.u8[3])); return res; } return NULL; } static int check_byte(uc_value_t *v) { int n; if (ucv_type(v) != UC_INTEGER) return -1; n = ucv_int64_get(v); if (n < 0 || n > 255) return -1; return n; } /** * Convert the given input array of byte values to an IP address string. * * Input arrays of length 4 are converted to IPv4 addresses, arrays of length 16 * to IPv6 ones. All other lengths are rejected. If any array element is not an * integer or exceeds the range 0..255 (inclusive), the array is rejected. * * Returns a string containing the formatted IP address. * Returns `null` if the input array was invalid. * * @function module:core#arrtoip * * @param {number[]} arr * The byte array to convert into an IP address string. * * @returns {?string} * * @example * arrtoip([ 192, 168, 1, 1 ]) // "192.168.1.1" * arrtoip([ 254, 128, 0, 0, 0, 0, 0, 0, 252, 84, 0, 255, 254, 130, 171, 189 ]) * // "fe80::fc54:ff:fe82:abbd" * arrtoip([ 1, 2, 3]) // null (invalid length) * arrtoip([ 1, "2", -5, 300 ]) // null (invalid values) * arrtoip("123") // null (not an array) */ static uc_value_t * uc_arrtoip(uc_vm_t *vm, size_t nargs) { uc_value_t *arr = uc_fn_arg(0); union { uint8_t u8[4]; struct in6_addr in6; } a; char buf[INET6_ADDRSTRLEN]; int i, n; if (ucv_type(arr) != UC_ARRAY) return NULL; switch (ucv_array_length(arr)) { case 4: for (i = 0; i < 4; i++) { n = check_byte(ucv_array_get(arr, i)); if (n < 0) return NULL; a.u8[i] = n; } inet_ntop(AF_INET, &a, buf, sizeof(buf)); return ucv_string_new(buf); case 16: for (i = 0; i < 16; i++) { n = check_byte(ucv_array_get(arr, i)); if (n < 0) return NULL; a.in6.s6_addr[i] = n; } inet_ntop(AF_INET6, &a, buf, sizeof(buf)); return ucv_string_new(buf); default: return NULL; } } /** * Match the given string against the regular expression pattern specified as * the second argument. * * If the passed regular expression uses the `g` flag, the return value will be * an array of arrays describing all found occurrences within the string. * * Without the `g` modifier, an array describing the first match is returned. * * Returns `null` if the pattern was not found within the given string. * * @function module:core#match * * @param {string} str * The string to be matched against the pattern. * * @param {RegExp} pattern * The regular expression pattern. * * @returns {?Array} * * @example * match("foobarbaz", /b.(.)/) // ["bar", "r"] * match("foobarbaz", /b.(.)/g) // [["bar", "r"], ["baz", "z"]] */ static uc_value_t * uc_match(uc_vm_t *vm, size_t nargs) { uc_value_t *subject = uc_fn_arg(0); uc_value_t *pattern = uc_fn_arg(1); uc_value_t *rv = NULL, *m; regmatch_t *pmatch = NULL; int eflags = 0, res; uc_regexp_t *re; bool freeable; char *p; size_t i; if (ucv_type(pattern) != UC_REGEXP || !subject) return NULL; re = (uc_regexp_t *)pattern; pmatch = calloc(1 + re->regexp.re_nsub, sizeof(regmatch_t)); if (!pmatch) return NULL; p = uc_cast_string(vm, &subject, &freeable); while (true) { res = regexec(&re->regexp, p, 1 + re->regexp.re_nsub, pmatch, eflags); if (res == REG_NOMATCH) break; m = ucv_array_new(vm); for (i = 0; i < 1 + re->regexp.re_nsub; i++) { if (pmatch[i].rm_so != -1) ucv_array_push(m, ucv_string_new_length(p + pmatch[i].rm_so, pmatch[i].rm_eo - pmatch[i].rm_so)); else ucv_array_push(m, NULL); } if (re->global) { if (!rv) rv = ucv_array_new(vm); ucv_array_push(rv, m); if (pmatch[0].rm_so != pmatch[0].rm_eo) p += pmatch[0].rm_eo; else if (*p) p++; else break; eflags |= REG_NOTBOL; } else { rv = m; break; } } free(pmatch); if (freeable) free(p); return rv; } static void uc_replace_cb(uc_vm_t *vm, uc_value_t *func, const char *subject, regmatch_t *pmatch, size_t plen, uc_stringbuf_t *resbuf) { uc_value_t *rv; size_t i; uc_vm_ctx_push(vm); uc_vm_stack_push(vm, ucv_get(func)); for (i = 0; i < plen; i++) { if (pmatch[i].rm_so != -1) uc_vm_stack_push(vm, ucv_string_new_length(subject + pmatch[i].rm_so, pmatch[i].rm_eo - pmatch[i].rm_so)); else uc_vm_stack_push(vm, NULL); } if (uc_vm_call(vm, true, i) == EXCEPTION_NONE) { rv = uc_vm_stack_pop(vm); ucv_to_stringbuf(vm, resbuf, rv, false); ucv_put(rv); } } static void uc_replace_str(uc_vm_t *vm, uc_value_t *str, const char *subject, regmatch_t *pmatch, size_t plen, uc_stringbuf_t *resbuf) { bool esc = false; char *p, *r; uint8_t i; for (p = r = ucv_to_string(vm, str); *p; p++) { if (esc) { switch (*p) { case '&': if (pmatch[0].rm_so != -1) ucv_stringbuf_addstr(resbuf, subject + pmatch[0].rm_so, pmatch[0].rm_eo - pmatch[0].rm_so); break; case '`': if (pmatch[0].rm_so != -1) ucv_stringbuf_addstr(resbuf, subject, pmatch[0].rm_so); break; case '\'': if (pmatch[0].rm_so != -1) ucv_stringbuf_addstr(resbuf, subject + pmatch[0].rm_eo, strlen(subject + pmatch[0].rm_eo)); break; case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': i = *p - '0'; if (i < plen && pmatch[i].rm_so != -1) { ucv_stringbuf_addstr(resbuf, subject + pmatch[i].rm_so, pmatch[i].rm_eo - pmatch[i].rm_so); } else { ucv_stringbuf_append(resbuf, "$"); ucv_stringbuf_addstr(resbuf, p, 1); } break; case '$': ucv_stringbuf_append(resbuf, "$"); break; default: ucv_stringbuf_append(resbuf, "$"); ucv_stringbuf_addstr(resbuf, p, 1); } esc = false; } else if (*p == '$') { esc = true; } else { ucv_stringbuf_addstr(resbuf, p, 1); } } free(r); } /** * Replace occurrences of the specified pattern in the string passed as the * first argument. * * - The pattern value may be either a regular expression or a plain string. * - The replace value may be a function which is invoked for each found pattern * or any other value which is converted into a plain string and used as * replacement. * - When an optional limit is specified, substitutions are performed only that * many times. * - If the pattern is a regular expression and not using the `g` flag, then * only the first occurrence in the string is replaced. * - If the `g` flag is used or if the pattern is not a regular expression, all * occurrences are replaced. * - If the replace value is a callback function, it is invoked with the found * substring as the first and any capture group values as subsequent * parameters. * - If the replace value is a string, specific substrings are substituted * before it is inserted into the result. * * Returns a new string with the pattern replaced. * * @function module:core#replace * * @param {string} str * The string in which to replace occurrences. * * @param {RegExp|string} pattern * The pattern to be replaced. * * @param {Function|string} replace * The replacement value. * * @param {number} [limit] * The optional limit of substitutions. * * @returns {string} * * @example * replace("barfoobaz", /(f)(o+)/g, "[$$|$`|$&|$'|$1|$2|$3]") // bar[$|bar|foo|baz|f|oo|$3]baz * replace("barfoobaz", /(f)(o+)/g, uc) // barFOObaz * replace("barfoobaz", "a", "X") // bXrfoobXz * replace("barfoobaz", /(.)(.)(.)/g, function(m, c1, c2, c3) { * return c3 + c2 + c1; * }) // raboofzab * replace("aaaaa", "a", "x", 3) // xxxaa * replace("foo bar baz", /[ao]/g, "x", 3) // fxx bxr baz */ static uc_value_t * uc_replace(uc_vm_t *vm, size_t nargs) { char *sb = NULL, *pt = NULL, *p, *l; uc_value_t *subject = uc_fn_arg(0); uc_value_t *pattern = uc_fn_arg(1); uc_value_t *replace = uc_fn_arg(2); uc_value_t *limitval = uc_fn_arg(3); bool sb_freeable, pt_freeable; regmatch_t *pmatch = NULL; size_t pl, nmatch, limit; uc_regexp_t *re = NULL; uc_stringbuf_t *resbuf; int eflags = 0, res; if (!pattern || !subject || !replace) return NULL; nmatch = 1; if (ucv_type(pattern) == UC_REGEXP) { re = (uc_regexp_t *)pattern; nmatch += re->regexp.re_nsub; } pmatch = calloc(nmatch, sizeof(regmatch_t)); if (!pmatch) return NULL; sb = uc_cast_string(vm, &subject, &sb_freeable); resbuf = ucv_stringbuf_new(); limit = limitval ? ucv_uint64_get(limitval) : SIZE_MAX; if (re) { p = sb; while (limit > 0) { res = regexec(&re->regexp, p, nmatch, pmatch, eflags); if (res == REG_NOMATCH) break; ucv_stringbuf_addstr(resbuf, p, pmatch[0].rm_so); if (ucv_is_callable(replace)) uc_replace_cb(vm, replace, p, pmatch, nmatch, resbuf); else uc_replace_str(vm, replace, p, pmatch, nmatch, resbuf); if (pmatch[0].rm_so != pmatch[0].rm_eo) p += pmatch[0].rm_eo; else if (*p) ucv_stringbuf_addstr(resbuf, p++, 1); else break; if (re->global) eflags |= REG_NOTBOL; else break; limit--; } ucv_stringbuf_addstr(resbuf, p, strlen(p)); } else { pt = uc_cast_string(vm, &pattern, &pt_freeable); pl = strlen(pt); l = p = sb; while (limit > 0) { if (pl == 0 || !strncmp(p, pt, pl)) { ucv_stringbuf_addstr(resbuf, l, p - l); pmatch[0].rm_so = p - l; pmatch[0].rm_eo = pmatch[0].rm_so + pl; if (ucv_is_callable(replace)) uc_replace_cb(vm, replace, l, pmatch, 1, resbuf); else uc_replace_str(vm, replace, l, pmatch, 1, resbuf); if (pl) { l = p + pl; p += pl - 1; } else { l = p; } limit--; } if (!*p++) break; } ucv_stringbuf_addstr(resbuf, l, strlen(l)); if (pt_freeable) free(pt); } free(pmatch); if (sb_freeable) free(sb); return ucv_stringbuf_finish(resbuf); } static struct json_tokener * uc_json_from_object(uc_vm_t *vm, uc_value_t *obj, json_object **jso) { bool trail = false, eof = false; enum json_tokener_error err; struct json_tokener *tok; uc_value_t *rfn, *rbuf; uc_stringbuf_t *buf; rfn = ucv_property_get(obj, "read"); if (!ucv_is_callable(rfn)) { uc_vm_raise_exception(vm, EXCEPTION_TYPE, "Input object does not implement read() method"); return NULL; } tok = xjs_new_tokener(); while (true) { uc_vm_stack_push(vm, ucv_get(obj)); uc_vm_stack_push(vm, ucv_get(rfn)); uc_vm_stack_push(vm, ucv_int64_new(1024)); if (uc_vm_call(vm, true, 1) != EXCEPTION_NONE) { json_tokener_free(tok); return NULL; } rbuf = uc_vm_stack_pop(vm); /* check EOF */ eof = (rbuf == NULL || (ucv_type(rbuf) == UC_STRING && ucv_string_length(rbuf) == 0)); /* on EOF, stop parsing unless trailing garbage was detected which handled below */ if (eof && !trail) { ucv_put(rbuf); /* Didn't parse a complete object yet, possibly a non-delimitted atomic value such as `null`, `true` etc. - nudge parser by sending final zero byte. See json-c issue #681 */ if (json_tokener_get_error(tok) == json_tokener_continue) *jso = json_tokener_parse_ex(tok, "\0", 1); break; } if (trail || *jso) { uc_vm_raise_exception(vm, EXCEPTION_SYNTAX, "Trailing garbage after JSON data"); json_tokener_free(tok); ucv_put(rbuf); return NULL; } if (ucv_type(rbuf) != UC_STRING) { buf = xprintbuf_new(); ucv_to_stringbuf_formatted(vm, buf, rbuf, 0, '\0', 0); *jso = json_tokener_parse_ex(tok, buf->buf, printbuf_length(buf)); trail = (json_tokener_get_error(tok) == json_tokener_success && json_tokener_get_parse_end(tok) < (size_t)printbuf_length(buf)); printbuf_free(buf); } else { *jso = json_tokener_parse_ex(tok, ucv_string_get(rbuf), ucv_string_length(rbuf)); trail = (json_tokener_get_error(tok) == json_tokener_success && json_tokener_get_parse_end(tok) < ucv_string_length(rbuf)); } ucv_put(rbuf); err = json_tokener_get_error(tok); if (err != json_tokener_success && err != json_tokener_continue) break; } return tok; } static struct json_tokener * uc_json_from_string(uc_vm_t *vm, uc_value_t *str, json_object **jso) { struct json_tokener *tok = xjs_new_tokener(); size_t i; char *p; /* NB: the len + 1 here is intentional to pass the terminating \0 byte * to the json-c parser. This is required to work-around upstream * issue #681 */ *jso = json_tokener_parse_ex(tok, ucv_string_get(str), ucv_string_length(str) + 1); if (json_tokener_get_error(tok) == json_tokener_success) { p = ucv_string_get(str); for (i = json_tokener_get_parse_end(tok); i < ucv_string_length(str); i++) { if (!isspace(p[i])) { uc_vm_raise_exception(vm, EXCEPTION_SYNTAX, "Trailing garbage after JSON data"); json_tokener_free(tok); return NULL; } } } return tok; } /** * Parse the given string or resource as JSON and return the resulting value. * * If the input argument is a plain string, it is directly parsed as JSON. * * If an array, object or resource value is given, this function will attempt to * invoke a `read()` method on it to read chunks of input text to incrementally * parse as JSON data. Reading will stop if the object's `read()` method returns * either `null` or an empty string. * * Throws an exception on parse errors, trailing garbage, or premature EOF. * * Returns the parsed JSON data. * * @function module:core#json * * @param {string} str_or_resource * The string or resource object to be parsed as JSON. * * @returns {*} * * @example * json('{"a":true, "b":123}') // { "a": true, "b": 123 } * json('[1,2,') // Throws an exception * * import { open } from 'fs'; * let fd = open('example.json', 'r'); * json(fd); // will keep invoking `fd.read()` until EOF and * // incrementally parse each read chunk. * * let x = proto( * [ '{"foo":', 'true, ', '"bar":', 'false}' ], * { read: function() { return shift(this) } } * ); * json(x); // will keep invoking `x.read()` until array * // is empty incrementally parse each piece * */ static uc_value_t * uc_json(uc_vm_t *vm, size_t nargs) { uc_value_t *rv = NULL, *src = uc_fn_arg(0); struct json_tokener *tok = NULL; enum json_tokener_error err; json_object *jso = NULL; switch (ucv_type(src)) { case UC_STRING: tok = uc_json_from_string(vm, src, &jso); break; case UC_RESOURCE: case UC_OBJECT: case UC_ARRAY: tok = uc_json_from_object(vm, src, &jso); break; default: uc_vm_raise_exception(vm, EXCEPTION_TYPE, "Passed value is neither a string nor an object"); } if (!tok) goto out; err = json_tokener_get_error(tok); if (err == json_tokener_continue) { uc_vm_raise_exception(vm, EXCEPTION_SYNTAX, "Unexpected end of string in JSON data"); goto out; } else if (err != json_tokener_success) { uc_vm_raise_exception(vm, EXCEPTION_SYNTAX, "Failed to parse JSON string: %s", json_tokener_error_desc(err)); goto out; } rv = ucv_from_json(vm, jso); out: if (tok) json_tokener_free(tok); json_object_put(jso); return rv; } static char * include_path(const char *curpath, const char *incpath) { char *dup, *res; int len; if (*incpath == '/') return realpath(incpath, NULL); dup = curpath ? strrchr(curpath, '/') : NULL; if (dup) len = asprintf(&res, "%.*s/%s", (int)(dup - curpath), curpath, incpath); else len = asprintf(&res, "./%s", incpath); if (len == -1) return NULL; dup = realpath(res, NULL); free(res); return dup; } static uc_value_t * uc_include_common(uc_vm_t *vm, size_t nargs, bool raw_mode) { uc_value_t *path = uc_fn_arg(0); uc_value_t *scope = uc_fn_arg(1); uc_value_t *rv = NULL, *sc = NULL; uc_closure_t *closure = NULL; size_t i; char *p; if (ucv_type(path) != UC_STRING) { uc_vm_raise_exception(vm, EXCEPTION_TYPE, "Passed filename is not a string"); return NULL; } if (scope && ucv_type(scope) != UC_OBJECT) { uc_vm_raise_exception(vm, EXCEPTION_TYPE, "Passed scope value is not an object"); return NULL; } /* find calling closure */ for (i = vm->callframes.count; i > 0; i--) { closure = vm->callframes.entries[i - 1].closure; if (closure) break; } if (!closure) return NULL; p = include_path(uc_program_function_source(closure->function)->runpath, ucv_string_get(path)); if (!p) { uc_vm_raise_exception(vm, EXCEPTION_RUNTIME, "Include file not found"); return NULL; } if (ucv_prototype_get(scope)) { sc = ucv_get(scope); } else if (scope) { sc = ucv_object_new(vm); ucv_object_foreach(scope, key, val) ucv_object_add(sc, key, ucv_get(val)); ucv_prototype_set(sc, ucv_get(uc_vm_scope_get(vm))); } else { sc = ucv_get(uc_vm_scope_get(vm)); } if (uc_require_ucode(vm, p, sc, &rv, raw_mode)) ucv_put(rv); ucv_put(sc); free(p); return NULL; } /** * Evaluate and include the file at the given path and optionally override the * execution scope with the given scope object. * * By default, the file is executed within the same scope as the calling * `include()`, but by passing an object as the second argument, it is possible * to extend the scope available to the included file. * * This is useful to supply additional properties as global variables to the * included code. To sandbox included code, that is giving it only access to * explicitly provided properties, the `proto()` function can be used to create * a scope object with an empty prototype. * * @function module:core#include * * @param {string} path * The path to the file to be included. * * @param {Object} [scope] * The optional scope object to override the execution scope. * * @example * // Load and execute "foo.uc" immediately * include("./foo.uc") * * // Execute the "supplemental.ucode" in an extended scope and make the "foo" * // and "bar" properties available as global variables * include("./supplemental.uc", { * foo: true, * bar: 123 * }) * * // Execute the "untrusted.ucode" in a sandboxed scope and make the "foo" and * // "bar" variables as well as the "print" function available to it. * // By assigning an empty prototype object to the scope, included code has no * // access to other global values anymore. * include("./untrusted.uc", proto({ * foo: true, * bar: 123, * print: print * }, {})) */ static uc_value_t * uc_include(uc_vm_t *vm, size_t nargs) { return uc_include_common(vm, nargs, vm->config && vm->config->raw_mode); } /** * When invoked with a string value as the first argument, the function acts * like `include()` but captures the output of the included file as a string and * returns the captured contents. * * The second argument is treated as the scope. * * When invoked with a function value as the first argument, `render()` calls * the given function and passes all subsequent arguments to it. * * Any output produced by the called function is captured and returned as a * string. The return value of the called function is discarded. * * @function module:core#render * * @param {string|Function} path_or_func * The path to the file or the function to be rendered. * * @param {Object|*} [scope_or_fnarg1] * The optional scope or the first argument for the function. * * @param {*} [fnarg2] * The second argument for the function. * * @param {...*} [fnargN] * Additional arguments for the function. * * @returns {string} * * @example * // Renders template file with given scope and captures the output as a string * const output = render("./template.uc", { foo: "bar" }); * * // Calls a function, captures the output, and returns it as a string * const result = render(function(name) { * printf("Hello, %s!\n", name); * }, "Alice"); */ static uc_value_t * uc_render(uc_vm_t *vm, size_t nargs) { uc_string_t hdr = { .header = { .type = UC_STRING, .refcount = 1 } }; uc_string_t *ustr = NULL; FILE *mem, *prev; size_t len = 0; mem = open_memstream((char **)&ustr, &len); if (!mem) goto out; /* reserve space for uc_string_t header... */ if (fwrite(&hdr, 1, sizeof(hdr), mem) != sizeof(hdr)) goto out; /* divert VM output to memory fd */ prev = vm->output; vm->output = mem; /* execute function */ if (ucv_is_callable(uc_fn_arg(0))) (void) uc_vm_call(vm, false, nargs - 1); /* execute include */ else (void) uc_include_common(vm, nargs, false); /* restore previous VM output */ vm->output = prev; fclose(mem); /* update uc_string_t length */ ustr->length = len - sizeof(*ustr); return &ustr->header; out: uc_vm_raise_exception(vm, EXCEPTION_RUNTIME, "Unable to initialize output memory: %s", strerror(errno)); if (mem) fclose(mem); free(ustr); return NULL; } /** * Print any of the given values to stderr. Arrays and objects are converted to * their JSON representation. * * Returns the amount of bytes printed. * * @function module:core#warn * * @param {...*} x * The values to be printed. * * @returns {number} * * @example * warn("Hello", "world"); // Print "Helloworld" to stderr * warn({ key: "value" }); // Print JSON representation of the object to stderr */ static uc_value_t * uc_warn(uc_vm_t *vm, size_t nargs) { return uc_print_common(vm, nargs, stderr); } /** * Executes the given command, waits for completion, and returns the resulting * exit code. * * The command argument may be either a string, in which case it is passed to * `/bin/sh -c`, or an array, which is directly converted into an `execv()` * argument vector. * * - If the program terminated normally, a positive integer holding the * program's `exit()` code is returned. * - If the program was terminated by an uncaught signal, a negative signal * number is returned. * - If the optional timeout argument is specified, the program is terminated * by `SIGKILL` after that many milliseconds if it doesn't complete within * the timeout. * * Omitting the timeout argument or passing `0` disables the command timeout. * * Returns the program exit code. * * @function module:core#system * * @param {string|Array} command * The command to be executed. * * @param {number} [timeout] * The optional timeout in milliseconds. * * @returns {number} * * @example * // Execute through `/bin/sh` * // prints "Hello world" to stdout and returns 3 * system("echo 'Hello world' && exit 3"); * * // Execute argument vector * // prints the UNIX timestamp to stdout and returns 0 * system(["/usr/bin/date", "+%s"]); * * // Apply a timeout * // returns -9 * system("sleep 3 && echo 'Success'", 1000); */ static uc_value_t * uc_system(uc_vm_t *vm, size_t nargs) { uc_value_t *cmdline = uc_fn_arg(0); uc_value_t *timeout = uc_fn_arg(1); const char **arglist, *fn; sigset_t sigmask, sigomask; struct timespec ts; size_t i, len; int64_t tms; pid_t cld; int rc; if (timeout && (ucv_type(timeout) != UC_INTEGER || ucv_int64_get(timeout) < 0)) { uc_vm_raise_exception(vm, EXCEPTION_TYPE, "Invalid timeout specified"); return NULL; } switch (ucv_type(cmdline)) { case UC_STRING: arglist = xalloc(sizeof(*arglist) * 4); arglist[0] = xstrdup("/bin/sh"); arglist[1] = xstrdup("-c"); arglist[2] = ucv_to_string(vm, cmdline); arglist[3] = NULL; break; case UC_ARRAY: len = ucv_array_length(cmdline); if (len == 0) { uc_vm_raise_exception(vm, EXCEPTION_TYPE, "Passed command array is empty"); return NULL; } arglist = xalloc(sizeof(*arglist) * (len + 1)); for (i = 0; i < len; i++) arglist[i] = ucv_to_string(vm, ucv_array_get(cmdline, i)); arglist[i] = NULL; break; default: uc_vm_raise_exception(vm, EXCEPTION_TYPE, "Passed command is neither string nor array"); return NULL; } tms = timeout ? ucv_int64_get(timeout) : 0; if (tms > 0) { sigemptyset(&sigmask); sigaddset(&sigmask, SIGCHLD); if (sigprocmask(SIG_BLOCK, &sigmask, &sigomask) < 0) { fn = "sigprocmask"; goto fail; } } cld = fork(); switch (cld) { case -1: fn = "fork"; goto fail; case 0: execvp(arglist[0], (char * const *)arglist); exit(-1); break; default: if (tms > 0) { ts.tv_sec = tms / 1000; ts.tv_nsec = (tms % 1000) * 1000000; while (1) { if (sigtimedwait(&sigmask, NULL, &ts) < 0) { if (errno == EINTR) continue; if (errno != EAGAIN) { fn = "sigtimedwait"; goto fail; } kill(cld, SIGKILL); } break; } } while (waitpid(cld, &rc, 0) < 0) { if (errno == EINTR) continue; fn = "waitpid"; goto fail; } if (tms > 0) sigprocmask(SIG_SETMASK, &sigomask, NULL); for (i = 0; arglist[i]; i++) free((char *)arglist[i]); free(arglist); if (WIFEXITED(rc)) return ucv_int64_new(WEXITSTATUS(rc)); else if (WIFSIGNALED(rc)) return ucv_int64_new(-WTERMSIG(rc)); else if (WIFSTOPPED(rc)) return ucv_int64_new(-WSTOPSIG(rc)); return NULL; } fail: if (tms > 0) sigprocmask(SIG_SETMASK, &sigomask, NULL); for (i = 0; arglist[i]; i++) free((char *)arglist[i]); free(arglist); uc_vm_raise_exception(vm, EXCEPTION_RUNTIME, "%s(): %s", fn, strerror(errno)); return NULL; } /** * Enables or disables VM opcode tracing. * * When invoked with a positive non-zero level, opcode tracing is enabled and * debug information is printed to stderr as the program is executed. * * Invoking `trace()` with zero as an argument turns off opcode tracing. * * @function module:core#trace * * @param {number} level * The level of tracing to enable. * * @example * trace(1); // Enables opcode tracing * trace(0); // Disables opcode tracing */ static uc_value_t * uc_trace(uc_vm_t *vm, size_t nargs) { uc_value_t *level = uc_fn_arg(0); uint8_t prev_level; if (ucv_type(level) != UC_INTEGER) { uc_vm_raise_exception(vm, EXCEPTION_TYPE, "Invalid level specified"); return NULL; } prev_level = vm->trace; vm->trace = ucv_int64_get(level); return ucv_int64_new(prev_level); } /** * Get or set the prototype of the array or object value `val`. * * When invoked without a second argument, the function returns the current * prototype of the value in `val` or `null` if there is no prototype or if the * given value is neither an object nor an array. * * When invoked with a second prototype argument, the given `proto` value is set * as the prototype on the array or object in `val`. * * Throws an exception if the given prototype value is not an object. * * @function module:core#proto * * @param {Array|Object} val * The array or object value. * * @param {Object} [proto] * The optional prototype object. * * @returns {?Object} * * @example * const arr = [1, 2, 3]; * proto(arr); // Returns the current prototype of the array (null by default) * proto(arr, { foo: true }); // Sets the given object as the prototype of the array */ static uc_value_t * uc_proto(uc_vm_t *vm, size_t nargs) { uc_value_t *val = uc_fn_arg(0); uc_value_t *proto = NULL; if (nargs < 2) return ucv_get(ucv_prototype_get(val)); proto = uc_fn_arg(1); if (!ucv_prototype_set(val, proto)) uc_vm_raise_exception(vm, EXCEPTION_TYPE, "Passed value is neither a prototype, resource or object"); ucv_get(proto); return ucv_get(val); } /** * Pause execution for the given amount of milliseconds. * * @function module:core#sleep * * @param {number} milliseconds * The amount of milliseconds to sleep. * * @returns {boolean} * * @example * sleep(1000); // Sleeps for 1 second */ static uc_value_t * uc_sleep(uc_vm_t *vm, size_t nargs) { uc_value_t *duration = uc_fn_arg(0); struct timeval tv; int64_t ms; ms = ucv_to_integer(duration); if (errno != 0 || ms <= 0) return ucv_boolean_new(false); tv.tv_sec = ms / 1000; tv.tv_usec = (ms % 1000) * 1000; select(0, NULL, NULL, NULL, &tv); return ucv_boolean_new(true); } /** * Raise an exception with the given message parameter when the value in `cond` * is not truish. * * When `message` is omitted, the default value is `Assertion failed`. * * @function module:core#assert * * @param {*} cond * The value to check for truthiness. * * @param {string} [message] * The message to include in the exception. * * @throws {Error} When the condition is falsy. * * @example * assert(true, "This is true"); // No exception is raised * assert(false); // Exception is raised with the default message "Assertion failed" */ static uc_value_t * uc_assert(uc_vm_t *vm, size_t nargs) { uc_value_t *cond = uc_fn_arg(0); uc_value_t *msg = uc_fn_arg(1); bool freeable = false; char *s; if (!ucv_is_truish(cond)) { s = msg ? uc_cast_string(vm, &msg, &freeable) : "Assertion failed"; uc_vm_raise_exception(vm, EXCEPTION_USER, "%s", s); if (freeable) free(s); return NULL; } return ucv_get(cond); } /** * Construct a regular expression instance from the given `source` pattern * string and any flags optionally specified by the `flags` argument. * * - Throws a type error exception if `flags` is not a string or if the string * in `flags` contains unrecognized regular expression flag characters. * - Throws a syntax error when the pattern in `source` cannot be compiled into * a valid regular expression. * * Returns the compiled regular expression value. * * @function module:core#regexp * * @param {string} source * The pattern string. * * @param {string} [flags] * The optional regular expression flags. * * @returns {RegExp} * * @example * regexp('foo.*bar', 'is'); // equivalent to /foo.*bar/is * regexp('foo.*bar', 'x'); // throws a "Type error: Unrecognized flag character 'x'" exception * regexp('foo.*('); // throws a "Syntax error: Unmatched ( or \( exception" */ static uc_value_t * uc_regexp(uc_vm_t *vm, size_t nargs) { bool icase = false, newline = false, global = false, freeable; uc_value_t *source = uc_fn_arg(0); uc_value_t *flags = uc_fn_arg(1); uc_value_t *regex = NULL; char *p, *err = NULL; if (flags) { if (ucv_type(flags) != UC_STRING) { uc_vm_raise_exception(vm, EXCEPTION_TYPE, "Given flags argument is not a string"); return NULL; } for (p = ucv_string_get(flags); *p; p++) { switch (*p) { case 'i': icase = true; break; case 's': newline = true; break; case 'g': global = true; break; default: uc_vm_raise_exception(vm, EXCEPTION_TYPE, "Unrecognized flag character '%c'", *p); return NULL; } } } p = uc_cast_string(vm, &source, &freeable); regex = ucv_regexp_new(p, icase, newline, global, &err); if (freeable) free(p); if (err) { uc_vm_raise_exception(vm, EXCEPTION_SYNTAX, "%s", err); ucv_put(regex); free(err); return NULL; } return regex; } /** * Match the given subject against the supplied wildcard (file glob) pattern. * * - If a truthy value is supplied as the third argument, case-insensitive * matching is performed. * - If a non-string value is supplied as the subject, it is converted into a * string before being matched. * * Returns `true` when the value matched the given pattern, otherwise `false`. * * @function module:core#wildcard * * @param {*} subject * The subject to match against the wildcard pattern. * * @param {string} pattern * The wildcard pattern. * * @param {boolean} [nocase] * Whether to perform case-insensitive matching. * * @returns {boolean} * * @example * wildcard("file.txt", "*.txt"); // Returns true * wildcard("file.txt", "*.TXT", true); // Returns true (case-insensitive match) * wildcard("file.txt", "*.jpg"); // Returns false */ static uc_value_t * uc_wildcard(uc_vm_t *vm, size_t nargs) { uc_value_t *subject = uc_fn_arg(0); uc_value_t *pattern = uc_fn_arg(1); uc_value_t *icase = uc_fn_arg(2); int flags = 0, rv; bool freeable; char *s; if (!subject || ucv_type(pattern) != UC_STRING) return NULL; if (ucv_is_truish(icase)) flags |= FNM_CASEFOLD; s = uc_cast_string(vm, &subject, &freeable); rv = fnmatch(ucv_string_get(pattern), s, flags); if (freeable) free(s); return ucv_boolean_new(rv == 0); } /** * Determine the path of the source file currently being executed by ucode. * * @function module:core#sourcepath * * @param {number} [depth=0] * The depth to walk up the call stack. * * @param {boolean} [dironly] * Whether to return only the directory portion of the source file path. * * @returns {?string} * * @example * sourcepath(); // Returns the path of the currently executed file * sourcepath(1); // Returns the path of the parent source file * sourcepath(2, true); // Returns the directory portion of the grandparent source file path */ static uc_value_t * uc_sourcepath(uc_vm_t *vm, size_t nargs) { uc_value_t *calldepth = uc_fn_arg(0); uc_value_t *dironly = uc_fn_arg(1); uc_value_t *rv = NULL; uc_callframe_t *frame; char *path = NULL; int64_t depth; size_t i; depth = ucv_to_integer(calldepth); if (errno) depth = 0; for (i = vm->callframes.count; i > 0; i--) { frame = &vm->callframes.entries[i - 1]; if (!frame->closure) continue; if (depth > 0) { depth--; continue; } path = realpath(uc_program_function_source(frame->closure->function)->runpath, NULL); break; } if (path) { if (ucv_is_truish(dironly)) rv = ucv_string_new(dirname(path)); else rv = ucv_string_new(path); free(path); } return rv; } static uc_value_t * uc_min_max(uc_vm_t *vm, size_t nargs, int cmp) { uc_value_t *rv = NULL, *val; bool set = false; size_t i; for (i = 0; i < nargs; i++) { val = uc_fn_arg(i); if (!set || ucv_compare(cmp, val, rv, NULL)) { set = true; rv = val; } } return ucv_get(rv); } /** * Return the smallest value among all parameters passed to the function. * * @function module:core#min * * @param {...*} [val] * The values to compare. * * @returns {*} * * @example * min(5, 2.1, 3, "abc", 0.3); // Returns 0.3 * min(1, "abc"); // Returns 1 * min("1", "abc"); // Returns "1" * min("def", "abc", "ghi"); // Returns "abc" * min(true, false); // Returns false */ static uc_value_t * uc_min(uc_vm_t *vm, size_t nargs) { return uc_min_max(vm, nargs, I_LT); } /** * Return the largest value among all parameters passed to the function. * * @function module:core#max * * @param {...*} [val] * The values to compare. * * @returns {*} * * @example * max(5, 2.1, 3, "abc", 0.3); // Returns 5 * max(1, "abc"); // Returns 1 (!) * max("1", "abc"); // Returns "abc" * max("def", "abc", "ghi"); // Returns "ghi" * max(true, false); // Returns true */ static uc_value_t * uc_max(uc_vm_t *vm, size_t nargs) { return uc_min_max(vm, nargs, I_GT); } /* ------------------------------------------------------------------------- * The following base64 encoding and decoding routines are taken from * https://git.openwrt.org/?p=project/libubox.git;a=blob;f=base64.c * and modified for use in ucode. * * Original copyright and license statements below. */ /* * base64 - libubox base64 functions * * Copyright (C) 2015 Felix Fietkau * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ /* $OpenBSD: base64.c,v 1.7 2013/12/31 02:32:56 tedu Exp $ */ /* * Copyright (c) 1996 by Internet Software Consortium. * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SOFTWARE CONSORTIUM DISCLAIMS * ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL INTERNET SOFTWARE * CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR * PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS * ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS * SOFTWARE. */ /* * Portions Copyright (c) 1995 by International Business Machines, Inc. * * International Business Machines, Inc. (hereinafter called IBM) grants * permission under its copyrights to use, copy, modify, and distribute this * Software with or without fee, provided that the above copyright notice and * all paragraphs of this notice appear in all copies, and that the name of IBM * not be used in connection with the marketing of any product incorporating * the Software or modifications thereof, without specific, written prior * permission. * * To the extent it has a right to do so, IBM grants an immunity from suit * under its patents, if any, for the use, sale or manufacture of products to * the extent that such products are used for performing Domain Name System * dynamic updates in TCP/IP networks by means of the Software. No immunity is * granted for any product per se or for any other function of any product. * * THE SOFTWARE IS PROVIDED "AS IS", AND IBM DISCLAIMS ALL WARRANTIES, * INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A * PARTICULAR PURPOSE. IN NO EVENT SHALL IBM BE LIABLE FOR ANY SPECIAL, * DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER ARISING * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE, EVEN * IF IBM IS APPRISED OF THE POSSIBILITY OF SUCH DAMAGES. */ /* skips all whitespace anywhere. converts characters, four at a time, starting at (or after) src from base - 64 numbers into three 8 bit bytes in the target area. it returns the number of data bytes stored at the target, or -1 on error. */ /** * Decodes the given base64 encoded string and returns the decoded result. * * - If non-whitespace, non-base64 characters are encountered, if invalid * padding or trailing garbage is found, the function returns `null`. * - If a non-string argument is given, the function returns `null`. * * @function module:core#b64dec * * @param {string} str * The base64 encoded string to decode. * * @returns {?string} * * @example * b64dec("VGhpcyBpcyBhIHRlc3Q="); // Returns "This is a test" * b64dec(123); // Returns null * b64dec("XXX"); // Returns null */ static uc_value_t * uc_b64dec(uc_vm_t *vm, size_t nargs) { enum { BYTE1, BYTE2, BYTE3, BYTE4 } state; uc_value_t *str = uc_fn_arg(0); uc_stringbuf_t *buf; const char *src; unsigned int ch; uint8_t val; size_t off; if (ucv_type(str) != UC_STRING) return NULL; buf = ucv_stringbuf_new(); src = ucv_string_get(str); off = printbuf_length(buf); state = BYTE1; /* memset the last expected output char to pre-grow the output buffer */ printbuf_memset(buf, off + (ucv_string_length(str) / 4) * 3, 0, 1); while ((ch = (unsigned char)*src++) != '\0') { if (isspace(ch)) /* Skip whitespace anywhere. */ continue; if (ch == '=') break; if (ch >= 'A' && ch <= 'Z') val = ch - 'A'; else if (ch >= 'a' && ch <= 'z') val = ch - 'a' + 26; else if (ch >= '0' && ch <= '9') val = ch - '0' + 52; else if (ch == '+') val = 62; else if (ch == '/') val = 63; else goto err; switch (state) { case BYTE1: buf->buf[off] = val << 2; state = BYTE2; break; case BYTE2: buf->buf[off++] |= val >> 4; buf->buf[off] = (val & 0x0f) << 4; state = BYTE3; break; case BYTE3: buf->buf[off++] |= val >> 2; buf->buf[off] = (val & 0x03) << 6; state = BYTE4; break; case BYTE4: buf->buf[off++] |= val; state = BYTE1; break; } } /* * We are done decoding Base-64 chars. Let's see if we ended * on a byte boundary, and/or with erroneous trailing characters. */ if (ch == '=') { /* We got a pad char. */ ch = (unsigned char)*src++; /* Skip it, get next. */ switch (state) { case BYTE1: /* Invalid = in first position */ case BYTE2: /* Invalid = in second position */ goto err; case BYTE3: /* Valid, means one byte of info */ /* Skip any number of spaces. */ for (; ch != '\0'; ch = (unsigned char)*src++) if (!isspace(ch)) break; /* Make sure there is another trailing = sign. */ if (ch != '=') goto err; ch = (unsigned char)*src++; /* Skip the = */ /* Fall through to "single trailing =" case. */ /* FALLTHROUGH */ case BYTE4: /* Valid, means two bytes of info */ /* * We know this char is an =. Is there anything but * whitespace after it? */ for (; ch != '\0'; ch = (unsigned char)*src++) if (!isspace(ch)) goto err; /* * Now make sure for cases BYTE3 and BYTE4 that the "extra" * bits that slopped past the last full byte were * zeros. If we don't check them, they become a * subliminal channel. */ if (buf->buf[off] != 0) goto err; } } else { /* * We ended by seeing the end of the string. Make sure we * have no partial bytes lying around. */ if (state != BYTE1) goto err; } /* Truncate buffer length to actual output length */ buf->bpos = off; return ucv_stringbuf_finish(buf); err: printbuf_free(buf); return NULL; } static const char Base64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; /** * Encodes the given string into base64 and returns the resulting string. * * - If a non-string argument is given, the function returns `null`. * * @function module:core#b64enc * * @param {string} str * The string to encode. * * @returns {?string} * * @example * b64enc("This is a test"); // Returns "VGhpcyBpcyBhIHRlc3Q=" * b64enc(123); // Returns null */ static uc_value_t * uc_b64enc(uc_vm_t *vm, size_t nargs) { uc_value_t *str = uc_fn_arg(0); unsigned char input[3] = {0}; uc_stringbuf_t *buf; const char *src; char output[4]; size_t len, i; if (ucv_type(str) != UC_STRING) return NULL; buf = ucv_stringbuf_new(); src = ucv_string_get(str); len = ucv_string_length(str); while (2 < len) { input[0] = (unsigned char)*src++; input[1] = (unsigned char)*src++; input[2] = (unsigned char)*src++; len -= 3; output[0] = Base64[input[0] >> 2]; output[1] = Base64[((input[0] & 0x03) << 4) + (input[1] >> 4)]; output[2] = Base64[((input[1] & 0x0f) << 2) + (input[2] >> 6)]; output[3] = Base64[input[2] & 0x3f]; ucv_stringbuf_addstr(buf, output, sizeof(output)); } /* Now we worry about padding. */ if (0 != len) { /* Get what's left. */ input[0] = input[1] = input[2] = '\0'; for (i = 0; i < len; i++) input[i] = *src++; output[0] = Base64[input[0] >> 2]; output[1] = Base64[((input[0] & 0x03) << 4) + (input[1] >> 4)]; output[2] = (len == 1) ? '=' : Base64[((input[1] & 0x0f) << 2) + (input[2] >> 6)]; output[3] = '='; ucv_stringbuf_addstr(buf, output, sizeof(output)); } return ucv_stringbuf_finish(buf); } /* End of base64 code. * ------------------------------------------------------------------------- */ static unsigned long uc_uniq_ucv_hash(const void *k) { union { double d; int64_t i; uint64_t u; } conv; uc_value_t *uv = (uc_value_t *)k; unsigned int h; uint8_t *u8; size_t len; h = ucv_type(uv); switch (h) { case UC_STRING: u8 = (uint8_t *)ucv_string_get(uv); len = ucv_string_length(uv); break; case UC_INTEGER: conv.i = ucv_int64_get(uv); if (errno == ERANGE) { h *= 2; conv.u = ucv_uint64_get(uv); } u8 = (uint8_t *)&conv.u; len = sizeof(conv.u); break; case UC_DOUBLE: conv.d = ucv_double_get(uv); u8 = (uint8_t *)&conv.u; len = sizeof(conv.u); break; default: u8 = (uint8_t *)&uv; len = sizeof(uv); break; } while (len > 0) { h = h * 129 + (*u8++) + LH_PRIME; len--; } return h; } static int uc_uniq_ucv_equal(const void *k1, const void *k2) { uc_value_t *uv1 = (uc_value_t *)k1; uc_value_t *uv2 = (uc_value_t *)k2; if (!ucv_is_scalar(uv1) && !ucv_is_scalar(uv2)) return (uv1 == uv2); /* for the sake of array item uniqueness, treat two NaNs as equal */ if (ucv_type(uv1) == UC_DOUBLE && ucv_type(uv2) == UC_DOUBLE && isnan(ucv_double_get(uv1)) && isnan(ucv_double_get(uv2))) return true; return ucv_is_equal(uv1, uv2); } /** * Returns a new array containing all unique values of the given input array. * * - The order is preserved, and subsequent duplicate values are skipped. * - If a non-array argument is given, the function returns `null`. * * @function module:core#uniq * * @param {Array} array * The input array. * * @returns {?Array} * * @example * uniq([1, true, "foo", 2, true, "bar", "foo"]); // Returns [1, true, "foo", 2, "bar"] * uniq("test"); // Returns null */ static uc_value_t * uc_uniq(uc_vm_t *vm, size_t nargs) { uc_value_t *list = uc_fn_arg(0); uc_value_t *uniq = NULL; struct lh_table *seen; unsigned long hash; uc_value_t *item; size_t i, len; if (ucv_type(list) != UC_ARRAY) return NULL; seen = lh_table_new(16, NULL, uc_uniq_ucv_hash, uc_uniq_ucv_equal); uniq = ucv_array_new(vm); assert(seen && uniq); for (i = 0, len = ucv_array_length(list); i < len; i++) { item = ucv_array_get(list, i); hash = lh_get_hash(seen, item); if (!lh_table_lookup_entry_w_hash(seen, item, hash)) { lh_table_insert_w_hash(seen, item, NULL, hash, 0); ucv_array_push(uniq, ucv_get(item)); } } lh_table_free(seen); return uniq; } /** * A time spec is a plain object describing a point in time, it is returned by * the {@link module:core#gmtime|gmtime()} and * {@link module:core#localtime|localtime()} functions and expected as parameter * by the complementary {@link module:core#timegm|timegm()} and * {@link module:core#timelocal|timelocal()} functions. * * When returned by `gmtime()` or `localtime()`, all members of the object will * be initialized, when passed as argument to `timegm()` or `timelocal()`, most * member values are optional. * * @typedef {Object} module:core.TimeSpec * @property {number} sec - Seconds (0..60) * @property {number} min - Minutes (0..59) * @property {number} hour - Hours (0..23) * @property {number} mday - Day of month (1..31) * @property {number} mon - Month (1..12) * @property {number} year - Year (>= 1900) * @property {number} wday - Day of week (1..7, Sunday = 7) * @property {number} yday - Day of year (1-366, Jan 1st = 1) * @property {number} isdst - Daylight saving time in effect (yes = 1) */ static uc_value_t * uc_gettime_common(uc_vm_t *vm, size_t nargs, bool local) { uc_value_t *ts = uc_fn_arg(0), *res; time_t t = ts ? (time_t)ucv_to_integer(ts) : time(NULL); struct tm *tm = (local ? localtime : gmtime)(&t); if (!tm) return NULL; res = ucv_object_new(vm); ucv_object_add(res, "sec", ucv_int64_new(tm->tm_sec)); ucv_object_add(res, "min", ucv_int64_new(tm->tm_min)); ucv_object_add(res, "hour", ucv_int64_new(tm->tm_hour)); ucv_object_add(res, "mday", ucv_int64_new(tm->tm_mday)); ucv_object_add(res, "mon", ucv_int64_new(tm->tm_mon + 1)); ucv_object_add(res, "year", ucv_int64_new(tm->tm_year + 1900)); ucv_object_add(res, "wday", ucv_int64_new(tm->tm_wday ? tm->tm_wday : 7)); ucv_object_add(res, "yday", ucv_int64_new(tm->tm_yday + 1)); ucv_object_add(res, "isdst", ucv_int64_new(tm->tm_isdst)); return res; } /** * Return the given epoch timestamp (or now, if omitted) as a dictionary * containing broken-down date and time information according to the local * system timezone. * * See {@link module:core.TimeSpec|TimeSpec} for a description of the fields. * * Note that in contrast to the underlying `localtime(3)` C library function, * the values for `mon`, `wday`, and `yday` are 1-based, and the `year` is * 1900-based. * * @function module:core#localtime * * @param {number} [epoch] * The epoch timestamp. * * @returns {module:core.TimeSpec} * * @example * localtime(1647953502); * // Returns: * // { * // sec: 42, * // min: 51, * // hour: 13, * // mday: 22, * // mon: 3, * // year: 2022, * // wday: 2, * // yday: 81, * // isdst: 0 * // } */ static uc_value_t * uc_localtime(uc_vm_t *vm, size_t nargs) { return uc_gettime_common(vm, nargs, true); } /** * Like `localtime()` but interpreting the given epoch value as UTC time. * * See {@link module:core#localtime|localtime()} for details on the return value. * * @function module:core#gmtime * * @param {number} [epoch] * The epoch timestamp. * * @returns {module:core.TimeSpec} * * @example * gmtime(1647953502); * // Returns: * // { * // sec: 42, * // min: 51, * // hour: 13, * // mday: 22, * // mon: 3, * // year: 2022, * // wday: 2, * // yday: 81, * // isdst: 0 * // } */ static uc_value_t * uc_gmtime(uc_vm_t *vm, size_t nargs) { return uc_gettime_common(vm, nargs, false); } static uc_value_t * uc_mktime_common(uc_vm_t *vm, size_t nargs, bool local) { #define FIELD(name, required) \ { #name, required, offsetof(struct tm, tm_##name) } const struct { const char *name; bool required; size_t off; } fields[] = { FIELD(sec, false), FIELD(min, false), FIELD(hour, false), FIELD(mday, true), FIELD(mon, true), FIELD(year, true), FIELD(isdst, false) }; uc_value_t *to = uc_fn_arg(0), *v; struct tm tm = { 0 }; bool exists; time_t t; size_t i; if (ucv_type(to) != UC_OBJECT) return NULL; for (i = 0; i < ARRAY_SIZE(fields); i++) { v = ucv_object_get(to, fields[i].name, &exists); if (!exists && fields[i].required) return NULL; *(int *)((char *)&tm + fields[i].off) = (int)ucv_to_integer(v); } if (tm.tm_mon > 0) tm.tm_mon--; if (tm.tm_year >= 1900) tm.tm_year -= 1900; t = (local ? mktime : timegm)(&tm); return (t != (time_t)-1) ? ucv_int64_new((int64_t)t) : NULL; } /** * Performs the inverse operation of {@link module:core#localtime|localtime()} * by taking a broken-down date and time dictionary and transforming it into an * epoch value according to the local system timezone. * * The `wday` and `yday` fields of the given date time specification are * ignored. Field values outside of their valid range are internally normalized, * e.g. October 40th is interpreted as November 9th. * * Returns the resulting epoch value or null if the input date time dictionary * was invalid or if the date time specification cannot be represented as epoch * value. * * @function module:core#timelocal * * @param {module:core.TimeSpec} datetimespec * The broken-down date and time dictionary. * * @returns {?number} * * @example * timelocal({ "sec": 42, "min": 51, "hour": 13, "mday": 22, "mon": 3, "year": 2022, "isdst": 0 }); * // Returns 1647953502 */ static uc_value_t * uc_timelocal(uc_vm_t *vm, size_t nargs) { return uc_mktime_common(vm, nargs, true); } /** * Like `timelocal()` but interpreting the given date time specification as UTC * time. * * See {@link module:core#timelocal|timelocal()} for details. * * @function module:core#timegm * * @param {module:core.TimeSpec} datetimespec * The broken-down date and time dictionary. * * @returns {?number} * * @example * timegm({ "sec": 42, "min": 51, "hour": 13, "mday": 22, "mon": 3, "year": 2022, "isdst": 0 }); * // Returns 1647953502 */ static uc_value_t * uc_timegm(uc_vm_t *vm, size_t nargs) { return uc_mktime_common(vm, nargs, false); } /** * Reads the current second and microsecond value of the system clock. * * By default, the realtime clock is queried which might skew forwards or * backwards due to NTP changes, system sleep modes etc. If a truish value is * passed as argument, the monotonic system clock is queried instead, which will * return the monotonically increasing time since some arbitrary point in the * past (usually the system boot time). * * Returns a two element array containing the full seconds as the first element * and the nanosecond fraction as the second element. * * Returns `null` if a monotonic clock value is requested and the system does * not implement this clock type. * * @function module:core#clock * * @param {boolean} [monotonic] * Whether to query the monotonic system clock. * * @returns {?number[]} * * @example * clock(); // [ 1647954926, 798269464 ] * clock(true); // [ 474751, 527959975 ] */ static uc_value_t * uc_clock(uc_vm_t *vm, size_t nargs) { clockid_t id = ucv_is_truish(uc_fn_arg(0)) ? CLOCK_MONOTONIC : CLOCK_REALTIME; struct timespec ts; uc_value_t *res; if (clock_gettime(id, &ts) == -1) return NULL; res = ucv_array_new(vm); ucv_array_set(res, 0, ucv_int64_new((int64_t)ts.tv_sec)); ucv_array_set(res, 1, ucv_int64_new((int64_t)ts.tv_nsec)); return res; } /** * Encodes the given byte string into a hexadecimal digit string, converting * the input value to a string if needed. * * @function module:core#hexenc * * @param {string} val * The byte string to encode. * * @returns {string} * * @example * hexenc("Hello world!\n"); // "48656c6c6f20776f726c64210a" */ static uc_value_t * uc_hexenc(uc_vm_t *vm, size_t nargs) { const char *hex = "0123456789abcdef"; uc_value_t *input = uc_fn_arg(0); uc_stringbuf_t *buf; size_t off, len; uint8_t byte; if (!input) return NULL; buf = ucv_stringbuf_new(); off = printbuf_length(buf); ucv_to_stringbuf(vm, buf, input, false); len = printbuf_length(buf) - off; /* memset the last expected output char to grow the output buffer */ printbuf_memset(buf, off + len * 2, 0, 1); /* translate string into hex back to front to reuse the same buffer */ while (len > 0) { byte = buf->buf[--len + off]; buf->buf[off + len * 2 + 0] = hex[byte / 16]; buf->buf[off + len * 2 + 1] = hex[byte % 16]; } /* do not include sentinel `\0` in string length */ buf->bpos--; return ucv_stringbuf_finish(buf); } static inline uint8_t hexval(unsigned char c, bool lo) { return ((c > '9') ? (c - 'a') + 10 : c - '0') << (lo ? 0 : 4); } /** * Decodes the given hexadecimal digit string into a byte string, optionally * skipping specified characters. * * If the characters to skip are not specified, a default of `" \t\n"` is used. * * Returns null if the input string contains invalid characters or an uneven * amount of hex digits. * * Returns the decoded byte string on success. * * @function module:core#hexdec * * @param {string} hexstring * The hexadecimal digit string to decode. * * @param {string} [skipchars] * The characters to skip during decoding. * * @returns {?string} * * @example * hexdec("48656c6c6f20776f726c64210a"); // "Hello world!\n" * hexdec("44:55:66:77:33:44", ":"); // "DUfw3D" */ static uc_value_t * uc_hexdec(uc_vm_t *vm, size_t nargs) { uc_value_t *input = uc_fn_arg(0); uc_value_t *skip = uc_fn_arg(1); size_t len, off, n, i; uc_stringbuf_t *buf; unsigned char *p; const char *s; if (ucv_type(input) != UC_STRING) return NULL; if (skip && ucv_type(skip) != UC_STRING) return NULL; p = (unsigned char *)ucv_string_get(input); len = ucv_string_length(input); s = skip ? (const char *)ucv_string_get(skip) : " \t\n"; for (i = 0, n = 0; i < len; i++) { if (isxdigit(p[i])) n++; else if (!s || !strchr(s, p[i])) return NULL; } if (n & 1) return NULL; buf = ucv_stringbuf_new(); off = printbuf_length(buf); /* preallocate the output buffer */ printbuf_memset(buf, off, 0, n / 2 + 1); for (i = 0, n = 0; i < len; i++) { if (!isxdigit(p[i])) continue; buf->buf[off + (n >> 1)] |= hexval(p[i] | 32, n & 1); n++; } /* do not include sentinel `\0` in string length */ buf->bpos--; return ucv_stringbuf_finish(buf); } /** * Interacts with the mark and sweep garbage collector of the running ucode * virtual machine. * * Depending on the given `operation` string argument, the meaning of `argument` * and the function return value differs. * * The following operations are defined: * * - `collect` - Perform a complete garbage collection cycle, returns `true`. * - `start` - (Re-)start periodic garbage collection, `argument` is an optional * integer in the range `1..65535` specifying the interval. * Defaults to `1000` if omitted. Returns `true` if the periodic GC * was previously stopped and is now started or if the interval * changed. Returns `false` otherwise. * - `stop` - Stop periodic garbage collection. Returns `true` if the periodic * GC was previously started and is now stopped, `false` otherwise. * - `count` - Count the amount of active complex object references in the VM * context, returns the counted amount. * * If the `operation` argument is omitted, the default is `collect`. * * @function module:core#gc * * @param {string} [operation] * The operation to perform. * * @param {*} [argument] * The argument for the operation. * * @returns {?(boolean|number)} * * @example * gc(); // true * gc("start"); // true * gc("count"); // 42 */ static uc_value_t * uc_gc(uc_vm_t *vm, size_t nargs) { uc_value_t *operation = uc_fn_arg(0); uc_value_t *argument = uc_fn_arg(1); const char *op = NULL; uc_weakref_t *ref; int64_t n; if (operation != NULL && ucv_type(operation) != UC_STRING) return NULL; op = ucv_string_get(operation); if (!op || !strcmp(op, "collect")) { ucv_gc(vm); return ucv_boolean_new(true); } else if (!strcmp(op, "start")) { n = argument ? ucv_int64_get(argument) : 0; if (errno || n < 0 || n > 0xFFFF) return NULL; if (n == 0) n = GC_DEFAULT_INTERVAL; return ucv_boolean_new(uc_vm_gc_start(vm, n)); } else if (!strcmp(op, "stop")) { return ucv_boolean_new(uc_vm_gc_stop(vm)); } else if (!strcmp(op, "count")) { for (n = 0, ref = vm->values.next; ref != &vm->values; ref = ref->next) n++; return ucv_uint64_new(n); } return NULL; } /** * A parse configuration is a plain object describing options to use when * compiling ucode at runtime. It is expected as parameter by the * {@link module:core#loadfile|loadfile()} and * {@link module:core#loadstring|loadstring()} functions. * * All members of the parse configuration object are optional and will default * to the state of the running ucode file if omitted. * * @typedef {Object} module:core.ParseConfig * * @property {boolean} lstrip_blocks * Whether to strip whitespace preceding template directives. * See {@link tutorial-02-syntax.html#whitespace-handling|Whitespace handling}. * * @property {boolean} trim_blocks * Whether to trim trailing newlines following template directives. * See {@link tutorial-02-syntax.html#whitespace-handling|Whitespace handling}. * * @property {boolean} strict_declarations * Whether to compile the code in strict mode (`true`) or not (`false`). * * @property {boolean} raw_mode * Whether to compile the code in plain script mode (`true`) or not (`false`). * * @property {string[]} module_search_path * Override the module search path for compile time imports while compiling the * ucode source. * * @property {string[]} force_dynlink_list * List of module names assumed to be dynamic library extensions, allows * compiling ucode source with import statements referring to `*.so` extensions * not present at compile time. */ static void uc_compile_parse_config(uc_parse_config_t *config, uc_value_t *spec) { uc_value_t *v, *p; size_t i, j; bool found; struct { const char *key; bool *flag; uc_search_path_t *path; } fields[] = { { "lstrip_blocks", &config->lstrip_blocks, NULL }, { "trim_blocks", &config->trim_blocks, NULL }, { "strict_declarations", &config->strict_declarations, NULL }, { "raw_mode", &config->raw_mode, NULL }, { "module_search_path", NULL, &config->module_search_path }, { "force_dynlink_list", NULL, &config->force_dynlink_list } }; for (i = 0; i < ARRAY_SIZE(fields); i++) { v = ucv_object_get(spec, fields[i].key, &found); if (!found) continue; if (fields[i].flag) { *fields[i].flag = ucv_is_truish(v); } else if (fields[i].path) { fields[i].path->count = 0; fields[i].path->entries = NULL; for (j = 0; j < ucv_array_length(v); j++) { p = ucv_array_get(v, j); if (ucv_type(p) != UC_STRING) continue; uc_vector_push(fields[i].path, ucv_string_get(p)); } } } } static uc_value_t * uc_load_common(uc_vm_t *vm, size_t nargs, uc_source_t *source) { uc_parse_config_t conf = *vm->config; uc_program_t *program; uc_value_t *closure; char *err = NULL; uc_compile_parse_config(&conf, uc_fn_arg(1)); program = uc_compile(&conf, source, &err); closure = program ? ucv_closure_new(vm, uc_program_entry(program), false) : NULL; uc_program_put(program); if (!vm->config || conf.module_search_path.entries != vm->config->module_search_path.entries) uc_vector_clear(&conf.module_search_path); if (!vm->config || conf.force_dynlink_list.entries != vm->config->force_dynlink_list.entries) uc_vector_clear(&conf.force_dynlink_list); if (!closure) { uc_error_message_indent(&err); if (source->buffer) uc_vm_raise_exception(vm, EXCEPTION_RUNTIME, "Unable to compile source string:\n\n%s", err); else uc_vm_raise_exception(vm, EXCEPTION_RUNTIME, "Unable to compile source file '%s':\n\n%s", source->filename, err); } uc_source_put(source); free(err); return closure; } /** * Compiles the given code string into a ucode program and returns the resulting * program entry function. * * The optional `options` dictionary overrides parse and compile options. * * - If a non-string `code` argument is given, it is implicitly converted to a * string value first. * - If `options` is omitted or a non-object value, the compile options of the * running ucode program are reused. * * See {@link module:core.ParseConfig|ParseConfig} for known keys within the * `options` object. Unrecognized keys are ignored, unspecified options default * to those of the running program. * * Returns the compiled program entry function. * * Throws an exception on compilation errors. * * @function module:core#loadstring * * @param {string} code * The code string to compile. * * @param {module:core.ParseConfig} [options] * The options for compilation. * * @returns {Function} * * @example * let fn1 = loadstring("Hello, {{ name }}", { raw_mode: false }); * * global.name = "Alice"; * fn1(); // prints `Hello, Alice` * * * let fn2 = loadstring("return 1 + 2;", { raw_mode: true }); * fn2(); // 3 */ static uc_value_t * uc_loadstring(uc_vm_t *vm, size_t nargs) { uc_value_t *code = uc_fn_arg(0); uc_source_t *source; size_t len; char *s; if (ucv_type(code) == UC_STRING) { len = ucv_string_length(code); s = xalloc(len); memcpy(s, ucv_string_get(code), len); } else { s = ucv_to_string(vm, code); len = strlen(s); } source = uc_source_new_buffer("[loadstring argument]", s, len); if (!source) { uc_vm_raise_exception(vm, EXCEPTION_RUNTIME, "Unable to allocate source buffer: %s", strerror(errno)); return NULL; } return uc_load_common(vm, nargs, source); } /** * Compiles the given file into a ucode program and returns the resulting * program entry function. * * See {@link module:core#loadstring|`loadstring()`} for details. * * Returns the compiled program entry function. * * Throws an exception on compilation or file I/O errors. * * @function module:core#loadfile * * @param {string} path * The path of the file to compile. * * @param {module:core.ParseConfig} [options] * The options for compilation. * * @returns {Function} * * @example * loadfile("./templates/example.uc"); // function main() { ... } */ static uc_value_t * uc_loadfile(uc_vm_t *vm, size_t nargs) { uc_value_t *path = uc_fn_arg(0); uc_source_t *source; if (ucv_type(path) != UC_STRING) return NULL; source = uc_source_new_file(ucv_string_get(path)); if (!source) { uc_vm_raise_exception(vm, EXCEPTION_RUNTIME, "Unable to open source file %s: %s", ucv_string_get(path), strerror(errno)); return NULL; } return uc_load_common(vm, nargs, source); } /** * Calls the given function value with a modified environment. * * The given `ctx` argument is used as `this` context for the invoked function * and the given `scope` value as global environment. Any further arguments are * passed to the invoked function as-is. * * When `ctx` is omitted or `null`, the function will get invoked with `this` * being `null`. * * When `scope` is omitted or `null`, the function will get executed with the * current global environment of the running program. When `scope` is set to a * dictionary, the dictionary is used as global function environment. * * When the `scope` dictionary has no prototype, the current global environment * will be set as prototype, means the scope will inherit from it. * * When a scope prototype is set, it is kept. This allows passing an isolated * (sandboxed) function scope without access to the global environment. * * Any further argument is forwarded as-is to the invoked function as function * call argument. * * Returns `null` if the given function value `fn` is not callable. * * Returns the return value of the invoked function in all other cases. * * Forwards exceptions thrown by the invoked function. * * @function module:core#call * * @param {Function} fn * Function value to call. * * @param {*} [ctx=null] * `this` context for the invoked function. * * @param {Object} [scope=null] * Global environment for the invoked function. * * @param {...*} [arg] * Additional arguments to pass to the invoked function. * * @returns {*} * * @example * // Override this context * call(function() { printf("%J\n", this) }); // null * call(function() { printf("%J\n", this) }, null); // null * call(function() { printf("%J\n", this) }, { x: 1 }); // { "x": 1 } * call(function() { printf("%J\n", this) }, { x: 2 }); // { "x": 2 } * * // Run with default scope * global.a = 1; * call(function() { printf("%J\n", a) }); // 1 * * // Override scope, inherit from current global scope (implicit) * call(function() { printf("%J\n", a) }, null, { a: 2 }); // 2 * * // Override scope, inherit from current global scope (explicit) * call(function() { printf("%J\n", a) }, null, * proto({ a: 2 }, global)); // 2 * * // Override scope, don't inherit (pass `printf()` but not `a`) * call(function() { printf("%J\n", a) }, null, * proto({}, { printf })); // null * * // Forward arguments * x = call((x, y, z) => x * y * z, null, null, 2, 3, 4); // x = 24 */ static uc_value_t * uc_callfunc(uc_vm_t *vm, size_t nargs) { size_t argoff = vm->stack.count - nargs, i; uc_value_t *fn_scope, *prev_scope, *res; uc_value_t *fn = uc_fn_arg(0); uc_value_t *this = uc_fn_arg(1); uc_value_t *scope = uc_fn_arg(2); if (!ucv_is_callable(fn)) return NULL; if (scope && ucv_type(scope) != UC_OBJECT) return NULL; if (ucv_prototype_get(scope)) { fn_scope = ucv_get(scope); } else if (scope) { fn_scope = ucv_object_new(vm); ucv_object_foreach(scope, k, v) ucv_object_add(fn_scope, k, ucv_get(v)); ucv_prototype_set(fn_scope, ucv_get(uc_vm_scope_get(vm))); } else { fn_scope = NULL; } uc_vm_stack_push(vm, ucv_get(this)); uc_vm_stack_push(vm, ucv_get(fn)); for (i = 3; i < nargs; i++) uc_vm_stack_push(vm, ucv_get(vm->stack.entries[3 + argoff++])); if (fn_scope) { prev_scope = ucv_get(uc_vm_scope_get(vm)); uc_vm_scope_set(vm, fn_scope); } if (uc_vm_call(vm, true, i - 3) == EXCEPTION_NONE) res = uc_vm_stack_pop(vm); else res = NULL; if (fn_scope) uc_vm_scope_set(vm, prev_scope); return res; } /** * Set or query process signal handler function. * * When invoked with two arguments, a signal specification and a signal handler * value, this function configures a new process signal handler. * * When invoked with one argument, a signal specification, this function returns * the currently configured handler for the given signal. * * The signal specification might either be an integer signal number or a string * value containing a signal name (with or without "SIG" prefix). Signal names * are treated case-insensitively. * * The signal handler might be either a callable function value or one of the * two special string values `"ignore"` and `"default"`. Passing `"ignore"` will * mask the given process signal while `"default"` will restore the operating * systems default behaviour for the given signal. * * In case a callable handler function is provided, it is invoked at the * earliest opportunity after receiving the corresponding signal from the * operating system. The invoked function will receive a single argument, the * number of the signal it is invoked for. * * Note that within the ucode VM, process signals are not immediately delivered, * instead the VM keeps track of received signals and delivers them to the ucode * script environment at the next opportunity, usually before executing the next * byte code instruction. This means that if a signal is received while * performing a computationally expensive operation in C mode, such as a complex * regexp match, the corresponding ucode signal handler will only be invoked * after that operation concluded and control flow returns to the VM. * * Returns the signal handler function or one of the special values `"ignore"` * or `"default"` corresponding to the given signal specification. * * Returns `null` if an invalid signal spec or signal handler was provided. * * Returns `null` if changing the signal action failed, e.g. due to insufficient * permission, or when attempting to ignore a non-ignorable signal. * * @function module:core#signal * * @param {number|string} signal * The signal to query/set handler for. * * @param {Function|string} [handler] * The signal handler to install for the given signal. * * @returns {Function|string} * * @example * // Ignore signals * signal('INT', 'ignore'); // "ignore" * signal('SIGINT', 'ignore'); // "ignore" (equivalent to 'INT') * signal('sigterm', 'ignore'); // "ignore" (signal names are case insensitive) * signal(9, 'ignore'); // null (SIGKILL cannot be ignored) * * // Restore signal default behavior * signal('INT', 'default'); // "default" * signal('foobar', 'default'); // null (unknown signal name) * signal(-313, 'default'); // null (invalid signal number) * * // Set custom handler function * function intexit(signo) { * printf("I received signal number %d\n", signo); * exit(1); * } * * signal('SIGINT', intexit); // returns intexit * signal('SIGINT') == intexit; // true */ static uc_value_t * uc_signal(uc_vm_t *vm, size_t nargs) { uc_value_t *signame = uc_fn_arg(0); uc_value_t *sighandler = uc_fn_arg(1); struct sigaction sa = { 0 }; char *sigstr; int sig; if (ucv_type(signame) == UC_INTEGER) { sig = (int)ucv_int64_get(signame); if (errno || sig < 0 || sig >= UC_SYSTEM_SIGNAL_COUNT) return NULL; if (!uc_system_signal_names[sig]) return NULL; } else if (ucv_type(signame) == UC_STRING) { sigstr = ucv_string_get(signame); if (!strncasecmp(sigstr, "SIG", 3)) sigstr += 3; for (sig = 0; sig < UC_SYSTEM_SIGNAL_COUNT; sig++) if (uc_system_signal_names[sig] && !strcasecmp(uc_system_signal_names[sig], sigstr)) break; if (sig == UC_SYSTEM_SIGNAL_COUNT) return NULL; } else { return NULL; } /* Query current signal handler state */ if (nargs < 2) { if (sigaction(sig, NULL, &sa) != 0) return NULL; if (sa.sa_handler == SIG_IGN) return ucv_string_new("ignore"); if (sa.sa_handler == SIG_DFL) return ucv_string_new("default"); return ucv_get(ucv_array_get(vm->signal.handler, sig)); } /* Install new signal handler */ if (ucv_type(sighandler) == UC_STRING) { sigstr = ucv_string_get(sighandler); sa.sa_flags = SA_ONSTACK | SA_RESTART; sigemptyset(&sa.sa_mask); if (!strcmp(sigstr, "ignore")) sa.sa_handler = SIG_IGN; else if (!strcmp(sigstr, "default")) sa.sa_handler = SIG_DFL; else return NULL; if (sigaction(sig, &sa, NULL) != 0) return NULL; ucv_array_set(vm->signal.handler, sig, NULL); } else if (ucv_is_callable(sighandler)) { if (sigaction(sig, &vm->signal.sa, NULL) != 0) return NULL; ucv_array_set(vm->signal.handler, sig, ucv_get(sighandler)); } else { return NULL; } return ucv_get(sighandler); } const uc_function_list_t uc_stdlib_functions[] = { { "chr", uc_chr }, { "die", uc_die }, { "exists", uc_exists }, { "exit", uc_exit }, { "filter", uc_filter }, { "getenv", uc_getenv }, { "hex", uc_hex }, { "index", uc_lindex }, { "int", uc_int }, { "join", uc_join }, { "keys", uc_keys }, { "lc", uc_lc }, { "length", uc_length }, { "ltrim", uc_ltrim }, { "map", uc_map }, { "ord", uc_ord }, { "pop", uc_pop }, { "print", uc_print }, { "push", uc_push }, { "reverse", uc_reverse }, { "rindex", uc_rindex }, { "rtrim", uc_rtrim }, { "shift", uc_shift }, { "sort", uc_sort }, { "splice", uc_splice }, { "slice", uc_slice }, { "split", uc_split }, { "substr", uc_substr }, { "time", uc_time }, { "trim", uc_trim }, { "type", uc_type }, { "uchr", uc_uchr }, { "uc", uc_uc }, { "unshift", uc_unshift }, { "values", uc_values }, { "sprintf", uc_sprintf }, { "printf", uc_printf }, { "require", uc_require }, { "iptoarr", uc_iptoarr }, { "arrtoip", uc_arrtoip }, { "match", uc_match }, { "replace", uc_replace }, { "json", uc_json }, { "include", uc_include }, { "warn", uc_warn }, { "system", uc_system }, { "trace", uc_trace }, { "proto", uc_proto }, { "sleep", uc_sleep }, { "assert", uc_assert }, { "render", uc_render }, { "regexp", uc_regexp }, { "wildcard", uc_wildcard }, { "sourcepath", uc_sourcepath }, { "min", uc_min }, { "max", uc_max }, { "b64dec", uc_b64dec }, { "b64enc", uc_b64enc }, { "uniq", uc_uniq }, { "localtime", uc_localtime }, { "gmtime", uc_gmtime }, { "timelocal", uc_timelocal }, { "timegm", uc_timegm }, { "clock", uc_clock }, { "hexdec", uc_hexdec }, { "hexenc", uc_hexenc }, { "gc", uc_gc }, { "loadstring", uc_loadstring }, { "loadfile", uc_loadfile }, { "call", uc_callfunc }, { "signal", uc_signal }, }; void uc_stdlib_load(uc_value_t *scope) { uc_function_list_register(scope, uc_stdlib_functions); } uc_cfn_ptr_t uc_stdlib_function(const char *name) { size_t i; for (i = 0; i < ARRAY_SIZE(uc_stdlib_functions); i++) if (!strcmp(uc_stdlib_functions[i].name, name)) return uc_stdlib_functions[i].func; return NULL; }