#include #include #include #include #include #include #include #include #include #include #include // Maximum length, in bytes, of a UTF-8 string that is read into a stack buffer. // Longer strings fall back to a heap allocation. This covers the vast majority // of URLs without touching the heap. #define BARE_URL_STACK_STRING_MAX 1024 // The number of component offsets the parser writes out. #define BARE_URL_COMPONENTS_LEN (sizeof(((url_t *) 0)->components) / sizeof(url_component_t)) static bool bare_url__check_argc(js_env_t *env, size_t argc, size_t expected) { int err; if (argc < expected) { err = js_throw_type_errorf(env, NULL, "Expected %zu arguments, got %zu", expected, argc); assert(err == 0); return false; } return true; } static bool bare_url__check_string(js_env_t *env, js_value_t *value, const char *message) { int err; bool is_string; err = js_is_string(env, value, &is_string); assert(err == 0); if (!is_string) { err = js_throw_type_error(env, NULL, message); assert(err == 0); } return is_string; } static bool bare_url__check_base(js_env_t *env, js_value_t *value, bool *has_base) { int err; js_value_type_t type; err = js_typeof(env, value, &type); assert(err == 0); *has_base = type == js_string; if (type == js_string || type == js_null || type == js_undefined) return true; err = js_throw_type_error(env, NULL, "Base must be a string, null, or undefined"); assert(err == 0); return false; } static bool bare_url__check_boolean(js_env_t *env, js_value_t *value, const char *message) { int err; bool is_boolean; err = js_is_boolean(env, value, &is_boolean); assert(err == 0); if (!is_boolean) { err = js_throw_type_error(env, NULL, message); assert(err == 0); } return is_boolean; } static bool bare_url__check_components(js_env_t *env, js_value_t *value, uint32_t **result) { int err; bool is_typedarray; err = js_is_typedarray(env, value, &is_typedarray); assert(err == 0); js_typedarray_type_t type; size_t len; js_value_t *arraybuffer; if (is_typedarray) { err = js_get_typedarray_info(env, value, &type, (void **) result, &len, &arraybuffer, NULL); if (err < 0) return false; } if (!is_typedarray || type != js_uint32array) { err = js_throw_type_error(env, NULL, "Components must be a Uint32Array"); assert(err == 0); return false; } // A detached typed array reports no length of its own, but its data pointer // is stale rather than null and so must not be written to. bool is_detached; err = js_is_detached_arraybuffer(env, arraybuffer, &is_detached); assert(err == 0); if (is_detached) { err = js_throw_type_error(env, NULL, "Components must not be detached"); assert(err == 0); return false; } if (len < BARE_URL_COMPONENTS_LEN) { err = js_throw_range_errorf(env, NULL, "Components must have at least %zu elements, got %zu", (size_t) BARE_URL_COMPONENTS_LEN, len); assert(err == 0); return false; } return true; } // The UTF-8 encoding of a JavaScript string, together with whatever backs it. typedef struct { const utf8_t *data; size_t len; js_string_view_t *view; utf8_t *heap; } bare_url_string_t; // Exposes `value` as UTF-8. Strings that are stored as ASCII, which is nearly // all of them, are borrowed directly from the engine without being copied. // Anything else is transcoded into `stack`, or into a freshly allocated heap // buffer when it does not fit. A non-zero return leaves an exception pending. // The result must be released with bare_url__free_string() either way. static inline int bare_url__read_string(js_env_t *env, js_value_t *value, utf8_t *stack, size_t stack_len, bare_url_string_t *result) { int err; result->data = NULL; result->len = 0; result->view = NULL; result->heap = NULL; js_string_encoding_t encoding; const void *data; size_t len; err = js_get_string_view(env, value, &encoding, &data, &len, &result->view); if (err < 0) return err; size_t utf8_len; if (encoding == js_utf8) { result->data = (const utf8_t *) data; result->len = len; return 0; } if (encoding == js_latin1) { utf8_len = utf8_length_from_latin1((const latin1_t *) data, len); // A Latin-1 string that is no longer as UTF-8 contains only ASCII, whose // Latin-1 and UTF-8 encodings are identical. if (utf8_len == len) { result->data = (const utf8_t *) data; result->len = len; return 0; } } else { assert(encoding == js_utf16le); utf8_len = utf8_length_from_utf16le((const utf16_t *) data, len); } utf8_t *buffer; if (utf8_len <= stack_len) { buffer = stack; } else { buffer = result->heap = malloc(utf8_len); if (buffer == NULL) { err = js_release_string_view(env, result->view); assert(err == 0); result->view = NULL; err = js_throw_error(env, uv_err_name(UV_ENOMEM), uv_strerror(UV_ENOMEM)); assert(err == 0); return -1; } } if (encoding == js_latin1) { latin1_convert_to_utf8((const latin1_t *) data, len, buffer); } else { utf16le_convert_to_utf8((const utf16_t *) data, len, buffer); } result->data = buffer; result->len = utf8_len; return 0; } // Releases a string read with bare_url__read_string(), freeing its buffer only // when it was heap allocated rather than borrowed or written to the caller's // stack buffer. static inline void bare_url__free_string(js_env_t *env, bare_url_string_t *string) { int err; free(string->heap); if (string->view == NULL) return; err = js_release_string_view(env, string->view); assert(err == 0); } static js_value_t * bare_url_parse(js_env_t *env, js_callback_info_t *info) { int err; size_t argc = 4; js_value_t *argv[4]; err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); assert(err == 0); if (!bare_url__check_argc(env, argc, 4)) return NULL; if (!bare_url__check_string(env, argv[0], "Input must be a string")) return NULL; bool has_base; if (!bare_url__check_base(env, argv[1], &has_base)) return NULL; uint32_t *components; if (!bare_url__check_components(env, argv[2], &components)) return NULL; if (!bare_url__check_boolean(env, argv[3], "Throw must be a boolean")) return NULL; bool should_throw; err = js_get_value_bool(env, argv[3], &should_throw); assert(err == 0); // Set when the input could not be read, which leaves an exception of its own // pending and so must not be reported as a parse failure on top. bool threw = false; url_t base; url_init(&base); if (has_base) { utf8_t stack[BARE_URL_STACK_STRING_MAX]; bare_url_string_t input; err = bare_url__read_string(env, argv[1], stack, sizeof(stack), &input); if (err == 0) err = url_parse(&base, input.data, input.len, NULL); else threw = true; bare_url__free_string(env, &input); if (err < 0) { url_destroy(&base); if (should_throw && !threw) { err = js_throw_error(env, NULL, "Invalid base URL"); assert(err == 0); } return NULL; } } utf8_t stack[BARE_URL_STACK_STRING_MAX]; bare_url_string_t input; err = bare_url__read_string(env, argv[0], stack, sizeof(stack), &input); url_t url; url_init(&url); if (err == 0) err = url_parse(&url, input.data, input.len, has_base ? &base : NULL); else threw = true; bare_url__free_string(env, &input); if (err < 0) { url_destroy(&base); url_destroy(&url); if (should_throw && !threw) { err = js_throw_error(env, NULL, "Invalid URL"); assert(err == 0); } return NULL; } js_value_t *href; err = js_create_string_latin1(env, (const latin1_t *) url.href.data, url.href.len, &href); // The offsets are only handed over once the href they refer to is, so a // failure here leaves the caller's buffer as it was. if (err == 0) memcpy(components, &url.components, sizeof(url.components)); url_destroy(&base); url_destroy(&url); if (err < 0) return NULL; return href; } static js_value_t * bare_url_can_parse(js_env_t *env, js_callback_info_t *info) { int err; size_t argc = 2; js_value_t *argv[2]; err = js_get_callback_info(env, info, &argc, argv, NULL, NULL); assert(err == 0); if (!bare_url__check_argc(env, argc, 2)) return NULL; if (!bare_url__check_string(env, argv[0], "Input must be a string")) return NULL; bool has_base; if (!bare_url__check_base(env, argv[1], &has_base)) return NULL; bool threw = false; url_t base; url_init(&base); if (has_base) { utf8_t stack[BARE_URL_STACK_STRING_MAX]; bare_url_string_t input; err = bare_url__read_string(env, argv[1], stack, sizeof(stack), &input); if (err == 0) err = url_parse(&base, input.data, input.len, NULL); else threw = true; bare_url__free_string(env, &input); if (err < 0) { url_destroy(&base); if (threw) return NULL; js_value_t *result; err = js_get_boolean(env, false, &result); assert(err == 0); return result; } } utf8_t stack[BARE_URL_STACK_STRING_MAX]; bare_url_string_t input; err = bare_url__read_string(env, argv[0], stack, sizeof(stack), &input); url_t url; url_init(&url); if (err == 0) err = url_parse(&url, input.data, input.len, has_base ? &base : NULL); else threw = true; bare_url__free_string(env, &input); url_destroy(&base); url_destroy(&url); if (threw) return NULL; js_value_t *result; err = js_get_boolean(env, err == 0, &result); assert(err == 0); return result; } static js_value_t * bare_url_exports(js_env_t *env, js_value_t *exports) { int err; #define V(name, fn) \ { \ js_value_t *val; \ err = js_create_function(env, name, -1, fn, NULL, &val); \ assert(err == 0); \ err = js_set_named_property(env, exports, name, val); \ assert(err == 0); \ } V("parse", bare_url_parse) V("canParse", bare_url_can_parse) #undef V return exports; } BARE_MODULE(bare_url, bare_url_exports)