| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent b71d5de commit 2a64032
3 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -167,41 +167,62 @@ function lazyCryptoRandom() { | |||
| 167 | 167 | return cryptoRandom; | |
| 168 | 168 | } | |
| 169 | 169 | ||
| 170 | + /** | ||
| 171 | + * Copy href and the latest `urlComponents` snapshot into a URLContext. | ||
| 172 | + * Property assignment order matches the historical URLContext fields so | ||
| 173 | + * `util.inspect(..., { showHidden: true })` stays stable. | ||
| 174 | + * @param {object} ctx | ||
| 175 | + * @param {string} href | ||
| 176 | + */ | ||
| 177 | + function setURLContextFromBinding(ctx, href) { | ||
| 178 | + const c = bindingUrl.urlComponents; | ||
| 179 | + ctx.href = href; | ||
| 180 | + ctx.protocol_end = c[0]; | ||
| 181 | + ctx.username_end = c[1]; | ||
| 182 | + ctx.host_start = c[2]; | ||
| 183 | + ctx.host_end = c[3]; | ||
| 184 | + ctx.pathname_start = c[5]; | ||
| 185 | + ctx.search_start = c[6]; | ||
| 186 | + ctx.hash_start = c[7]; | ||
| 187 | + ctx.port = c[4]; | ||
| 188 | + ctx.scheme_type = c[8]; | ||
| 189 | + } | ||
| 190 | + | ||
| 170 | 191 | // This class provides the internal state of a URL object. An instance of this | |
| 171 | 192 | // class is stored in every URL object and is accessed internally by setters | |
| 172 | 193 | // and getters. It roughly corresponds to the concept of a URL record in the | |
| 173 | 194 | // URL Standard, with a few differences. It is also the object transported to | |
| 174 | 195 | // the C++ binding. | |
| 175 | 196 | // Refs: https://url.spec.whatwg.org/#concept-url | |
| 197 | + // | ||
| 198 | + // scheme_type refers to ada::scheme::type: | ||
| 199 | + // HTTP = 0, NOT_SPECIAL = 1, HTTPS = 2, WS = 3, FTP = 4, WSS = 5, FILE = 6 | ||
| 176 | 200 | class URLContext { | |
| 177 | 201 | // This is the maximum value uint32_t can get. | |
| 178 | 202 | // Ada uses uint32_t(-1) for declaring omitted values. | |
| 179 | 203 | static #omitted = 4294967295; | |
| 180 | 204 | ||
| 181 | - href = ''; | ||
| 182 | - protocol_end = 0; | ||
| 183 | - username_end = 0; | ||
| 184 | - host_start = 0; | ||
| 185 | - host_end = 0; | ||
| 186 | - pathname_start = 0; | ||
| 187 | - search_start = 0; | ||
| 188 | - hash_start = 0; | ||
| 189 | - port = 0; | ||
| 190 | 205 | /** | |
| 191 | - * Refers to `ada::scheme::type` | ||
| 192 | - * | ||
| 193 | - * enum type : uint8_t { | ||
| 194 | - * HTTP = 0, | ||
| 195 | - * NOT_SPECIAL = 1, | ||
| 196 | - * HTTPS = 2, | ||
| 197 | - * WS = 3, | ||
| 198 | - * FTP = 4, | ||
| 199 | - * WSS = 5, | ||
| 200 | - * FILE = 6 | ||
| 201 | - * }; | ||
| 202 | - * @type {number} | ||
| 206 | + * @param {string} [href] Parsed href. When omitted, create an empty context | ||
| 207 | + * (used by `URL.parse` on invalid input). When provided, `bindingUrl.parse` | ||
| 208 | + * / `update` has just written `urlComponents`. | ||
| 203 | 209 | */ | |
| 204 | - scheme_type = 1; | ||
| 210 | + constructor(href) { | ||
| 211 | + if (href === undefined) { | ||
| 212 | + this.href = ''; | ||
| 213 | + this.protocol_end = 0; | ||
| 214 | + this.username_end = 0; | ||
| 215 | + this.host_start = 0; | ||
| 216 | + this.host_end = 0; | ||
| 217 | + this.pathname_start = 0; | ||
| 218 | + this.search_start = 0; | ||
| 219 | + this.hash_start = 0; | ||
| 220 | + this.port = 0; | ||
| 221 | + this.scheme_type = 1; | ||
| 222 | + return; | ||
| 223 | + } | ||
| 224 | + setURLContextFromBinding(this, href); | ||
| 225 | + } | ||
| 205 | 226 | ||
| 206 | 227 | get hasPort() { | |
| 207 | 228 | return this.port !== URLContext.#omitted; | |
@@ -835,7 +856,7 @@ const kCreateURLFromPosixPathSymbol = Symbol('kCreateURLFromPosixPath'); | |||
| 835 | 856 | const kCreateURLFromWindowsPathSymbol = Symbol('kCreateURLFromWindowsPath'); | |
| 836 | 857 | ||
| 837 | 858 | class URL { | |
| 838 | - #context = new URLContext(); | ||
| 859 | + #context; | ||
| 839 | 860 | #searchParams; | |
| 840 | 861 | #searchParamsModified; | |
| 841 | 862 | ||
@@ -860,16 +881,16 @@ class URL { | |||
| 860 | 881 | } | |
| 861 | 882 | ||
| 862 | 883 | constructor(input, base = undefined, parseSymbol = undefined) { | |
| 863 | - markTransferMode(this, false, false); | ||
| 864 | - | ||
| 865 | 884 | if (arguments.length === 0) { | |
| 866 | 885 | throw new ERR_MISSING_ARGS('url'); | |
| 867 | 886 | } | |
| 868 | 887 | ||
| 869 | 888 | // StringPrototypeToWellFormed is not needed. | |
| 870 | - input = `${input}`; | ||
| 889 | + if (typeof input !== 'string') { | ||
| 890 | + input = `${input}`; | ||
| 891 | + } | ||
| 871 | 892 | ||
| 872 | - if (base !== undefined) { | ||
| 893 | + if (base !== undefined && typeof base !== 'string') { | ||
| 873 | 894 | base = `${base}`; | |
| 874 | 895 | } | |
| 875 | 896 | ||
@@ -884,9 +905,12 @@ class URL { | |||
| 884 | 905 | bindingUrl.pathToFileURL(input, interpretAsWindowsPath, base) : | |
| 885 | 906 | bindingUrl.parse(input, base, raiseException); | |
| 886 | 907 | } | |
| 887 | - if (href) { | ||
| 888 | - this.#updateContext(href); | ||
| 889 | - } | ||
| 908 | + | ||
| 909 | + // Delay context allocation until parse finishes so invalid URLs that | ||
| 910 | + // throw do not pay for an unused URLContext. Initialize in one shot | ||
| 911 | + // from the binding snapshot instead of writing an empty context first. | ||
| 912 | + this.#context = href ? new URLContext(href) : new URLContext(); | ||
| 913 | + markTransferMode(this, false, false); | ||
| 890 | 914 | } | |
| 891 | 915 | ||
| 892 | 916 | static parse(input, base = undefined) { | |
@@ -955,29 +979,7 @@ class URL { | |||
| 955 | 979 | const previousSearch = shouldUpdateSearchParams && this.#searchParams && | |
| 956 | 980 | (this.#searchParamsModified ? this.#getSearchFromParams() : this.#getSearchFromContext()); | |
| 957 | 981 | ||
| 958 | - this.#context.href = href; | ||
| 959 | - | ||
| 960 | - const { | ||
| 961 | - 0: protocol_end, | ||
| 962 | - 1: username_end, | ||
| 963 | - 2: host_start, | ||
| 964 | - 3: host_end, | ||
| 965 | - 4: port, | ||
| 966 | - 5: pathname_start, | ||
| 967 | - 6: search_start, | ||
| 968 | - 7: hash_start, | ||
| 969 | - 8: scheme_type, | ||
| 970 | - } = bindingUrl.urlComponents; | ||
| 971 | - | ||
| 972 | - this.#context.protocol_end = protocol_end; | ||
| 973 | - this.#context.username_end = username_end; | ||
| 974 | - this.#context.host_start = host_start; | ||
| 975 | - this.#context.host_end = host_end; | ||
| 976 | - this.#context.port = port; | ||
| 977 | - this.#context.pathname_start = pathname_start; | ||
| 978 | - this.#context.search_start = search_start; | ||
| 979 | - this.#context.hash_start = hash_start; | ||
| 980 | - this.#context.scheme_type = scheme_type; | ||
| 982 | + setURLContextFromBinding(this.#context, href); | ||
| 981 | 983 | ||
| 982 | 984 | if (this.#searchParams) { | |
| 983 | 985 | // If the search string has updated, URL becomes the source of truth, and we update URLSearchParams. | |
@@ -1202,10 +1204,12 @@ class URL { | |||
| 1202 | 1204 | throw new ERR_MISSING_ARGS('url'); | |
| 1203 | 1205 | } | |
| 1204 | 1206 | ||
| 1205 | - url = `${url}`; | ||
| 1207 | + if (typeof url !== 'string') { | ||
| 1208 | + url = `${url}`; | ||
| 1209 | + } | ||
| 1206 | 1210 | ||
| 1207 | 1211 | if (base !== undefined) { | |
| 1208 | - return bindingUrl.canParse(url, `${base}`); | ||
| 1212 | + return bindingUrl.canParse(url, typeof base === 'string' ? base : `${base}`); | ||
| 1209 | 1213 | } | |
| 1210 | 1214 | ||
| 1211 | 1215 | // It is important to differentiate the canParse call statements | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -8,6 +8,7 @@ | |||
| 8 | 8 | #include "node_metadata.h" | |
| 9 | 9 | #include "node_process-inl.h" | |
| 10 | 10 | #include "path.h" | |
| 11 | + #include "simdutf.h" | ||
| 11 | 12 | #include "util-inl.h" | |
| 12 | 13 | #include "v8-fast-api-calls.h" | |
| 13 | 14 | #include "v8-local-handle.h" | |
@@ -33,6 +34,38 @@ using v8::SnapshotCreator; | |||
| 33 | 34 | using v8::String; | |
| 34 | 35 | using v8::Value; | |
| 35 | 36 | ||
| 37 | + namespace { | ||
| 38 | + | ||
| 39 | + // Parse a V8 string as a URL. One-byte ASCII inputs are parsed in place | ||
| 40 | + // without allocating a UTF-8 copy. `reuse_input` is set when the serialized | ||
| 41 | + // href is identical to that ASCII input so the caller can return the original | ||
| 42 | + // V8 string. Non-ASCII inputs are never reused: UTF-8 conversion may replace | ||
| 43 | + // unpaired surrogates, so the original string may not match href. | ||
| 44 | + ada::result<ada::url_aggregator> ParseUrlFromV8String( | ||
| 45 | + Isolate* isolate, | ||
| 46 | + Local<String> input, | ||
| 47 | + const ada::url_aggregator* base_url, | ||
| 48 | + bool* reuse_input) { | ||
| 49 | + { | ||
| 50 | + String::ValueView view(isolate, input); | ||
| 51 | + if (view.is_one_byte()) { | ||
| 52 | + const char* data = reinterpret_cast<const char*>(view.data8()); | ||
| 53 | + const size_t length = static_cast<size_t>(view.length()); | ||
| 54 | + if (simdutf::validate_ascii(data, length)) [[likely]] { | ||
| 55 | + const std::string_view input_view(data, length); | ||
| 56 | + auto out = ada::parse<ada::url_aggregator>(input_view, base_url); | ||
| 57 | + *reuse_input = out.has_value() && out->get_href() == input_view; | ||
| 58 | + return out; | ||
| 59 | + } | ||
| 60 | + } | ||
| 61 | + } | ||
| 62 | + *reuse_input = false; | ||
| 63 | + Utf8Value utf8(isolate, input); | ||
| 64 | + return ada::parse<ada::url_aggregator>(utf8.ToStringView(), base_url); | ||
| 65 | + } | ||
| 66 | + | ||
| 67 | + } // namespace | ||
| 68 | + | ||
| 36 | 69 | void BindingData::MemoryInfo(MemoryTracker* tracker) const { | |
| 37 | 70 | tracker->TrackField("url_components_buffer", url_components_buffer_); | |
| 38 | 71 | } | |
@@ -392,32 +425,51 @@ void BindingData::Parse(const FunctionCallbackInfo<Value>& args) { | |||
| 392 | 425 | Realm* realm = Realm::GetCurrent(args); | |
| 393 | 426 | BindingData* binding_data = realm->GetBindingData<BindingData>(); | |
| 394 | 427 | Isolate* isolate = realm->isolate(); | |
| 395 | - std::optional<std::string> base_{}; | ||
| 428 | + Local<String> input_string = args[0].As<String>(); | ||
| 396 | 429 | ||
| 397 | - Utf8Value input(isolate, args[0]); | ||
| 398 | 430 | ada::result<ada::url_aggregator> base; | |
| 399 | 431 | ada::url_aggregator* base_pointer = nullptr; | |
| 400 | 432 | if (args[1]->IsString()) { | |
| 401 | - base_ = Utf8Value(isolate, args[1]).ToString(); | ||
| 402 | - base = ada::parse<ada::url_aggregator>(*base_); | ||
| 403 | - if (!base && raise_exception) { | ||
| 404 | - return ThrowInvalidURL(realm->env(), input.ToStringView(), base_); | ||
| 405 | - } else if (!base) { | ||
| 433 | + bool unused_reuse = false; | ||
| 434 | + base = ParseUrlFromV8String( | ||
| 435 | + isolate, args[1].As<String>(), nullptr, &unused_reuse); | ||
| 436 | + if (!base) { | ||
| 437 | + if (raise_exception) { | ||
| 438 | + Utf8Value input(isolate, input_string); | ||
| 439 | + Utf8Value base_utf8(isolate, args[1]); | ||
| 440 | + return ThrowInvalidURL( | ||
| 441 | + realm->env(), input.ToStringView(), base_utf8.ToString()); | ||
| 442 | + } | ||
| 406 | 443 | return; | |
| 407 | 444 | } | |
| 408 | 445 | base_pointer = &base.value(); | |
| 409 | 446 | } | |
| 410 | - auto out = | ||
| 411 | - ada::parse<ada::url_aggregator>(input.ToStringView(), base_pointer); | ||
| 412 | 447 | ||
| 413 | - if (!out && raise_exception) { | ||
| 414 | - return ThrowInvalidURL(realm->env(), input.ToStringView(), base_); | ||
| 415 | - } else if (!out) { | ||
| 448 | + bool reuse_input = false; | ||
| 449 | + auto out = | ||
| 450 | + ParseUrlFromV8String(isolate, input_string, base_pointer, &reuse_input); | ||
| 451 | + if (!out) { | ||
| 452 | + if (raise_exception) { | ||
| 453 | + Utf8Value input(isolate, input_string); | ||
| 454 | + std::optional<std::string> base_error; | ||
| 455 | + if (args[1]->IsString()) { | ||
| 456 | + base_error = Utf8Value(isolate, args[1]).ToString(); | ||
| 457 | + } | ||
| 458 | + return ThrowInvalidURL( | ||
| 459 | + realm->env(), input.ToStringView(), std::move(base_error)); | ||
| 460 | + } | ||
| 416 | 461 | return; | |
| 417 | 462 | } | |
| 418 | 463 | ||
| 419 | 464 | binding_data->UpdateComponents(out->get_components(), out->type); | |
| 420 | 465 | ||
| 466 | + // Already-serialized ASCII URLs are the common case. Reuse the input | ||
| 467 | + // string instead of allocating an identical V8 string from href. | ||
| 468 | + if (reuse_input) { | ||
| 469 | + args.GetReturnValue().Set(args[0]); | ||
| 470 | + return; | ||
| 471 | + } | ||
| 472 | + | ||
| 421 | 473 | Local<Value> ret; | |
| 422 | 474 | if (ToV8Value(realm->context(), out->get_href(), isolate).ToLocal(&ret)) | |
| 423 | 475 | [[likely]] { | |
@@ -439,13 +491,15 @@ void BindingData::Update(const FunctionCallbackInfo<Value>& args) { | |||
| 439 | 491 | return; | |
| 440 | 492 | } | |
| 441 | 493 | enum url_update_action action = static_cast<enum url_update_action>(val); | |
| 442 | - Utf8Value input(isolate, args[0].As<String>()); | ||
| 443 | 494 | Utf8Value new_value(isolate, args[2].As<String>()); | |
| 444 | 495 | ||
| 445 | 496 | std::string_view new_value_view = new_value.ToStringView(); | |
| 446 | 497 | // A serialized URL is not always reparsable: the IDNA encoder can emit a | |
| 447 | 498 | // host label that the decoder rejects. Fail the update instead of crashing. | |
| 448 | - auto out = ada::parse<ada::url_aggregator>(input.ToStringView()); | ||
| 499 | + // Existing hrefs are typically already-serialized ASCII, so parse in place. | ||
| 500 | + bool unused_reuse = false; | ||
| 501 | + auto out = ParseUrlFromV8String( | ||
| 502 | + isolate, args[0].As<String>(), nullptr, &unused_reuse); | ||
| 449 | 503 | if (!out) { | |
| 450 | 504 | return args.GetReturnValue().Set(false); | |
| 451 | 505 | } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,88 @@ | |||
| 1 | + 'use strict'; | ||
| 2 | + | ||
| 3 | + // Covers the URL constructor parse paths that avoid a UTF-8 copy and/or | ||
| 4 | + // reuse the input string when it is already a serialized ASCII href. | ||
| 5 | + | ||
| 6 | + const { hasIntl } = require('../common'); | ||
| 7 | + const assert = require('assert'); | ||
| 8 | + | ||
| 9 | + const alreadySerialized = [ | ||
| 10 | + 'https://nodejs.org/en/blog/', | ||
| 11 | + 'http://nodejs.org:89/docs/latest/api/foo/bar/qua/13949281/0f28b/' + | ||
| 12 | + '/5d49/b3020/url.html#test?payload1=true&payload2=false&test=1' + | ||
| 13 | + '&benchmark=3&foo=38.38.011.293&bar=1234834910480&test=19299&3992&' + | ||
| 14 | + 'key=f5c65e1e98fe07e648249ad41e1cfdb0', | ||
| 15 | + 'https://user:pass@example.com/path?search=1', | ||
| 16 | + 'file:///foo/bar/test/node.js', | ||
| 17 | + 'ws://localhost:9229/f46db715-70df-43ad-a359-7f9949f39868', | ||
| 18 | + ]; | ||
| 19 | + | ||
| 20 | + for (const href of alreadySerialized) { | ||
| 21 | + const url = new URL(href); | ||
| 22 | + assert.strictEqual(url.href, href); | ||
| 23 | + assert.strictEqual(URL.parse(href).href, href); | ||
| 24 | + assert.strictEqual(URL.canParse(href), true); | ||
| 25 | + } | ||
| 26 | + | ||
| 27 | + // Special-scheme URLs with an empty path gain a trailing slash. | ||
| 28 | + { | ||
| 29 | + const url = new URL('https://example.com'); | ||
| 30 | + assert.strictEqual(url.href, 'https://example.com/'); | ||
| 31 | + assert.strictEqual(url.pathname, '/'); | ||
| 32 | + } | ||
| 33 | + | ||
| 34 | + // Dot-segment normalization must still rewrite the path. | ||
| 35 | + { | ||
| 36 | + const url = new URL('https://example.org/./a/../b/./c'); | ||
| 37 | + assert.strictEqual(url.href, 'https://example.org/b/c'); | ||
| 38 | + assert.strictEqual(url.pathname, '/b/c'); | ||
| 39 | + } | ||
| 40 | + | ||
| 41 | + // Relative resolution against a base URL. | ||
| 42 | + { | ||
| 43 | + const url = new URL('/path?x=1#h', 'https://example.com:8443/base'); | ||
| 44 | + assert.strictEqual(url.href, 'https://example.com:8443/path?x=1#h'); | ||
| 45 | + assert.strictEqual(url.host, 'example.com:8443'); | ||
| 46 | + } | ||
| 47 | + | ||
| 48 | + // Non-string input is still stringified. | ||
| 49 | + { | ||
| 50 | + const url = new URL({ toString: () => 'https://example.com/from-object' }); | ||
| 51 | + assert.strictEqual(url.href, 'https://example.com/from-object'); | ||
| 52 | + } | ||
| 53 | + | ||
| 54 | + // Invalid input still throws from the constructor and is null from parse(). | ||
| 55 | + { | ||
| 56 | + assert.throws(() => new URL('not a url'), { | ||
| 57 | + code: 'ERR_INVALID_URL', | ||
| 58 | + name: 'TypeError', | ||
| 59 | + }); | ||
| 60 | + assert.strictEqual(URL.parse('not a url'), null); | ||
| 61 | + assert.strictEqual(URL.canParse('not a url'), false); | ||
| 62 | + } | ||
| 63 | + | ||
| 64 | + // Unpaired surrogates must not be returned as-is from href. | ||
| 65 | + { | ||
| 66 | + const input = 'https://example.com/\uD800'; | ||
| 67 | + const url = new URL(input); | ||
| 68 | + assert.notStrictEqual(url.href, input); | ||
| 69 | + assert.ok(url.href.startsWith('https://example.com/')); | ||
| 70 | + } | ||
| 71 | + | ||
| 72 | + if (hasIntl) { | ||
| 73 | + const url = new URL('http://你好你好.在线'); | ||
| 74 | + assert.ok(url.hostname.startsWith('xn--')); | ||
| 75 | + assert.ok(url.href.startsWith('http://xn--')); | ||
| 76 | + } | ||
| 77 | + | ||
| 78 | + // Setters re-parse the existing href; keep component updates correct. | ||
| 79 | + { | ||
| 80 | + const url = new URL('https://example.com/old'); | ||
| 81 | + url.pathname = '/new'; | ||
| 82 | + url.search = 'q=1'; | ||
| 83 | + url.hash = 'frag'; | ||
| 84 | + assert.strictEqual(url.href, 'https://example.com/new?q=1#frag'); | ||
| 85 | + assert.strictEqual(url.pathname, '/new'); | ||
| 86 | + assert.strictEqual(url.search, '?q=1'); | ||
| 87 | + assert.strictEqual(url.hash, '#frag'); | ||
| 88 | + } | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments