mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-25 06:24:27 +08:00
Add the legacy url API and url.domainToASCII/domainToUnicode (#973)
This commit is contained in:
@@ -56,6 +56,7 @@ A declaration in the standard library does not guarantee that an API compiles. U
|
||||
- **ESM metadata:** Native `import.meta.url`, `filename`, `dirname`, and `main` support dot access, statically known keys, and destructuring selected fields. Storing or mutating the metadata object, rest bindings, runtime keys, and stored `resolve` callbacks are unsupported. Native `import.meta.resolve()` requires a literal specifier. Module instances distinguished by URL query strings or fragments are unsupported.
|
||||
- **Date:** Date unions, year/month constructors, setters, statically typed TypeScript identity comparisons, thrown Date values, and locale/string formatters are unsupported. Zero-argument and single number/string constructors, storage and passing, `getTime`, `valueOf`, `toISOString`, calendar getters, `getTimezoneOffset`, and single-string `Date.parse` compile. JavaScript Dates retain identity through checked storage and support Date-copy construction and JSON serialization. See [Dates and timezones](#dates-and-timezones) for parsing restrictions.
|
||||
- **URL:** Non-ASCII or percent-escaped hosts are unsupported. Absolute inputs and relative inputs with runtime string or URL bases, including optional bases, compile. `URL.canParse`, `URL.parse`, `toString()`, and `toJSON()` compile; the factories use the same parser as construction, returning `false` or `null` for rejected inputs while preserving conversion errors. `href`, `protocol`, `username`, `password`, `host`, `hostname`, `port`, `pathname`, `search`, and `hash` support assignment in native code. Invalid component writes are ignored except for invalid `href` values, which throw; credentials are percent-encoded and ports accept valid leading digits. Native URL assignments require statically known property keys; `+=` accepts primitive operands, while other assignment operators and object operands for `+=` are refused. `origin` is read-only. `searchParams` is a live view whose identity survives query and URL replacement. Opaque paths retain their input bytes without WHATWG C0 encoding.
|
||||
- **Legacy URL API:** `url.parse()`, `url.format()`, `url.resolve()`, `url.domainToASCII()`, and `url.domainToUnicode()` compile, including Punycode hostnames and the `DEP0169`/`DEP0170` warnings. A parsed `Url` object supports reads, writes, and its `format()` method; its `parse()`, `resolve()`, `resolveObject()`, and `parseHost()` methods throw `SC2020`, and `url.Url` and `url.resolveObject()` are unsupported. `ERR_INVALID_URL` errors have no `input` property.
|
||||
- **Filesystem flags:** `fs.openSync` supports inline bitwise OR expressions of `O_RDONLY`, `O_WRONLY`, `O_RDWR`, `O_CREAT`, `O_EXCL`, `O_NOFOLLOW`, `O_NONBLOCK`, `O_TRUNC`, and `O_APPEND`, with an optional creation mode. Computed numeric flags are unsupported because bit values vary by target. `fstatSync`, `fchmodSync`, `fsyncSync`, and `linkSync` support direct calls. On Windows, numeric opens with `O_NOFOLLOW` and calls to `fchmodSync` throw `ENOSYS`; use a POSIX target if you require these operations.
|
||||
- **FileHandle and descriptor I/O:** Static reads and writes accept Buffer and Uint8Array windows, numeric positions, and UTF-8 strings. FileHandle reads also accept options records and allocate a buffer when omitted. Vector I/O, truncation, syncing, and permission changes compile. FileHandle streams, `readLines`, ownership changes, timestamps, and bigint Stats remain unsupported. Dynamic descriptor I/O accepts other ArrayBuffer views and Buffer encodings; read positions outside the safe integer range are refused. File operations complete synchronously before their callbacks or promise continuations. Descriptor permission changes on Windows throw `ENOSYS`.
|
||||
- **Crypto:** Hashing, HMAC, PBKDF2, and HKDF support MD5, SHA-1, SHA-224, SHA-256, SHA-384, and SHA-512. Scrypt supports its default parameters, numeric options, aliases, and memory limit. Both KDFs accept strings, Buffers, numeric typed arrays, DataViews, and fixed-length ArrayBuffers. KeyObject inputs, other digests, and cipher or public-key operations remain unsupported. Native derivations finish before their deferred callbacks, so computation blocks the caller.
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
"schemaVersion": 3,
|
||||
"nodeVersion": "24.15.0",
|
||||
"nodeCommit": "848430679556aed0bd073f2bc263331ad84fa119",
|
||||
"artifactVersion": "019292162568b058a19a",
|
||||
"artifactVersion": "f07d45bde0fcf3dfa325",
|
||||
"rowCount": 3662
|
||||
}
|
||||
|
||||
@@ -764,9 +764,9 @@
|
||||
"apiEntries": 62,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 19,
|
||||
"partial": 39,
|
||||
"refused": 0,
|
||||
"not-implemented": 43,
|
||||
"not-implemented": 23,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
@@ -67678,9 +67678,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -67701,9 +67701,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -67770,9 +67770,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -67839,9 +67839,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -67862,9 +67862,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -67885,9 +67885,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -67908,9 +67908,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -67931,9 +67931,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -67954,9 +67954,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -67977,9 +67977,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -68000,9 +68000,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -68023,9 +68023,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -68046,9 +68046,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -68069,9 +68069,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -68092,9 +68092,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -68115,9 +68115,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the documented scriptc module-loader subset.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -68138,9 +68138,9 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -68161,9 +68161,9 @@
|
||||
"inherited": false
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -68184,9 +68184,9 @@
|
||||
"inherited": false
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -68207,9 +68207,9 @@
|
||||
"inherited": false
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"detail": "No static compiler lowering is registered for this API yet.",
|
||||
"verification": "registry-gap"
|
||||
"status": "partial",
|
||||
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
|
||||
"verification": "test-backed"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
"tiers": {
|
||||
"static": {
|
||||
"replace-refusal": 40,
|
||||
"verify-gap": 2342,
|
||||
"audit-partial": 806,
|
||||
"verify-gap": 2322,
|
||||
"audit-partial": 826,
|
||||
"classify": 2,
|
||||
"implement": 80
|
||||
}
|
||||
@@ -68551,12 +68551,16 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:url",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "surface-manifest:node-builtin.url.domainToASCII",
|
||||
"tests": [
|
||||
"tests/corpus/5228-url-domain-conversion.ts",
|
||||
"tests/corpus/5229-url-legacy-errors.cjs",
|
||||
"packages/runtime/test/url-legacy.test.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -68576,12 +68580,16 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:url",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "surface-manifest:node-builtin.url.domainToUnicode",
|
||||
"tests": [
|
||||
"tests/corpus/5228-url-domain-conversion.ts",
|
||||
"tests/corpus/5229-url-legacy-errors.cjs",
|
||||
"packages/runtime/test/url-legacy.test.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -68661,12 +68669,19 @@
|
||||
"priority": "high",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:url",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "surface-manifest:node-builtin.url.format",
|
||||
"tests": [
|
||||
"tests/corpus/5224-url-format-objects.ts",
|
||||
"tests/corpus/5225-url-format-whatwg.ts",
|
||||
"tests/corpus/5227-url-legacy-dependency/main.ts",
|
||||
"tests/corpus/5229-url-legacy-errors.cjs",
|
||||
"packages/runtime/test/url-legacy.test.ts",
|
||||
"tests/fixtures/node-types/url-legacy.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -68741,12 +68756,18 @@
|
||||
"priority": "low",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:url",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:url.urlObject",
|
||||
"tests": [
|
||||
"tests/corpus/5221-url-parse-shapes.ts",
|
||||
"tests/corpus/5222-url-parse-hosts.ts",
|
||||
"tests/corpus/5223-url-parse-query.ts",
|
||||
"tests/corpus/5224-url-format-objects.ts",
|
||||
"tests/fixtures/node-types/url-legacy.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -68766,12 +68787,18 @@
|
||||
"priority": "low",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:url",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:url.urlObject.auth",
|
||||
"tests": [
|
||||
"tests/corpus/5221-url-parse-shapes.ts",
|
||||
"tests/corpus/5222-url-parse-hosts.ts",
|
||||
"tests/corpus/5223-url-parse-query.ts",
|
||||
"tests/corpus/5224-url-format-objects.ts",
|
||||
"tests/fixtures/node-types/url-legacy.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -68791,12 +68818,18 @@
|
||||
"priority": "low",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:url",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:url.urlObject.hash",
|
||||
"tests": [
|
||||
"tests/corpus/5221-url-parse-shapes.ts",
|
||||
"tests/corpus/5222-url-parse-hosts.ts",
|
||||
"tests/corpus/5223-url-parse-query.ts",
|
||||
"tests/corpus/5224-url-format-objects.ts",
|
||||
"tests/fixtures/node-types/url-legacy.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -68816,12 +68849,18 @@
|
||||
"priority": "low",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:url",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:url.urlObject.host",
|
||||
"tests": [
|
||||
"tests/corpus/5221-url-parse-shapes.ts",
|
||||
"tests/corpus/5222-url-parse-hosts.ts",
|
||||
"tests/corpus/5223-url-parse-query.ts",
|
||||
"tests/corpus/5224-url-format-objects.ts",
|
||||
"tests/fixtures/node-types/url-legacy.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -68841,12 +68880,18 @@
|
||||
"priority": "low",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:url",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:url.urlObject.hostname",
|
||||
"tests": [
|
||||
"tests/corpus/5221-url-parse-shapes.ts",
|
||||
"tests/corpus/5222-url-parse-hosts.ts",
|
||||
"tests/corpus/5223-url-parse-query.ts",
|
||||
"tests/corpus/5224-url-format-objects.ts",
|
||||
"tests/fixtures/node-types/url-legacy.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -68866,12 +68911,18 @@
|
||||
"priority": "low",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:url",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:url.urlObject.href",
|
||||
"tests": [
|
||||
"tests/corpus/5221-url-parse-shapes.ts",
|
||||
"tests/corpus/5222-url-parse-hosts.ts",
|
||||
"tests/corpus/5223-url-parse-query.ts",
|
||||
"tests/corpus/5224-url-format-objects.ts",
|
||||
"tests/fixtures/node-types/url-legacy.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -68891,12 +68942,18 @@
|
||||
"priority": "low",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:url",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:url.urlObject.path",
|
||||
"tests": [
|
||||
"tests/corpus/5221-url-parse-shapes.ts",
|
||||
"tests/corpus/5222-url-parse-hosts.ts",
|
||||
"tests/corpus/5223-url-parse-query.ts",
|
||||
"tests/corpus/5224-url-format-objects.ts",
|
||||
"tests/fixtures/node-types/url-legacy.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -68916,12 +68973,18 @@
|
||||
"priority": "low",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:url",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:url.urlObject.pathname",
|
||||
"tests": [
|
||||
"tests/corpus/5221-url-parse-shapes.ts",
|
||||
"tests/corpus/5222-url-parse-hosts.ts",
|
||||
"tests/corpus/5223-url-parse-query.ts",
|
||||
"tests/corpus/5224-url-format-objects.ts",
|
||||
"tests/fixtures/node-types/url-legacy.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -68941,12 +69004,18 @@
|
||||
"priority": "low",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:url",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:url.urlObject.port",
|
||||
"tests": [
|
||||
"tests/corpus/5221-url-parse-shapes.ts",
|
||||
"tests/corpus/5222-url-parse-hosts.ts",
|
||||
"tests/corpus/5223-url-parse-query.ts",
|
||||
"tests/corpus/5224-url-format-objects.ts",
|
||||
"tests/fixtures/node-types/url-legacy.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -68966,12 +69035,18 @@
|
||||
"priority": "low",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:url",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:url.urlObject.protocol",
|
||||
"tests": [
|
||||
"tests/corpus/5221-url-parse-shapes.ts",
|
||||
"tests/corpus/5222-url-parse-hosts.ts",
|
||||
"tests/corpus/5223-url-parse-query.ts",
|
||||
"tests/corpus/5224-url-format-objects.ts",
|
||||
"tests/fixtures/node-types/url-legacy.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -68991,12 +69066,18 @@
|
||||
"priority": "low",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:url",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:url.urlObject.query",
|
||||
"tests": [
|
||||
"tests/corpus/5221-url-parse-shapes.ts",
|
||||
"tests/corpus/5222-url-parse-hosts.ts",
|
||||
"tests/corpus/5223-url-parse-query.ts",
|
||||
"tests/corpus/5224-url-format-objects.ts",
|
||||
"tests/fixtures/node-types/url-legacy.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -69016,12 +69097,18 @@
|
||||
"priority": "low",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:url",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:url.urlObject.search",
|
||||
"tests": [
|
||||
"tests/corpus/5221-url-parse-shapes.ts",
|
||||
"tests/corpus/5222-url-parse-hosts.ts",
|
||||
"tests/corpus/5223-url-parse-query.ts",
|
||||
"tests/corpus/5224-url-format-objects.ts",
|
||||
"tests/fixtures/node-types/url-legacy.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -69041,12 +69128,18 @@
|
||||
"priority": "low",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:url",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "compiler-feature:url.urlObject.slashes",
|
||||
"tests": [
|
||||
"tests/corpus/5221-url-parse-shapes.ts",
|
||||
"tests/corpus/5222-url-parse-hosts.ts",
|
||||
"tests/corpus/5223-url-parse-query.ts",
|
||||
"tests/corpus/5224-url-format-objects.ts",
|
||||
"tests/fixtures/node-types/url-legacy.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -69066,12 +69159,19 @@
|
||||
"priority": "low",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:url",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "surface-manifest:node-builtin.url.format",
|
||||
"tests": [
|
||||
"tests/corpus/5224-url-format-objects.ts",
|
||||
"tests/corpus/5225-url-format-whatwg.ts",
|
||||
"tests/corpus/5227-url-legacy-dependency/main.ts",
|
||||
"tests/corpus/5229-url-legacy-errors.cjs",
|
||||
"packages/runtime/test/url-legacy.test.ts",
|
||||
"tests/fixtures/node-types/url-legacy.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -69091,12 +69191,19 @@
|
||||
"priority": "low",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:url",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "surface-manifest:node-builtin.url.format",
|
||||
"tests": [
|
||||
"tests/corpus/5224-url-format-objects.ts",
|
||||
"tests/corpus/5225-url-format-whatwg.ts",
|
||||
"tests/corpus/5227-url-legacy-dependency/main.ts",
|
||||
"tests/corpus/5229-url-legacy-errors.cjs",
|
||||
"packages/runtime/test/url-legacy.test.ts",
|
||||
"tests/fixtures/node-types/url-legacy.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -69116,12 +69223,21 @@
|
||||
"priority": "low",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:url",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "surface-manifest:node-builtin.url.parse",
|
||||
"tests": [
|
||||
"tests/corpus/5220-url-parse-warnings.ts",
|
||||
"tests/corpus/5221-url-parse-shapes.ts",
|
||||
"tests/corpus/5222-url-parse-hosts.ts",
|
||||
"tests/corpus/5223-url-parse-query.ts",
|
||||
"tests/corpus/5227-url-legacy-dependency/main.ts",
|
||||
"tests/corpus/5229-url-legacy-errors.cjs",
|
||||
"packages/runtime/test/url-legacy.test.ts",
|
||||
"tests/fixtures/node-types/url-legacy.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -69141,12 +69257,18 @@
|
||||
"priority": "low",
|
||||
"tiers": {
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"action": "verify-gap",
|
||||
"verification": "registry-gap",
|
||||
"confidence": "low",
|
||||
"source": "compiler-unmatched:url",
|
||||
"tests": []
|
||||
"status": "partial",
|
||||
"action": "audit-partial",
|
||||
"verification": "test-backed",
|
||||
"confidence": "high",
|
||||
"source": "surface-manifest:node-builtin.url.resolve",
|
||||
"tests": [
|
||||
"tests/corpus/5226-url-resolve.ts",
|
||||
"tests/corpus/5227-url-legacy-dependency/main.ts",
|
||||
"tests/corpus/5229-url-legacy-errors.cjs",
|
||||
"packages/runtime/test/url-legacy.test.ts",
|
||||
"tests/fixtures/node-types/url-legacy.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -821,9 +821,9 @@
|
||||
"apiEntries": 62,
|
||||
"static": {
|
||||
"supported": 0,
|
||||
"partial": 19,
|
||||
"partial": 39,
|
||||
"refused": 0,
|
||||
"not-implemented": 43,
|
||||
"not-implemented": 23,
|
||||
"by-design": 0,
|
||||
"unreviewed": 0,
|
||||
"not-applicable": 0
|
||||
@@ -26629,8 +26629,18 @@
|
||||
"anchor": "dep0170-invalid-port-when-using-urlparse",
|
||||
"nodeStability": null,
|
||||
"static": {
|
||||
"status": "not-applicable",
|
||||
"evidence": "compiler-chapter-policy:deprecations"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.url.parse",
|
||||
"tests": [
|
||||
"tests/corpus/5220-url-parse-warnings.ts",
|
||||
"tests/corpus/5221-url-parse-shapes.ts",
|
||||
"tests/corpus/5222-url-parse-hosts.ts",
|
||||
"tests/corpus/5223-url-parse-query.ts",
|
||||
"tests/corpus/5227-url-legacy-dependency/main.ts",
|
||||
"tests/corpus/5229-url-legacy-errors.cjs",
|
||||
"packages/runtime/test/url-legacy.test.ts",
|
||||
"tests/fixtures/node-types/url-legacy.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -90908,6 +90918,17 @@
|
||||
"status": "partial",
|
||||
"evidence": "derived:descendants",
|
||||
"tests": [
|
||||
"packages/runtime/test/url-legacy.test.ts",
|
||||
"tests/corpus/5220-url-parse-warnings.ts",
|
||||
"tests/corpus/5221-url-parse-shapes.ts",
|
||||
"tests/corpus/5222-url-parse-hosts.ts",
|
||||
"tests/corpus/5223-url-parse-query.ts",
|
||||
"tests/corpus/5224-url-format-objects.ts",
|
||||
"tests/corpus/5225-url-format-whatwg.ts",
|
||||
"tests/corpus/5226-url-resolve.ts",
|
||||
"tests/corpus/5227-url-legacy-dependency/main.ts",
|
||||
"tests/corpus/5228-url-domain-conversion.ts",
|
||||
"tests/corpus/5229-url-legacy-errors.cjs",
|
||||
"tests/corpus/node24-file-url-buffer.ts",
|
||||
"tests/corpus/node24-file-url-errors.js",
|
||||
"tests/corpus/node24-file-url-options.ts",
|
||||
@@ -90920,6 +90941,7 @@
|
||||
"tests/corpus/url-runtime-base.ts",
|
||||
"tests/fixtures/node-types/file-url-conversion.ts",
|
||||
"tests/fixtures/node-types/url-factories.ts",
|
||||
"tests/fixtures/node-types/url-legacy.ts",
|
||||
"tests/fixtures/node-types/url-mutation.ts",
|
||||
"tests/harness/project-config.test.ts"
|
||||
]
|
||||
@@ -91967,8 +91989,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:url"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.url.domainToASCII",
|
||||
"tests": [
|
||||
"tests/corpus/5228-url-domain-conversion.ts",
|
||||
"tests/corpus/5229-url-legacy-errors.cjs",
|
||||
"packages/runtime/test/url-legacy.test.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -91990,8 +92017,13 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:url"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.url.domainToUnicode",
|
||||
"tests": [
|
||||
"tests/corpus/5228-url-domain-conversion.ts",
|
||||
"tests/corpus/5229-url-legacy-errors.cjs",
|
||||
"packages/runtime/test/url-legacy.test.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -92071,8 +92103,16 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:url"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.url.format",
|
||||
"tests": [
|
||||
"tests/corpus/5224-url-format-objects.ts",
|
||||
"tests/corpus/5225-url-format-whatwg.ts",
|
||||
"tests/corpus/5227-url-legacy-dependency/main.ts",
|
||||
"tests/corpus/5229-url-legacy-errors.cjs",
|
||||
"packages/runtime/test/url-legacy.test.ts",
|
||||
"tests/fixtures/node-types/url-legacy.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -92169,8 +92209,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:url"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:url.urlObject",
|
||||
"tests": [
|
||||
"tests/corpus/5221-url-parse-shapes.ts",
|
||||
"tests/corpus/5222-url-parse-hosts.ts",
|
||||
"tests/corpus/5223-url-parse-query.ts",
|
||||
"tests/corpus/5224-url-format-objects.ts",
|
||||
"tests/fixtures/node-types/url-legacy.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -92192,8 +92239,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:url"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:url.urlObject.auth",
|
||||
"tests": [
|
||||
"tests/corpus/5221-url-parse-shapes.ts",
|
||||
"tests/corpus/5222-url-parse-hosts.ts",
|
||||
"tests/corpus/5223-url-parse-query.ts",
|
||||
"tests/corpus/5224-url-format-objects.ts",
|
||||
"tests/fixtures/node-types/url-legacy.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -92215,8 +92269,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:url"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:url.urlObject.hash",
|
||||
"tests": [
|
||||
"tests/corpus/5221-url-parse-shapes.ts",
|
||||
"tests/corpus/5222-url-parse-hosts.ts",
|
||||
"tests/corpus/5223-url-parse-query.ts",
|
||||
"tests/corpus/5224-url-format-objects.ts",
|
||||
"tests/fixtures/node-types/url-legacy.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -92238,8 +92299,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:url"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:url.urlObject.host",
|
||||
"tests": [
|
||||
"tests/corpus/5221-url-parse-shapes.ts",
|
||||
"tests/corpus/5222-url-parse-hosts.ts",
|
||||
"tests/corpus/5223-url-parse-query.ts",
|
||||
"tests/corpus/5224-url-format-objects.ts",
|
||||
"tests/fixtures/node-types/url-legacy.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -92261,8 +92329,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:url"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:url.urlObject.hostname",
|
||||
"tests": [
|
||||
"tests/corpus/5221-url-parse-shapes.ts",
|
||||
"tests/corpus/5222-url-parse-hosts.ts",
|
||||
"tests/corpus/5223-url-parse-query.ts",
|
||||
"tests/corpus/5224-url-format-objects.ts",
|
||||
"tests/fixtures/node-types/url-legacy.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -92284,8 +92359,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:url"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:url.urlObject.href",
|
||||
"tests": [
|
||||
"tests/corpus/5221-url-parse-shapes.ts",
|
||||
"tests/corpus/5222-url-parse-hosts.ts",
|
||||
"tests/corpus/5223-url-parse-query.ts",
|
||||
"tests/corpus/5224-url-format-objects.ts",
|
||||
"tests/fixtures/node-types/url-legacy.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -92307,8 +92389,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:url"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:url.urlObject.path",
|
||||
"tests": [
|
||||
"tests/corpus/5221-url-parse-shapes.ts",
|
||||
"tests/corpus/5222-url-parse-hosts.ts",
|
||||
"tests/corpus/5223-url-parse-query.ts",
|
||||
"tests/corpus/5224-url-format-objects.ts",
|
||||
"tests/fixtures/node-types/url-legacy.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -92330,8 +92419,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:url"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:url.urlObject.pathname",
|
||||
"tests": [
|
||||
"tests/corpus/5221-url-parse-shapes.ts",
|
||||
"tests/corpus/5222-url-parse-hosts.ts",
|
||||
"tests/corpus/5223-url-parse-query.ts",
|
||||
"tests/corpus/5224-url-format-objects.ts",
|
||||
"tests/fixtures/node-types/url-legacy.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -92353,8 +92449,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:url"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:url.urlObject.port",
|
||||
"tests": [
|
||||
"tests/corpus/5221-url-parse-shapes.ts",
|
||||
"tests/corpus/5222-url-parse-hosts.ts",
|
||||
"tests/corpus/5223-url-parse-query.ts",
|
||||
"tests/corpus/5224-url-format-objects.ts",
|
||||
"tests/fixtures/node-types/url-legacy.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -92376,8 +92479,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:url"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:url.urlObject.protocol",
|
||||
"tests": [
|
||||
"tests/corpus/5221-url-parse-shapes.ts",
|
||||
"tests/corpus/5222-url-parse-hosts.ts",
|
||||
"tests/corpus/5223-url-parse-query.ts",
|
||||
"tests/corpus/5224-url-format-objects.ts",
|
||||
"tests/fixtures/node-types/url-legacy.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -92399,8 +92509,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:url"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:url.urlObject.query",
|
||||
"tests": [
|
||||
"tests/corpus/5221-url-parse-shapes.ts",
|
||||
"tests/corpus/5222-url-parse-hosts.ts",
|
||||
"tests/corpus/5223-url-parse-query.ts",
|
||||
"tests/corpus/5224-url-format-objects.ts",
|
||||
"tests/fixtures/node-types/url-legacy.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -92422,8 +92539,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:url"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:url.urlObject.search",
|
||||
"tests": [
|
||||
"tests/corpus/5221-url-parse-shapes.ts",
|
||||
"tests/corpus/5222-url-parse-hosts.ts",
|
||||
"tests/corpus/5223-url-parse-query.ts",
|
||||
"tests/corpus/5224-url-format-objects.ts",
|
||||
"tests/fixtures/node-types/url-legacy.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -92445,8 +92569,15 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:url"
|
||||
"status": "partial",
|
||||
"evidence": "compiler-feature:url.urlObject.slashes",
|
||||
"tests": [
|
||||
"tests/corpus/5221-url-parse-shapes.ts",
|
||||
"tests/corpus/5222-url-parse-hosts.ts",
|
||||
"tests/corpus/5223-url-parse-query.ts",
|
||||
"tests/corpus/5224-url-format-objects.ts",
|
||||
"tests/fixtures/node-types/url-legacy.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -92468,8 +92599,16 @@
|
||||
"inherited": true
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:url"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.url.format",
|
||||
"tests": [
|
||||
"tests/corpus/5224-url-format-objects.ts",
|
||||
"tests/corpus/5225-url-format-whatwg.ts",
|
||||
"tests/corpus/5227-url-legacy-dependency/main.ts",
|
||||
"tests/corpus/5229-url-legacy-errors.cjs",
|
||||
"packages/runtime/test/url-legacy.test.ts",
|
||||
"tests/fixtures/node-types/url-legacy.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -92491,8 +92630,16 @@
|
||||
"inherited": false
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:url"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.url.format",
|
||||
"tests": [
|
||||
"tests/corpus/5224-url-format-objects.ts",
|
||||
"tests/corpus/5225-url-format-whatwg.ts",
|
||||
"tests/corpus/5227-url-legacy-dependency/main.ts",
|
||||
"tests/corpus/5229-url-legacy-errors.cjs",
|
||||
"packages/runtime/test/url-legacy.test.ts",
|
||||
"tests/fixtures/node-types/url-legacy.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -92514,8 +92661,18 @@
|
||||
"inherited": false
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:url"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.url.parse",
|
||||
"tests": [
|
||||
"tests/corpus/5220-url-parse-warnings.ts",
|
||||
"tests/corpus/5221-url-parse-shapes.ts",
|
||||
"tests/corpus/5222-url-parse-hosts.ts",
|
||||
"tests/corpus/5223-url-parse-query.ts",
|
||||
"tests/corpus/5227-url-legacy-dependency/main.ts",
|
||||
"tests/corpus/5229-url-legacy-errors.cjs",
|
||||
"packages/runtime/test/url-legacy.test.ts",
|
||||
"tests/fixtures/node-types/url-legacy.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
@@ -92537,8 +92694,15 @@
|
||||
"inherited": false
|
||||
},
|
||||
"static": {
|
||||
"status": "not-implemented",
|
||||
"evidence": "compiler-unmatched:url"
|
||||
"status": "partial",
|
||||
"evidence": "surface-manifest:node-builtin.url.resolve",
|
||||
"tests": [
|
||||
"tests/corpus/5226-url-resolve.ts",
|
||||
"tests/corpus/5227-url-legacy-dependency/main.ts",
|
||||
"tests/corpus/5229-url-legacy-errors.cjs",
|
||||
"packages/runtime/test/url-legacy.test.ts",
|
||||
"tests/fixtures/node-types/url-legacy.ts"
|
||||
]
|
||||
},
|
||||
"anchorSource": "exact"
|
||||
},
|
||||
|
||||
@@ -93,6 +93,11 @@
|
||||
"node-builtin.os.uptime": ["tests/corpus/5140-os-host-facts.ts"],
|
||||
"node-builtin.os.version": ["tests/corpus/5140-os-host-facts.ts"],
|
||||
"node-builtin.os.constants": ["tests/corpus/5141-os-constants.ts"],
|
||||
"node-builtin.url.parse": ["tests/corpus/5220-url-parse-warnings.ts", "tests/corpus/5221-url-parse-shapes.ts", "tests/corpus/5222-url-parse-hosts.ts", "tests/corpus/5223-url-parse-query.ts", "tests/corpus/5227-url-legacy-dependency/main.ts", "tests/corpus/5229-url-legacy-errors.cjs", "packages/runtime/test/url-legacy.test.ts", "tests/fixtures/node-types/url-legacy.ts"],
|
||||
"node-builtin.url.format": ["tests/corpus/5224-url-format-objects.ts", "tests/corpus/5225-url-format-whatwg.ts", "tests/corpus/5227-url-legacy-dependency/main.ts", "tests/corpus/5229-url-legacy-errors.cjs", "packages/runtime/test/url-legacy.test.ts", "tests/fixtures/node-types/url-legacy.ts"],
|
||||
"node-builtin.url.resolve": ["tests/corpus/5226-url-resolve.ts", "tests/corpus/5227-url-legacy-dependency/main.ts", "tests/corpus/5229-url-legacy-errors.cjs", "packages/runtime/test/url-legacy.test.ts", "tests/fixtures/node-types/url-legacy.ts"],
|
||||
"node-builtin.url.domainToASCII": ["tests/corpus/5228-url-domain-conversion.ts", "tests/corpus/5229-url-legacy-errors.cjs", "packages/runtime/test/url-legacy.test.ts"],
|
||||
"node-builtin.url.domainToUnicode": ["tests/corpus/5228-url-domain-conversion.ts", "tests/corpus/5229-url-legacy-errors.cjs", "packages/runtime/test/url-legacy.test.ts"],
|
||||
"node-builtin.path": ["tests/corpus/957-builtins-namespace.ts", "tests/corpus/2915-path-function-values.ts", "tests/corpus/2916-path-function-values-cjs.ts", "tests/corpus/2917-builtin-callable-widths.ts", "tests/corpus/2918-builtin-callable-widths-cjs.ts"],
|
||||
"node-builtin.path.parse": ["tests/corpus/5142-path-parse-format.ts", "packages/runtime/test/path.test.ts"],
|
||||
"node-builtin.path.format": ["tests/corpus/5142-path-parse-format.ts", "packages/runtime/test/path.test.ts"],
|
||||
@@ -269,6 +274,7 @@
|
||||
{ "chapter": "url", "parent": "Class: `URL`", "signatures": ["`protocol` Type: {string}", "`username` Type: {string}", "`password` Type: {string}", "`host` Type: {string}", "`hostname` Type: {string}", "`port` Type: {string}"], "status": "partial", "evidence": ["tests/corpus/node24-url-authority.ts", "tests/corpus/node24-url-authority-coercion.js", "tests/fixtures/node-types/url-mutation.ts"] },
|
||||
{ "chapter": "url", "parent": "Class: `URL`", "signatures": ["`href` Type: {string}", "`pathname` Type: {string}", "`search` Type: {string}", "`hash` Type: {string}"], "status": "partial", "evidence": ["tests/corpus/node24-url-mutation.ts", "tests/corpus/node24-url-mutation-coercion.js", "tests/fixtures/node-types/url-mutation.ts"] },
|
||||
{ "chapter": "url", "signatures": ["`port` Type: {string}"], "status": "partial", "evidence": ["tests/corpus/2946-url-port-hash.ts"] },
|
||||
{ "chapter": "url", "signatures": ["Legacy `urlObject`", "`urlObject.auth`", "`urlObject.hash`", "`urlObject.host`", "`urlObject.hostname`", "`urlObject.href`", "`urlObject.path`", "`urlObject.pathname`", "`urlObject.port`", "`urlObject.protocol`", "`urlObject.query`", "`urlObject.search`", "`urlObject.slashes`"], "status": "partial", "evidence": ["tests/corpus/5221-url-parse-shapes.ts", "tests/corpus/5222-url-parse-hosts.ts", "tests/corpus/5223-url-parse-query.ts", "tests/corpus/5224-url-format-objects.ts", "tests/fixtures/node-types/url-legacy.ts"] },
|
||||
{ "chapter": "util", "signatures": ["`util.promisify(original)`"], "status": "partial", "evidence": ["tests/corpus/1473-promisify-execfile.ts", "tests/corpus/1573-promisify-execfile-env-spread.ts", "tests/corpus/2912-promisify-fs-readfile.ts"] },
|
||||
{ "chapter": "typescript", "symbols": ["type"], "status": "supported", "evidence": ["tests/corpus/2702-type-import-link-static/main.ts", "tests/corpus/2703-type-import-link-static-reexport/main.ts", "tests/corpus/2704-type-import-link-static-success/main.ts", "tests/corpus/2707-type-import-link-static-default-reexport/main.ts", "tests/harness/errors.test.ts"] }
|
||||
],
|
||||
|
||||
+63
-1
@@ -2029,7 +2029,10 @@ declare var URLSearchParams: {
|
||||
/* node:url — the file-URL bridge pair. fileURLToPath accepts a URL value
|
||||
* or a URL string and throws Node's TypeErrors (non-file scheme, encoded
|
||||
* slashes, non-empty host); pathToFileURL resolves the path and percent-
|
||||
* encodes it into a file: URL. */
|
||||
* encodes it into a file: URL. The legacy API: parse() answers a Url
|
||||
* object, format() serializes a Url-shaped object, a string, or a URL,
|
||||
* resolve() resolves one URL string against another, and the domain pair
|
||||
* converts a hostname to and from its Punycode form. */
|
||||
declare module "url" {
|
||||
export type URL = globalThis.URL;
|
||||
export type URLSearchParams = globalThis.URLSearchParams;
|
||||
@@ -2038,6 +2041,65 @@ declare module "url" {
|
||||
export function fileURLToPath(url: string | URL, options?: { windows?: boolean }): string;
|
||||
export function fileURLToPathBuffer(url: string | URL, options?: { windows?: boolean }): Buffer;
|
||||
export function pathToFileURL(path: string, options?: { windows?: boolean }): URL;
|
||||
export interface UrlObject {
|
||||
auth?: string | null | undefined;
|
||||
hash?: string | null | undefined;
|
||||
host?: string | null | undefined;
|
||||
hostname?: string | null | undefined;
|
||||
href?: string | null | undefined;
|
||||
pathname?: string | null | undefined;
|
||||
protocol?: string | null | undefined;
|
||||
search?: string | null | undefined;
|
||||
slashes?: boolean | null | undefined;
|
||||
port?: string | number | null | undefined;
|
||||
query?: string | null | import("querystring").ParsedUrlQueryInput | undefined;
|
||||
}
|
||||
export interface Url {
|
||||
auth: string | null;
|
||||
hash: string | null;
|
||||
host: string | null;
|
||||
hostname: string | null;
|
||||
href: string;
|
||||
path: string | null;
|
||||
pathname: string | null;
|
||||
protocol: string | null;
|
||||
search: string | null;
|
||||
slashes: boolean | null;
|
||||
port: string | null;
|
||||
query: string | null | import("querystring").ParsedUrlQuery;
|
||||
}
|
||||
export interface UrlWithParsedQuery extends Url {
|
||||
query: import("querystring").ParsedUrlQuery;
|
||||
}
|
||||
export interface UrlWithStringQuery extends Url {
|
||||
query: string | null;
|
||||
}
|
||||
export interface URLFormatOptions {
|
||||
auth?: boolean | undefined;
|
||||
fragment?: boolean | undefined;
|
||||
search?: boolean | undefined;
|
||||
unicode?: boolean | undefined;
|
||||
}
|
||||
export function parse(
|
||||
urlString: string,
|
||||
parseQueryString?: false,
|
||||
slashesDenoteHost?: boolean,
|
||||
): UrlWithStringQuery;
|
||||
export function parse(
|
||||
urlString: string,
|
||||
parseQueryString: true,
|
||||
slashesDenoteHost?: boolean,
|
||||
): UrlWithParsedQuery;
|
||||
export function parse(
|
||||
urlString: string,
|
||||
parseQueryString: boolean,
|
||||
slashesDenoteHost?: boolean,
|
||||
): Url;
|
||||
export function format(urlObject: URL, options?: URLFormatOptions): string;
|
||||
export function format(urlObject: UrlObject | string): string;
|
||||
export function resolve(from: string, to: string): string;
|
||||
export function domainToASCII(domain: string): string;
|
||||
export function domainToUnicode(domain: string): string;
|
||||
}
|
||||
declare module "node:url" {
|
||||
export * from "url";
|
||||
|
||||
@@ -525,6 +525,11 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"url.fileURLToPathBuffer": "scr_url_checked_to_path_buffer",
|
||||
"url.pathToFileURLChecked": "scr_url_checked_from_path",
|
||||
"url.fileURLToPathStr": "scr_url_str_to_path",
|
||||
"url.legacyParse": "scr_url_legacy_parse",
|
||||
"url.legacyFormat": "scr_url_legacy_format",
|
||||
"url.legacyResolve": "scr_url_legacy_resolve",
|
||||
"url.domainToASCII": "scr_url_domain_to_ascii",
|
||||
"url.domainToUnicode": "scr_url_domain_to_unicode",
|
||||
"url.pathToFileURL": "scr_url_from_path",
|
||||
"url.pathToFileURLPlatform": "scr_url_from_path_platform",
|
||||
"url.pathToFileURLWin32": "scr_url_from_path",
|
||||
@@ -1126,6 +1131,10 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
export const USES_TIMERS_LIB_FNS = new Set<string>([
|
||||
"process.emitWarning",
|
||||
"process.emitWarningText",
|
||||
// The legacy url entries queue DEP0169/DEP0170 on the next tick.
|
||||
"url.legacyParse",
|
||||
"url.legacyFormat",
|
||||
"url.legacyResolve",
|
||||
// The wrappers schedule warnings and callbacks on the tick queue.
|
||||
"util.deprecate",
|
||||
"util.callbackify",
|
||||
|
||||
@@ -480,14 +480,16 @@ export async function compileCInternal(
|
||||
// SCR_MUSL flag and this target-selected TU travel together.
|
||||
...(isMuslTarget(driver) ? [rt(join(rtDir, "scr_musl.c"))] : []),
|
||||
// scr_glob.c (path.matchesGlob) compiles its segments through the
|
||||
// regex unit, so it rides the same switch; section GC drops it from
|
||||
// binaries that never match a glob.
|
||||
// regex unit and scr_idna.c (node:url's IDNA) normalizes through it,
|
||||
// so both ride the same switch; section GC drops them from binaries
|
||||
// that never use them.
|
||||
...(regex
|
||||
? [
|
||||
"-I",
|
||||
vendorEngineDir(),
|
||||
rt(join(rtDir, "scr_regex.c")),
|
||||
rt(join(rtDir, "scr_glob.c")),
|
||||
rt(join(rtDir, "scr_idna.c")),
|
||||
...lreObjects,
|
||||
]
|
||||
: []),
|
||||
@@ -526,6 +528,9 @@ export async function compileCInternal(
|
||||
...(opts.symbol ? [rt(join(rtDir, "scr_symbol.c"))] : []),
|
||||
...(opts.assert && opts.bigint ? [rt(join(rtDir, "scr_bigint_assert.c"))] : []),
|
||||
...(opts.qs ? [rt(join(rtDir, "scr_qs.c"))] : []),
|
||||
// The legacy url unit references the regex, querystring and dyn-async
|
||||
// units; the url.parse family's IR sets all three.
|
||||
...(regex && opts.qs && opts.dynAsync ? [rt(join(rtDir, "scr_url_legacy.c"))] : []),
|
||||
...(opts.parseArgs ? [rt(join(rtDir, "scr_util.c"))] : []),
|
||||
...(opts.parseArgs && opts.symbol ? [rt(join(rtDir, "scr_util_compare.c"))] : []),
|
||||
...(opts.parseArgs && opts.inspect && opts.dynAsync
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { lowerFileUrlCall } from "./url.js";
|
||||
import { lowerFileUrlCall, lowerLegacyUrlCall } from "./url.js";
|
||||
import { lowerReadlineCreateCall } from "./readline.js";
|
||||
import { lowerDiagnosticsChannelCall } from "./async-context.js";
|
||||
import { lowerTimersPromisesCall } from "./timers.js";
|
||||
@@ -167,6 +167,12 @@ export function lowerBuiltinModuleCall(
|
||||
return lowerQuerystringStringifyCall(lowerer, expr, loc);
|
||||
}
|
||||
}
|
||||
if (
|
||||
bi.module === "url" &&
|
||||
["parse", "format", "resolve", "domainToASCII", "domainToUnicode"].includes(bi.member)
|
||||
) {
|
||||
return lowerLegacyUrlCall(lowerer, expr, bi, fn, loc);
|
||||
}
|
||||
if (bi.module === "timers/promises") {
|
||||
const lowered = lowerTimersPromisesCall(lowerer, expr, bi, loc);
|
||||
if (lowered) return lowered;
|
||||
|
||||
@@ -702,3 +702,52 @@ export function lowerFileUrlCall(
|
||||
);
|
||||
return { kind: "libCall", fn: fn.fn, args, type: fn.result, loc };
|
||||
}
|
||||
|
||||
/** Node's isInsideNodeModules test on the calling file: url.parse()'s
|
||||
* DEP0169 warning skips calls made from dependencies. */
|
||||
function insideNodeModules(file: string): boolean {
|
||||
return /[/\\]node_modules[/\\]/.test(file);
|
||||
}
|
||||
|
||||
/** node:url's legacy API and the domain pair. Every argument crosses as a
|
||||
* checked-dynamic value so the runtime applies Node's own validation
|
||||
* (validateString, the urlObject type ladder, String() for the domain);
|
||||
* parse/format/resolve also carry whether this call site emits DEP0169. */
|
||||
export function lowerLegacyUrlCall(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.CallExpression,
|
||||
bi: { module: string; member: string },
|
||||
fn: BuiltinModuleFn,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const arity =
|
||||
bi.member === "parse" ? 3 : bi.member === "format" || bi.member === "resolve" ? 2 : 1;
|
||||
if (expr.arguments.some(ts.isSpreadElement) || expr.arguments.length > arity) {
|
||||
lowerer.noLowering(
|
||||
`url.${bi.member} with these arguments`,
|
||||
expr,
|
||||
`pass at most ${arity} argument${arity === 1 ? "" : "s"} directly`,
|
||||
);
|
||||
}
|
||||
const args: IrExpr[] = [];
|
||||
for (let index = 0; index < arity; index++) {
|
||||
let arg = expr.arguments[index];
|
||||
while (arg && ts.isParenthesizedExpression(arg)) arg = arg.expression;
|
||||
// An inline urlObject or options literal builds as the checked-dynamic
|
||||
// object format() reads (nested query objects included).
|
||||
args.push(
|
||||
!arg
|
||||
? dynUndefinedExpr(loc)
|
||||
: ts.isObjectLiteralExpression(arg)
|
||||
? lowerDynObjectLiteral(lowerer, arg)
|
||||
: lowerer.lowerExprExpecting(arg, DYN),
|
||||
);
|
||||
}
|
||||
if (bi.member === "domainToASCII" || bi.member === "domainToUnicode") {
|
||||
args.push({ kind: "boolLit", value: expr.arguments.length === 0, type: BOOL, loc });
|
||||
} else {
|
||||
const warn = !insideNodeModules(expr.getSourceFile().fileName);
|
||||
args.push({ kind: "boolLit", value: warn, type: BOOL, loc });
|
||||
}
|
||||
return { kind: "libCall", fn: fn.fn, args, type: fn.result, loc };
|
||||
}
|
||||
|
||||
@@ -157,6 +157,7 @@ import { mixinFnOfCallee } from "./lower-mixins.js";
|
||||
import {
|
||||
isConstAssertionTypeNode,
|
||||
isGenericCallableMemberType,
|
||||
isLegacyUrlSymbol,
|
||||
isOsConstantsType,
|
||||
isParseArgsDynTypeName,
|
||||
underConstAssertion,
|
||||
@@ -2436,7 +2437,8 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
|
||||
isParseArgsDynProperty(lowerer, expr) ||
|
||||
isStatFsDynProperty(lowerer, expr) ||
|
||||
segmentDataProperty ||
|
||||
osConstants
|
||||
osConstants ||
|
||||
isLegacyUrlSymbol(lowerer.checker.getSymbolAtLocation(expr.name), lowerer)
|
||||
) {
|
||||
const recv = lowerer.lowerExpr(expr.expression);
|
||||
if (recv.type.kind === "dyn") {
|
||||
@@ -8517,7 +8519,24 @@ export function lowerAsExpression(
|
||||
);
|
||||
}
|
||||
const boxed = lowerer.coerceInto(expr, inner, DYN);
|
||||
return boxed.kind === "dynFrom" ? { ...boxed, liveRef: true } : boxed;
|
||||
// Only references box live; a scalar or unit source (`value as
|
||||
// unknown as View`) is an ordinary snapshot.
|
||||
const seen = new Set<string>();
|
||||
const live = (type: IrType): boolean => {
|
||||
if (
|
||||
type.kind === "record" ||
|
||||
type.kind === "array" ||
|
||||
type.kind === "bytes" ||
|
||||
isDynTypedRefType(type)
|
||||
)
|
||||
return true;
|
||||
if (type.kind !== "union" || seen.has(type.unionId)) return false;
|
||||
seen.add(type.unionId);
|
||||
return lowerer.unions.get(type.unionId)?.arms.some(live) ?? false;
|
||||
};
|
||||
return boxed.kind === "dynFrom" && live(boxed.value.type)
|
||||
? { ...boxed, liveRef: true }
|
||||
: boxed;
|
||||
}
|
||||
// Collection assertions are interface views only: their backing
|
||||
// value must already have the same native key/element/value layout.
|
||||
|
||||
@@ -8460,6 +8460,8 @@ export class Lowerer {
|
||||
builtinOption("RequestInit") ||
|
||||
builtinOption("ErrorOptions") ||
|
||||
builtinOption("SpawnOptions") ||
|
||||
builtinOption("UrlObject") ||
|
||||
builtinOption("URLFormatOptions") ||
|
||||
!this.mapTypeOf(this.typeOf(x))
|
||||
) {
|
||||
return lowerDynObjectLiteral(this, x);
|
||||
|
||||
@@ -1407,6 +1407,14 @@ export const BUILTIN_MODULE_FNS: Record<
|
||||
{ mode: "optional", type: DYN, defaultValue: undefined },
|
||||
],
|
||||
},
|
||||
// The legacy API and the domain pair (lowerLegacyUrlCall): arguments
|
||||
// cross as dyn values for Node's own validators; url.parse() answers
|
||||
// the checked-dynamic Url instance.
|
||||
parse: { fn: "url.legacyParse", params: [DYN, DYN, DYN], result: DYN },
|
||||
format: { fn: "url.legacyFormat", params: [DYN, DYN], result: STRING },
|
||||
resolve: { fn: "url.legacyResolve", params: [DYN, DYN], result: STRING },
|
||||
domainToASCII: { fn: "url.domainToASCII", params: [DYN], result: STRING },
|
||||
domainToUnicode: { fn: "url.domainToUnicode", params: [DYN], result: STRING },
|
||||
},
|
||||
child_process: {
|
||||
// spawnSync's and spawn's call completions are entirely special-cased
|
||||
|
||||
@@ -1059,6 +1059,48 @@ let mapTypeDepth = 0;
|
||||
* stay fenced instead of interning a per-context-wrong shape). */
|
||||
let contextResolutions = 0;
|
||||
|
||||
/** node:url's legacy record interfaces: url.parse()'s Url instance (and
|
||||
* its two query-typed refinements), url.format()'s UrlObject input, and the
|
||||
* URL form's URLFormatOptions. The values are checked-dynamic objects —
|
||||
* Node's Url instance carries its class name and null fields, and format()
|
||||
* reads any object's fields with JS truthiness. */
|
||||
const LEGACY_URL_TYPES = new Set([
|
||||
"Url",
|
||||
"UrlObject",
|
||||
"UrlWithParsedQuery",
|
||||
"UrlWithStringQuery",
|
||||
"URLFormatOptions",
|
||||
]);
|
||||
|
||||
export function isLegacyUrlType(
|
||||
type: ts.Type,
|
||||
ctx: Pick<TypeMapperCtx, "checker" | "isStdlibFile">,
|
||||
): boolean {
|
||||
const symbol = type.getAliasSymbol() ?? type.getSymbol();
|
||||
return isLegacyUrlSymbol(symbol, ctx);
|
||||
}
|
||||
|
||||
/** A symbol declared as one of node:url's legacy record interfaces, or as
|
||||
* a member of one. */
|
||||
export function isLegacyUrlSymbol(
|
||||
symbol: ts.Symbol | null | undefined,
|
||||
ctx: Pick<TypeMapperCtx, "checker" | "isStdlibFile">,
|
||||
): boolean {
|
||||
if (!symbol) return false;
|
||||
for (const d of ctx.checker.declarationsOf(symbol)) {
|
||||
if (!ctx.isStdlibFile(d.getSourceFile())) continue;
|
||||
let family = false;
|
||||
for (let n: ts.Node | undefined = d; n; n = n.parent) {
|
||||
if (ts.isInterfaceDeclaration(n) && LEGACY_URL_TYPES.has(n.name.text)) family = true;
|
||||
if (ts.isModuleDeclaration(n) && ts.isStringLiteral(n.name)) {
|
||||
if (family && (n.name.text === "url" || n.name.text === "node:url")) return true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** os.constants and its groups — @types/node's `namespace constants`
|
||||
* (errno, dlopen, priority) and SignalConstants, and the fallback's twins:
|
||||
* the runtime's checked-dynamic constants object, whose members vary by
|
||||
@@ -1216,7 +1258,7 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
) {
|
||||
return { kind: "object", className: "%Error" };
|
||||
}
|
||||
if (isOsConstantsType(type, ctx)) return DYN;
|
||||
if (isOsConstantsType(type, ctx) || isLegacyUrlType(type, ctx)) return DYN;
|
||||
// Immutable<T> (@scriptc/types) is a deep-readonly view: the value is
|
||||
// still the T it maps.
|
||||
const immutableSource = immutableSourceType(type, ctx);
|
||||
|
||||
@@ -499,6 +499,13 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"url.fileURLToPathBuffer",
|
||||
"url.pathToFileURLChecked",
|
||||
"url.fileURLToPathStr",
|
||||
// The legacy API: validateString/urlObject ladders, ERR_INVALID_URL,
|
||||
// auth's URIError, and the domain pair's ERR_MISSING_ARGS.
|
||||
"url.legacyParse",
|
||||
"url.legacyFormat",
|
||||
"url.legacyResolve",
|
||||
"url.domainToASCII",
|
||||
"url.domainToUnicode",
|
||||
// The win32-target flavor of pathToFileURL (same runtime entry point —
|
||||
// the bridge dispatches by the binary's platform): Node's win32 arm
|
||||
// raises ERR_INVALID_ARG_VALUE TypeErrors for malformed UNC inputs, so
|
||||
|
||||
@@ -751,6 +751,18 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
"url.fileURLToPathUrl": { argTypes: [URL_T], result: STRING },
|
||||
"url.fileURLToPathChecked": { argTypes: [DYN], result: STRING },
|
||||
"url.fileURLToPathOptions": { argTypes: [DYN, DYN], result: STRING },
|
||||
/** node:url's legacy API (scr_url_legacy.c): parse(url, parseQueryString,
|
||||
* slashesDenoteHost) answers the checked-dynamic Url instance; format
|
||||
* takes the urlObject (a Url-shaped object, a string, or a URL) and the
|
||||
* URL form's options; resolve takes (from, to). The trailing bool says
|
||||
* whether the call site emits DEP0169. domainToASCII/domainToUnicode
|
||||
* take the domain and whether it was omitted. Node's validation errors
|
||||
* throw catchably. */
|
||||
"url.legacyParse": { argTypes: [DYN, DYN, DYN, BOOL], result: DYN },
|
||||
"url.legacyFormat": { argTypes: [DYN, DYN, BOOL], result: STRING },
|
||||
"url.legacyResolve": { argTypes: [DYN, DYN, BOOL], result: STRING },
|
||||
"url.domainToASCII": { argTypes: [DYN, BOOL], result: STRING },
|
||||
"url.domainToUnicode": { argTypes: [DYN, BOOL], result: STRING },
|
||||
"url.fileURLToPathBuffer": { argTypes: [DYN, DYN], result: BYTES_U8 },
|
||||
"url.pathToFileURLChecked": { argTypes: [DYN, DYN], result: URL_T },
|
||||
"url.fileURLToPathStr": { argTypes: [STRING], result: STRING },
|
||||
|
||||
@@ -72,6 +72,14 @@ const DYN_ASYNC_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"dc.chanRunStores",
|
||||
]);
|
||||
|
||||
const LEGACY_URL_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"url.legacyParse",
|
||||
"url.legacyFormat",
|
||||
"url.legacyResolve",
|
||||
"url.domainToASCII",
|
||||
"url.domainToUnicode",
|
||||
]);
|
||||
|
||||
const UTIL_CALLABLE_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"util.inherits",
|
||||
"util.deprecate",
|
||||
@@ -213,6 +221,14 @@ function scanRuntimeFeatures(mod: IrModule, stopAt?: keyof RuntimeFeatures): Run
|
||||
if (fn.startsWith("bigint.")) features.bigint = true;
|
||||
if (fn.startsWith("sp.") || fn === "url.searchParams") features.searchParams = true;
|
||||
if (fn === "qs.parse" || fn === "qs.stringify" || fn === "qs.unescape") features.qs = true;
|
||||
// scr_url_legacy.c lowercases and normalizes hostnames (the regex
|
||||
// unit's libunicode), parses and stringifies queries, and emits its
|
||||
// deprecation warnings through the dyn-async unit.
|
||||
if (LEGACY_URL_LIB_FNS.has(fn)) {
|
||||
features.regex = true;
|
||||
features.qs = true;
|
||||
features.dynAsync = true;
|
||||
}
|
||||
if (fn === "util.parseArgs" || fn === "util.isDeepStrictEqual" || fn === "util.styleText")
|
||||
features.parseArgs = true;
|
||||
if (fn === "util.styleText") features.processEvents = true;
|
||||
|
||||
@@ -2098,6 +2098,18 @@
|
||||
"status": "static",
|
||||
"note": "recognized module (bare and node:-prefixed specifiers)"
|
||||
},
|
||||
{
|
||||
"id": "node-builtin.url.domainToASCII",
|
||||
"kind": "node-builtin",
|
||||
"name": "url.domainToASCII",
|
||||
"status": "static"
|
||||
},
|
||||
{
|
||||
"id": "node-builtin.url.domainToUnicode",
|
||||
"kind": "node-builtin",
|
||||
"name": "url.domainToUnicode",
|
||||
"status": "static"
|
||||
},
|
||||
{
|
||||
"id": "node-builtin.url.fileURLToPath",
|
||||
"kind": "node-builtin",
|
||||
@@ -2110,12 +2122,30 @@
|
||||
"name": "url.fileURLToPathBuffer",
|
||||
"status": "static"
|
||||
},
|
||||
{
|
||||
"id": "node-builtin.url.format",
|
||||
"kind": "node-builtin",
|
||||
"name": "url.format",
|
||||
"status": "static"
|
||||
},
|
||||
{
|
||||
"id": "node-builtin.url.parse",
|
||||
"kind": "node-builtin",
|
||||
"name": "url.parse",
|
||||
"status": "static"
|
||||
},
|
||||
{
|
||||
"id": "node-builtin.url.pathToFileURL",
|
||||
"kind": "node-builtin",
|
||||
"name": "url.pathToFileURL",
|
||||
"status": "static"
|
||||
},
|
||||
{
|
||||
"id": "node-builtin.url.resolve",
|
||||
"kind": "node-builtin",
|
||||
"name": "url.resolve",
|
||||
"status": "static"
|
||||
},
|
||||
{
|
||||
"id": "node-builtin.util",
|
||||
"kind": "node-builtin",
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5220-url-parse-warnings.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5221-url-parse-shapes.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5222-url-parse-hosts.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5223-url-parse-query.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5224-url-format-objects.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5225-url-format-whatwg.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5226-url-resolve.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5227-url-legacy-dependency/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5228-url-domain-conversion.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5229-url-legacy-errors.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5270-url-parse-no-deprecation.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5271-url-parse-throw-deprecation.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/node-types/url-legacy.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -58,6 +58,7 @@ const optional = [
|
||||
["scr_file_handle.c", "fileHandle"],
|
||||
["scr_regex.c", "regex"],
|
||||
["scr_glob.c", "regex"],
|
||||
["scr_idna.c", "regex"],
|
||||
["scr_assert.c", any("assert", "regex", "symbol")],
|
||||
["scr_inspect.c", "inspect"],
|
||||
["scr_console_native.c", "inspect"],
|
||||
@@ -73,6 +74,9 @@ const optional = [
|
||||
["scr_symbol.c", "symbol"],
|
||||
["scr_bigint_assert.c", all("assert", "bigint")],
|
||||
["scr_qs.c", "qs"],
|
||||
// The legacy url unit lowercases and normalizes through the regex unit
|
||||
// and warns through the dyn-async one.
|
||||
["scr_url_legacy.c", all("regex", "qs", "dynAsync")],
|
||||
["scr_util.c", "parseArgs"],
|
||||
// Utility objects must only reference runtime families selected with them.
|
||||
["scr_util_compare.c", all("parseArgs", "symbol")],
|
||||
|
||||
@@ -48,6 +48,7 @@ const OPTIONAL = [
|
||||
["scr_file_handle.c", "fileHandle"],
|
||||
["scr_regex.c", "regex"],
|
||||
["scr_glob.c", "regex"],
|
||||
["scr_idna.c", "regex"],
|
||||
["scr_assert.c", any("assert", "regex", "symbol")],
|
||||
["scr_inspect.c", "inspect"],
|
||||
["scr_console_native.c", "inspect"],
|
||||
@@ -63,6 +64,9 @@ const OPTIONAL = [
|
||||
["scr_symbol.c", "symbol"],
|
||||
["scr_bigint_assert.c", all("assert", "bigint")],
|
||||
["scr_qs.c", "qs"],
|
||||
// The legacy url unit lowercases and normalizes through the regex unit
|
||||
// and warns through the dyn-async one.
|
||||
["scr_url_legacy.c", all("regex", "qs", "dynAsync")],
|
||||
["scr_util.c", "parseArgs"],
|
||||
// Utility objects must only reference runtime families selected with them.
|
||||
["scr_util_compare.c", all("parseArgs", "symbol")],
|
||||
|
||||
@@ -739,15 +739,30 @@ static void scr_assert_cf_value(ScrAssertBuf *b, const ScrDyn *d, size_t indent,
|
||||
case SCR_DYN_OBJ: {
|
||||
/* The null-proto dictionary renders with Node's prefix in the
|
||||
* failure diff too (assertion_error.js inspects both sides). */
|
||||
const char *cls = scr_dyn_class_name(d);
|
||||
if (d->v.obj.len == 0) {
|
||||
if (cls) {
|
||||
ab_cstr(b, cls);
|
||||
ab_cstr(b, " {}");
|
||||
return;
|
||||
}
|
||||
ab_cstr(b, d->null_proto ? "[Object: null prototype] {}" : "{}");
|
||||
return;
|
||||
}
|
||||
if (depth == 0) {
|
||||
if (cls) {
|
||||
ab_cstr(b, "[");
|
||||
ab_cstr(b, cls);
|
||||
ab_cstr(b, "]");
|
||||
return;
|
||||
}
|
||||
ab_cstr(b, d->null_proto ? "[Object: null prototype]" : "[Object]");
|
||||
return;
|
||||
}
|
||||
if (d->null_proto) ab_cstr(b, "[Object: null prototype] ");
|
||||
if (cls) {
|
||||
ab_cstr(b, cls);
|
||||
ab_cstr(b, " ");
|
||||
} else if (d->null_proto) ab_cstr(b, "[Object: null prototype] ");
|
||||
/* Render every entry, then sort the RENDERED texts (Node's
|
||||
* sorted:true sorts formatted entries, quotes included). */
|
||||
ScrCfEntry *ents = malloc(d->v.obj.len * sizeof *ents);
|
||||
|
||||
@@ -0,0 +1,514 @@
|
||||
/* UTS #46 domain processing for node:url — the conversions Node applies to
|
||||
* url.parse() hostnames (ToASCII), url.domainToASCII()/domainToUnicode()
|
||||
* and url.format()'s unicode option: nontransitional mapping, NFC, label
|
||||
* validity (leading combining marks, ContextJ, and the RFC 5893 bidi rule),
|
||||
* and Punycode. It matches Node 24's implementation, including its
|
||||
* validity-data snapshot and the shortcuts its label checks take; the
|
||||
* differential corpus and test/url-legacy-cases.txt (generated from Node)
|
||||
* are the oracle.
|
||||
*
|
||||
* Code points travel as UTF-32 buffers. Input bytes must be well-formed
|
||||
* UTF-8: lone surrogates and malformed sequences fail ToASCII, as they do
|
||||
* once Node has replaced them with U+FFFD (a disallowed code point). All
|
||||
* inputs are BORROWED; string results are fresh (+1). Nothing throws. */
|
||||
#include "scr_runtime.h"
|
||||
#include "scr_idna_data.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* ── UTF-32 scratch buffers ─────────────────────────────────────────── */
|
||||
|
||||
typedef struct {
|
||||
uint32_t *data;
|
||||
size_t len;
|
||||
size_t cap;
|
||||
} IdnaBuf;
|
||||
|
||||
static void ib32_reserve(IdnaBuf *b, size_t extra) {
|
||||
if (b->len + extra <= b->cap) return;
|
||||
size_t cap = b->cap ? b->cap : 16;
|
||||
while (b->len + extra > cap) cap *= 2;
|
||||
uint32_t *grown = realloc(b->data, cap * sizeof *grown);
|
||||
if (!grown) scr_trap("scriptc: out of memory\n");
|
||||
b->data = grown;
|
||||
b->cap = cap;
|
||||
}
|
||||
|
||||
static void ib32_push(IdnaBuf *b, uint32_t c) {
|
||||
ib32_reserve(b, 1);
|
||||
b->data[b->len++] = c;
|
||||
}
|
||||
|
||||
static void ib32_insert(IdnaBuf *b, size_t at, uint32_t c) {
|
||||
ib32_reserve(b, 1);
|
||||
memmove(b->data + at + 1, b->data + at, (b->len - at) * sizeof *b->data);
|
||||
b->data[at] = c;
|
||||
b->len++;
|
||||
}
|
||||
|
||||
/* Strict UTF-8 → UTF-32; false on any malformed sequence, overlong form,
|
||||
* surrogate, or code point past U+10FFFF. */
|
||||
static bool idna_decode_utf8(const char *p, size_t n, IdnaBuf *out) {
|
||||
const unsigned char *s = (const unsigned char *)p;
|
||||
for (size_t i = 0; i < n;) {
|
||||
unsigned char c = s[i];
|
||||
uint32_t cp;
|
||||
size_t extra;
|
||||
if (c < 0x80) {
|
||||
ib32_push(out, c);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if ((c & 0xE0) == 0xC0) {
|
||||
cp = c & 0x1F;
|
||||
extra = 1;
|
||||
} else if ((c & 0xF0) == 0xE0) {
|
||||
cp = c & 0x0F;
|
||||
extra = 2;
|
||||
} else if ((c & 0xF8) == 0xF0) {
|
||||
cp = c & 0x07;
|
||||
extra = 3;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
if (i + extra >= n) return false;
|
||||
for (size_t k = 1; k <= extra; k++) {
|
||||
if ((s[i + k] & 0xC0) != 0x80) return false;
|
||||
cp = (cp << 6) | (s[i + k] & 0x3F);
|
||||
}
|
||||
if ((extra == 1 && cp < 0x80) || (extra == 2 && (cp < 0x800 || (cp >= 0xD800 && cp < 0xE000))) ||
|
||||
(extra == 3 && (cp < 0x10000 || cp > 0x10FFFF)))
|
||||
return false;
|
||||
ib32_push(out, cp);
|
||||
i += extra + 1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void idna_put_utf8(ScrJsonBuf *b, uint32_t cp) {
|
||||
char out[4];
|
||||
size_t n;
|
||||
if (cp < 0x80) {
|
||||
out[0] = (char)cp;
|
||||
n = 1;
|
||||
} else if (cp < 0x800) {
|
||||
out[0] = (char)(0xC0 | (cp >> 6));
|
||||
out[1] = (char)(0x80 | (cp & 0x3F));
|
||||
n = 2;
|
||||
} else if (cp < 0x10000) {
|
||||
out[0] = (char)(0xE0 | (cp >> 12));
|
||||
out[1] = (char)(0x80 | ((cp >> 6) & 0x3F));
|
||||
out[2] = (char)(0x80 | (cp & 0x3F));
|
||||
n = 3;
|
||||
} else {
|
||||
out[0] = (char)(0xF0 | (cp >> 18));
|
||||
out[1] = (char)(0x80 | ((cp >> 12) & 0x3F));
|
||||
out[2] = (char)(0x80 | ((cp >> 6) & 0x3F));
|
||||
out[3] = (char)(0x80 | (cp & 0x3F));
|
||||
n = 4;
|
||||
}
|
||||
scr_jb_write_utf8(b, out, n);
|
||||
}
|
||||
|
||||
/* ── table lookups ─────────────────────────────────────────────────── */
|
||||
|
||||
static uint32_t idna_status(uint32_t cp) {
|
||||
size_t lo = 0, hi = sizeof scr_idna_status / sizeof scr_idna_status[0];
|
||||
while (hi - lo > 1) {
|
||||
size_t mid = lo + (hi - lo) / 2;
|
||||
if (scr_idna_status[mid][0] <= cp) lo = mid;
|
||||
else hi = mid;
|
||||
}
|
||||
return scr_idna_status[lo][1];
|
||||
}
|
||||
|
||||
static uint8_t idna_range_value(const ScrIdnaRange *table, size_t n, uint32_t cp) {
|
||||
size_t lo = 0, hi = n;
|
||||
while (lo < hi) {
|
||||
size_t mid = lo + (hi - lo) / 2;
|
||||
if (table[mid].end < cp) lo = mid + 1;
|
||||
else hi = mid;
|
||||
}
|
||||
return lo < n && table[lo].start <= cp ? table[lo].value : 0;
|
||||
}
|
||||
|
||||
#define IDNA_LOOKUP(table, cp) idna_range_value(table, sizeof table / sizeof table[0], cp)
|
||||
|
||||
static uint8_t idna_bidi(uint32_t cp) { return IDNA_LOOKUP(scr_idna_bidi, cp); }
|
||||
|
||||
static bool idna_is_virama(uint32_t cp) {
|
||||
size_t lo = 0, hi = sizeof scr_idna_virama / sizeof scr_idna_virama[0];
|
||||
while (lo < hi) {
|
||||
size_t mid = lo + (hi - lo) / 2;
|
||||
if (scr_idna_virama[mid] < cp) lo = mid + 1;
|
||||
else hi = mid;
|
||||
}
|
||||
return lo < sizeof scr_idna_virama / sizeof scr_idna_virama[0] && scr_idna_virama[lo] == cp;
|
||||
}
|
||||
|
||||
/* The UTS #46 Map step: false when a disallowed code point appears. */
|
||||
static bool idna_map(const uint32_t *in, size_t n, IdnaBuf *out) {
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
uint32_t descriptor = idna_status(in[i]);
|
||||
switch (descriptor & 0xFF) {
|
||||
case 0:
|
||||
break;
|
||||
case 1:
|
||||
ib32_push(out, in[i]);
|
||||
break;
|
||||
case 2:
|
||||
return false;
|
||||
default: {
|
||||
size_t count = descriptor >> 24;
|
||||
size_t index = (descriptor >> 8) & 0xFFFF;
|
||||
for (size_t k = 0; k < count; k++) ib32_push(out, scr_idna_mappings[index + k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void idna_normalize(IdnaBuf *b) {
|
||||
bool ascii = true;
|
||||
for (size_t i = 0; i < b->len && ascii; i++) ascii = b->data[i] < 0x80;
|
||||
if (ascii) return;
|
||||
size_t len;
|
||||
uint32_t *nfc = scr_nfc_code_points(b->data, b->len, &len);
|
||||
free(b->data);
|
||||
b->data = nfc;
|
||||
b->len = b->cap = len;
|
||||
}
|
||||
|
||||
static bool idna_same(const uint32_t *a, size_t an, const uint32_t *b, size_t bn) {
|
||||
return an == bn && (an == 0 || memcmp(a, b, an * sizeof *a) == 0);
|
||||
}
|
||||
|
||||
/* ── Punycode (RFC 3492) ───────────────────────────────────────────── */
|
||||
|
||||
enum { PUNY_BASE = 36, PUNY_TMIN = 1, PUNY_TMAX = 26, PUNY_SKEW = 38, PUNY_DAMP = 700 };
|
||||
|
||||
static int32_t puny_digit(char c) {
|
||||
if (c >= 'a' && c <= 'z') return c - 'a';
|
||||
if (c >= '0' && c <= '9') return c - '0' + 26;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static char puny_char(int32_t digit) { return (char)(digit < 26 ? digit + 'a' : digit + 22); }
|
||||
|
||||
static int32_t puny_adapt(int32_t d, int32_t n, bool first) {
|
||||
d = first ? d / PUNY_DAMP : d / 2;
|
||||
d += d / n;
|
||||
int32_t k = 0;
|
||||
while (d > ((PUNY_BASE - PUNY_TMIN) * PUNY_TMAX) / 2) {
|
||||
d /= PUNY_BASE - PUNY_TMIN;
|
||||
k += PUNY_BASE;
|
||||
}
|
||||
return k + (((PUNY_BASE - PUNY_TMIN + 1) * d) / (d + PUNY_SKEW));
|
||||
}
|
||||
|
||||
/* Decodes the label after its "xn--" prefix. `out` may be NULL to only
|
||||
* verify. A second "xn--" prefix fails (whatwg/url#803). */
|
||||
static bool puny_decode(const char *p, size_t n, IdnaBuf *out) {
|
||||
if (n >= 4 && memcmp(p, "xn--", 4) == 0) return false;
|
||||
int32_t written = 0;
|
||||
uint32_t code = 128;
|
||||
int32_t i = 0;
|
||||
int32_t bias = 72;
|
||||
size_t dash = n;
|
||||
for (size_t k = n; k > 0; k--) {
|
||||
if (p[k - 1] == '-') {
|
||||
dash = k - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
size_t pos = 0;
|
||||
if (dash != n) {
|
||||
for (size_t k = 0; k < dash; k++) {
|
||||
if ((unsigned char)p[k] >= 0x80) return false;
|
||||
if (out) ib32_push(out, (unsigned char)p[k]);
|
||||
written++;
|
||||
}
|
||||
pos = dash + 1;
|
||||
}
|
||||
while (pos < n) {
|
||||
int32_t old = i;
|
||||
int32_t w = 1;
|
||||
for (int32_t k = PUNY_BASE;; k += PUNY_BASE) {
|
||||
if (pos >= n) return false;
|
||||
int32_t digit = puny_digit(p[pos++]);
|
||||
if (digit < 0) return false;
|
||||
if (digit > (0x7fffffff - i) / w) return false;
|
||||
i += digit * w;
|
||||
int32_t t = k <= bias ? PUNY_TMIN : k >= bias + PUNY_TMAX ? PUNY_TMAX : k - bias;
|
||||
if (digit < t) break;
|
||||
if (w > 0x7fffffff / (PUNY_BASE - t)) return false;
|
||||
w *= PUNY_BASE - t;
|
||||
}
|
||||
bias = puny_adapt(i - old, written + 1, old == 0);
|
||||
if (i / (written + 1) > (int32_t)(0x7fffffff - code)) return false;
|
||||
code += (uint32_t)(i / (written + 1));
|
||||
i %= written + 1;
|
||||
if (code < 0x80) return false;
|
||||
if (out) ib32_insert(out, (size_t)i, code);
|
||||
written++;
|
||||
i++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool puny_encode(const uint32_t *in, size_t n, ScrJsonBuf *out) {
|
||||
uint32_t code = 128;
|
||||
int32_t delta = 0;
|
||||
int32_t bias = 72;
|
||||
size_t handled = 0;
|
||||
for (size_t k = 0; k < n; k++) {
|
||||
if (in[k] < 0x80) {
|
||||
handled++;
|
||||
scr_jb_putc(out, (char)in[k]);
|
||||
}
|
||||
if (in[k] > 0x10FFFF || (in[k] >= 0xD800 && in[k] < 0xE000)) return false;
|
||||
}
|
||||
size_t basic = handled;
|
||||
if (basic > 0) scr_jb_putc(out, '-');
|
||||
while (handled < n) {
|
||||
uint32_t m = 0x10FFFF;
|
||||
for (size_t k = 0; k < n; k++)
|
||||
if (in[k] >= code && in[k] < m) m = in[k];
|
||||
if ((m - code) > (0x7fffffff - (uint32_t)delta) / (handled + 1)) return false;
|
||||
delta += (int32_t)((m - code) * (handled + 1));
|
||||
code = m;
|
||||
for (size_t k = 0; k < n; k++) {
|
||||
if (in[k] < code) {
|
||||
if (delta == 0x7fffffff) return false;
|
||||
delta++;
|
||||
}
|
||||
if (in[k] == code) {
|
||||
int32_t q = delta;
|
||||
for (int32_t j = PUNY_BASE;; j += PUNY_BASE) {
|
||||
int32_t t = j <= bias ? PUNY_TMIN : j >= bias + PUNY_TMAX ? PUNY_TMAX : j - bias;
|
||||
if (q < t) break;
|
||||
scr_jb_putc(out, puny_char(t + ((q - t) % (PUNY_BASE - t))));
|
||||
q = (q - t) / (PUNY_BASE - t);
|
||||
}
|
||||
scr_jb_putc(out, puny_char(q));
|
||||
bias = puny_adapt(delta, (int32_t)(handled + 1), handled == basic);
|
||||
delta = 0;
|
||||
handled++;
|
||||
}
|
||||
}
|
||||
delta++;
|
||||
code++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ── label validity ────────────────────────────────────────────────── */
|
||||
|
||||
/* The UTS #46 Validity Criteria as Node checks them: a leading combining
|
||||
* mark fails; the FIRST joiner decides the whole label (a joiner after a
|
||||
* virama passes, a ZWNJ otherwise needs a left- or dual-joining character
|
||||
* somewhere before it and a right- or dual-joining one somewhere after);
|
||||
* otherwise a label holding an R, AL or AN character follows the RFC 5893
|
||||
* rule for the direction of its first character. */
|
||||
static bool idna_label_valid(const uint32_t *label, size_t n) {
|
||||
if (n == 0) return true;
|
||||
if (IDNA_LOOKUP(scr_idna_marks, label[0])) return false;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
uint32_t c = label[i];
|
||||
if (c != 0x200C && c != 0x200D) continue;
|
||||
if (i > 0 && idna_is_virama(label[i - 1])) return true;
|
||||
if (c == 0x200D || i + 1 >= n) return false;
|
||||
bool before = false, after = false;
|
||||
for (size_t k = 0; k < i && !before; k++) {
|
||||
uint8_t j = IDNA_LOOKUP(scr_idna_joining, label[k]);
|
||||
before = j == SCR_JOIN_L || j == SCR_JOIN_D;
|
||||
}
|
||||
for (size_t k = i + 1; k < n && !after; k++) {
|
||||
uint8_t j = IDNA_LOOKUP(scr_idna_joining, label[k]);
|
||||
after = j == SCR_JOIN_R || j == SCR_JOIN_D;
|
||||
}
|
||||
return before && after;
|
||||
}
|
||||
size_t last = n;
|
||||
for (size_t k = n; k > 0; k--) {
|
||||
if (idna_bidi(label[k - 1]) != SCR_BIDI_NSM) {
|
||||
last = k - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (last == n) return false;
|
||||
bool rtl = false;
|
||||
for (size_t k = 0; k < n && !rtl; k++) {
|
||||
uint8_t d = idna_bidi(label[k]);
|
||||
rtl = d == SCR_BIDI_R || d == SCR_BIDI_AL || d == SCR_BIDI_AN;
|
||||
}
|
||||
if (!rtl) return true;
|
||||
if (idna_bidi(label[0]) == SCR_BIDI_L) {
|
||||
for (size_t k = 0; k < last; k++) {
|
||||
uint8_t d = idna_bidi(label[k]);
|
||||
if (!(d == SCR_BIDI_L || d == SCR_BIDI_EN || d == SCR_BIDI_ES || d == SCR_BIDI_CS ||
|
||||
d == SCR_BIDI_ET || d == SCR_BIDI_ON || d == SCR_BIDI_BN || d == SCR_BIDI_NSM))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool has_an = false, has_en = false;
|
||||
for (size_t k = 0; k <= last; k++) {
|
||||
uint8_t d = idna_bidi(label[k]);
|
||||
if (d == SCR_BIDI_EN) {
|
||||
has_en = true;
|
||||
if (has_an) return false;
|
||||
} else if (d == SCR_BIDI_AN) {
|
||||
has_an = true;
|
||||
if (has_en) return false;
|
||||
}
|
||||
if (!(d == SCR_BIDI_R || d == SCR_BIDI_AL || d == SCR_BIDI_AN || d == SCR_BIDI_EN ||
|
||||
d == SCR_BIDI_ES || d == SCR_BIDI_CS || d == SCR_BIDI_ET || d == SCR_BIDI_ON ||
|
||||
d == SCR_BIDI_BN || d == SCR_BIDI_NSM))
|
||||
return false;
|
||||
if (k == last && !(d == SCR_BIDI_R || d == SCR_BIDI_AL || d == SCR_BIDI_AN || d == SCR_BIDI_EN))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* An "xn--" label's Punycode must decode to a non-ASCII label that the
|
||||
* mapping and NFC leave unchanged and that passes the validity checks. */
|
||||
static bool idna_check_puny_label(const char *p, size_t n) {
|
||||
IdnaBuf decoded = {0}, mapped = {0};
|
||||
bool ok = puny_decode(p, n, &decoded);
|
||||
if (ok) {
|
||||
bool ascii = true;
|
||||
for (size_t k = 0; k < decoded.len && ascii; k++) ascii = decoded.data[k] < 0x80;
|
||||
ok = !ascii && idna_map(decoded.data, decoded.len, &mapped) &&
|
||||
idna_same(decoded.data, decoded.len, mapped.data, mapped.len);
|
||||
}
|
||||
if (ok) {
|
||||
IdnaBuf normal = {0};
|
||||
ib32_reserve(&normal, mapped.len);
|
||||
memcpy(normal.data, mapped.data, mapped.len * sizeof *mapped.data);
|
||||
normal.len = mapped.len;
|
||||
idna_normalize(&normal);
|
||||
ok = idna_same(normal.data, normal.len, mapped.data, mapped.len) && mapped.len > 0 &&
|
||||
idna_label_valid(mapped.data, mapped.len);
|
||||
free(normal.data);
|
||||
}
|
||||
free(decoded.data);
|
||||
free(mapped.data);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static ScrStr *idna_fail(ScrJsonBuf *b) {
|
||||
scr_str_release(scr_jb_finish(b));
|
||||
return scr_str_new("", 0);
|
||||
}
|
||||
|
||||
/* ── ToASCII / ToUnicode ───────────────────────────────────────────── */
|
||||
|
||||
ScrStr *scr_idna_to_ascii(const char *p, size_t n) {
|
||||
bool ascii = true;
|
||||
for (size_t i = 0; i < n && ascii; i++) ascii = (unsigned char)p[i] < 0x80;
|
||||
ScrJsonBuf out;
|
||||
scr_jb_init(&out);
|
||||
if (ascii) {
|
||||
/* ASCII input maps by lowercasing alone; only Punycode labels need
|
||||
* the full check. */
|
||||
char *lower = malloc(n ? n : 1);
|
||||
if (!lower) scr_trap("scriptc: out of memory\n");
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
char c = p[i];
|
||||
lower[i] = c >= 'A' && c <= 'Z' ? (char)(c + 32) : c;
|
||||
}
|
||||
for (size_t start = 0; start < n;) {
|
||||
const char *dot = memchr(lower + start, '.', n - start);
|
||||
size_t end = dot ? (size_t)(dot - lower) : n;
|
||||
size_t len = end - start;
|
||||
if (len >= 4 && memcmp(lower + start, "xn--", 4) == 0 &&
|
||||
!idna_check_puny_label(lower + start + 4, len - 4)) {
|
||||
free(lower);
|
||||
return idna_fail(&out);
|
||||
}
|
||||
scr_jb_write_utf8(&out, lower + start, len);
|
||||
if (!dot) break;
|
||||
scr_jb_putc(&out, '.');
|
||||
start = end + 1;
|
||||
}
|
||||
free(lower);
|
||||
return scr_jb_finish(&out);
|
||||
}
|
||||
IdnaBuf input = {0}, mapped = {0};
|
||||
if (!idna_decode_utf8(p, n, &input) || !idna_map(input.data, input.len, &mapped)) {
|
||||
free(input.data);
|
||||
free(mapped.data);
|
||||
return idna_fail(&out);
|
||||
}
|
||||
free(input.data);
|
||||
idna_normalize(&mapped);
|
||||
bool ok = true;
|
||||
for (size_t start = 0; ok && start < mapped.len;) {
|
||||
size_t end = start;
|
||||
while (end < mapped.len && mapped.data[end] != '.') end++;
|
||||
const uint32_t *label = mapped.data + start;
|
||||
size_t len = end - start;
|
||||
bool label_ascii = true;
|
||||
for (size_t k = 0; k < len && label_ascii; k++) label_ascii = label[k] < 0x80;
|
||||
if (len >= 4 && label[0] == 'x' && label[1] == 'n' && label[2] == '-' && label[3] == '-') {
|
||||
if (!label_ascii) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
char *bytes = malloc(len);
|
||||
if (!bytes) scr_trap("scriptc: out of memory\n");
|
||||
for (size_t k = 0; k < len; k++) bytes[k] = (char)label[k];
|
||||
ok = idna_check_puny_label(bytes + 4, len - 4);
|
||||
if (ok) scr_jb_write_utf8(&out, bytes, len);
|
||||
free(bytes);
|
||||
} else if (label_ascii) {
|
||||
for (size_t k = 0; k < len; k++) scr_jb_putc(&out, (char)label[k]);
|
||||
} else {
|
||||
ok = idna_label_valid(label, len);
|
||||
if (ok) {
|
||||
scr_jb_puts(&out, "xn--");
|
||||
ok = puny_encode(label, len, &out);
|
||||
}
|
||||
}
|
||||
if (end < mapped.len) scr_jb_putc(&out, '.');
|
||||
start = end + 1;
|
||||
}
|
||||
free(mapped.data);
|
||||
return ok ? scr_jb_finish(&out) : idna_fail(&out);
|
||||
}
|
||||
|
||||
ScrStr *scr_idna_to_unicode(const char *p, size_t n) {
|
||||
ScrJsonBuf out;
|
||||
scr_jb_init(&out);
|
||||
for (size_t start = 0; start < n;) {
|
||||
const char *dot = memchr(p + start, '.', n - start);
|
||||
size_t end = dot ? (size_t)(dot - p) : n;
|
||||
const char *label = p + start;
|
||||
size_t len = end - start;
|
||||
bool ascii = true;
|
||||
for (size_t k = 0; k < len && ascii; k++) ascii = (unsigned char)label[k] < 0x80;
|
||||
bool decoded = false;
|
||||
if (ascii && len >= 4 && memcmp(label, "xn--", 4) == 0 &&
|
||||
puny_decode(label + 4, len - 4, NULL)) {
|
||||
IdnaBuf cps = {0};
|
||||
if (puny_decode(label + 4, len - 4, &cps)) {
|
||||
bool scalar = true;
|
||||
for (size_t k = 0; k < cps.len && scalar; k++)
|
||||
scalar = cps.data[k] <= 0x10FFFF && (cps.data[k] < 0xD800 || cps.data[k] >= 0xE000);
|
||||
if (scalar) {
|
||||
for (size_t k = 0; k < cps.len; k++) idna_put_utf8(&out, cps.data[k]);
|
||||
decoded = true;
|
||||
}
|
||||
}
|
||||
free(cps.data);
|
||||
}
|
||||
if (!decoded) scr_jb_write(&out, label, len);
|
||||
if (!dot) break;
|
||||
scr_jb_putc(&out, '.');
|
||||
start = end + 1;
|
||||
}
|
||||
return scr_jb_finish(&out);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -864,6 +864,21 @@ static ScrStr *scr_insp_dyn_error(ScrDyn *d, double recurse, double depth) {
|
||||
return out;
|
||||
}
|
||||
|
||||
/* A class instance's name forms: "Name {}" for an empty instance, "[Name]"
|
||||
* past the depth limit (tail NULL). */
|
||||
static ScrStr *scr_insp_class_tag(const char *cls, const char *tail) {
|
||||
InspBuf out = {0};
|
||||
if (tail) {
|
||||
ib_cstr(&out, cls);
|
||||
ib_cstr(&out, tail);
|
||||
} else {
|
||||
ib_char(&out, '[');
|
||||
ib_cstr(&out, cls);
|
||||
ib_char(&out, ']');
|
||||
}
|
||||
return ib_take(&out);
|
||||
}
|
||||
|
||||
ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) {
|
||||
if (d->kind == SCR_DYN_PROXY) return scr_insp_dyn(d->v.proxy.target, recurse, depth);
|
||||
switch (d->kind) {
|
||||
@@ -916,11 +931,14 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) {
|
||||
* with "[Object: null prototype]" (formatValue's constructor-less
|
||||
* base), the empty form included, and the beyond-depth answer IS
|
||||
* the bare marker (where a plain object says [Object]). */
|
||||
const char *cls = scr_dyn_class_name(d);
|
||||
if (d->v.obj.len == 0) {
|
||||
if (cls) return scr_insp_class_tag(cls, " {}");
|
||||
return d->null_proto ? scr_str_new("[Object: null prototype] {}", 27)
|
||||
: scr_str_new("{}", 2);
|
||||
}
|
||||
if (recurse > depth) {
|
||||
if (cls) return scr_insp_class_tag(cls, NULL);
|
||||
return d->null_proto ? scr_str_new("[Object: null prototype]", 24)
|
||||
: scr_str_new("[Object]", 8);
|
||||
}
|
||||
@@ -937,8 +955,9 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) {
|
||||
scr_insp_entry(entry, false);
|
||||
scr_str_release(entry);
|
||||
}
|
||||
ScrStr *base = d->null_proto ? scr_str_new("[Object: null prototype]", 24)
|
||||
: scr_str_new("", 0);
|
||||
ScrStr *base = cls ? scr_str_new(cls, strlen(cls))
|
||||
: d->null_proto ? scr_str_new("[Object: null prototype]", 24)
|
||||
: scr_str_new("", 0);
|
||||
ScrStr *b0 = scr_str_new("{", 1);
|
||||
ScrStr *b1 = scr_str_new("}", 1);
|
||||
ScrStr *out = scr_insp_end(base, b0, b1, recurse + 1, false, false);
|
||||
|
||||
@@ -256,12 +256,20 @@ ScrStr *scr_qs_unescape(const ScrStr *s) {
|
||||
|
||||
/* ── parse ───────────────────────────────────────────────────────────── */
|
||||
|
||||
/* addKeyVal's tail: decode-if-encoded, then group into the overflow map
|
||||
* (first value = the str_tag arm; a repeat REPLACES it with a two-element
|
||||
* string[] in the arr_tag arm; later repeats push). key/value move in. */
|
||||
static void qs_add_key_val(ScrMap *out, ScrStr *key, ScrStr *value,
|
||||
bool key_encoded, bool val_encoded,
|
||||
uint32_t str_tag, uint32_t arr_tag) {
|
||||
/* Where parse's pairs land: the typed ParsedUrlQuery dictionary's overflow
|
||||
* map (its string and string[] union tags), or a checked-dynamic
|
||||
* null-prototype object (url.parse's parsed query). */
|
||||
typedef struct {
|
||||
ScrMap *map;
|
||||
uint32_t str_tag, arr_tag;
|
||||
ScrDyn *obj;
|
||||
} QsSink;
|
||||
|
||||
/* addKeyVal's tail: decode-if-encoded, then group (first value = a
|
||||
* string; a repeat REPLACES it with a two-element string array; later
|
||||
* repeats push). key/value move in. */
|
||||
static void qs_add_key_val(QsSink *out, ScrStr *key, ScrStr *value,
|
||||
bool key_encoded, bool val_encoded) {
|
||||
if (key->len > 0 && key_encoded) {
|
||||
ScrStr *dec = scr_qs_unescape(key);
|
||||
scr_str_release(key);
|
||||
@@ -272,9 +280,27 @@ static void qs_add_key_val(ScrMap *out, ScrStr *key, ScrStr *value,
|
||||
scr_str_release(value);
|
||||
value = dec;
|
||||
}
|
||||
ScrUnion *cell = scr_map_get_str_ref(out, key);
|
||||
if (out->obj) {
|
||||
ScrDyn *cur = scr_dyn_obj_get(out->obj, key->data, key->len);
|
||||
ScrDyn *boxed = scr_dyn_new_str(value);
|
||||
scr_str_release(value);
|
||||
if (!cur) {
|
||||
scr_dyn_obj_set(out->obj, key->data, key->len, boxed);
|
||||
} else if (cur->kind == SCR_DYN_STR) {
|
||||
ScrDyn *rows = scr_dyn_new_arr();
|
||||
scr_dyn_arr_push(rows, scr_dyn_retain(cur));
|
||||
scr_dyn_arr_push(rows, boxed);
|
||||
scr_dyn_obj_set(out->obj, key->data, key->len, rows);
|
||||
} else {
|
||||
scr_dyn_arr_push(cur, boxed);
|
||||
}
|
||||
scr_str_release(key);
|
||||
return;
|
||||
}
|
||||
uint32_t str_tag = out->str_tag, arr_tag = out->arr_tag;
|
||||
ScrUnion *cell = scr_map_get_str_ref(out->map, key);
|
||||
if (!cell) {
|
||||
scr_map_set_str_ref(out, key,
|
||||
scr_map_set_str_ref(out->map, key,
|
||||
scr_union_new_ref(str_tag, value, &scr_str_retain_v,
|
||||
&scr_str_release_v, NULL));
|
||||
scr_str_release(key);
|
||||
@@ -285,7 +311,7 @@ static void qs_add_key_val(ScrMap *out, ScrStr *key, ScrStr *value,
|
||||
ScrArr *rows = scr_arr_new(SCR_ELEM_STR, 2);
|
||||
scr_arr_push_ref(rows, scr_str_retain_v(scr_union_peek(cell)));
|
||||
scr_arr_push_ref(rows, value);
|
||||
scr_map_set_str_ref(out, key,
|
||||
scr_map_set_str_ref(out->map, key,
|
||||
scr_union_new_ref(arr_tag, rows, &scr_arr_retain_v,
|
||||
&scr_arr_release_v, NULL));
|
||||
} else {
|
||||
@@ -295,9 +321,8 @@ static void qs_add_key_val(ScrMap *out, ScrStr *key, ScrStr *value,
|
||||
scr_str_release(key);
|
||||
}
|
||||
|
||||
void scr_qs_parse_into(ScrMap *out, const ScrStr *qs, const ScrStr *sep,
|
||||
const ScrStr *eq, double max_keys, uint32_t str_tag,
|
||||
uint32_t arr_tag) {
|
||||
static void qs_parse(QsSink *out, const ScrStr *qs, const ScrStr *sep,
|
||||
const ScrStr *eq, double max_keys) {
|
||||
if (qs->len == 0) return;
|
||||
/* Node's falsy rule: undefined/null/'' all mean the default. */
|
||||
const char *sep_b = (sep && sep->len) ? sep->data : "&";
|
||||
@@ -344,7 +369,7 @@ void scr_qs_parse_into(ScrMap *out, const ScrStr *qs, const ScrStr *sep,
|
||||
|
||||
qs_add_key_val(out, scr_str_new(key.data, key.len),
|
||||
scr_str_new(value.data, value.len), key_encoded,
|
||||
val_encoded, str_tag, arr_tag);
|
||||
val_encoded);
|
||||
|
||||
if (--pairs == 0) { returned = true; break; }
|
||||
key_encoded = val_encoded = false;
|
||||
@@ -419,13 +444,37 @@ void scr_qs_parse_into(ScrMap *out, const ScrStr *qs, const ScrStr *sep,
|
||||
if (!ended_empty) {
|
||||
qs_add_key_val(out, scr_str_new(key.data, key.len),
|
||||
scr_str_new(value.data, value.len), key_encoded,
|
||||
val_encoded, str_tag, arr_tag);
|
||||
val_encoded);
|
||||
}
|
||||
}
|
||||
free(key.data);
|
||||
free(value.data);
|
||||
}
|
||||
|
||||
void scr_qs_parse_into(ScrMap *out, const ScrStr *qs, const ScrStr *sep,
|
||||
const ScrStr *eq, double max_keys, uint32_t str_tag,
|
||||
uint32_t arr_tag) {
|
||||
QsSink sink = {.map = out, .str_tag = str_tag, .arr_tag = arr_tag};
|
||||
qs_parse(&sink, qs, sep, eq, max_keys);
|
||||
}
|
||||
|
||||
ScrDyn *scr_qs_parse_dyn(const ScrStr *qs) {
|
||||
QsSink sink = {.obj = scr_dyn_new_obj_null_proto()};
|
||||
qs_parse(&sink, qs, NULL, NULL, 1000);
|
||||
/* Rebuild in JS own-key order (array-index keys ascending first), the
|
||||
* order Node's object holds them in. */
|
||||
ScrDyn *keys = scr_dyn_obj_keys(sink.obj);
|
||||
ScrDyn *out = scr_dyn_new_obj_null_proto();
|
||||
for (size_t i = 0; i < keys->v.arr.len; i++) {
|
||||
const ScrStr *k = keys->v.arr.items[i]->v.str;
|
||||
ScrDyn *v = scr_dyn_obj_get(sink.obj, k->data, k->len);
|
||||
scr_dyn_obj_set(out, k->data, k->len, scr_dyn_retain(v));
|
||||
}
|
||||
scr_dyn_release(keys);
|
||||
scr_dyn_release(sink.obj);
|
||||
return out;
|
||||
}
|
||||
|
||||
/* ── stringify ───────────────────────────────────────────────────────── */
|
||||
|
||||
/* Node's encodeStringified over one dyn value: strings escape, finite
|
||||
|
||||
@@ -1665,6 +1665,15 @@ ScrStr *scr_str_normalize(const ScrStr *s, const ScrStr *form) {
|
||||
return scr_jb_finish(&buffer);
|
||||
}
|
||||
|
||||
uint32_t *scr_nfc_code_points(const uint32_t *input, size_t length, size_t *out_len) {
|
||||
if (length > INT32_MAX) scr_regex_oom();
|
||||
uint32_t *output = NULL;
|
||||
int count = unicode_normalize(&output, input, (int)length, UNICODE_NFC, NULL, scr_normalize_realloc);
|
||||
if (count < 0) scr_regex_oom();
|
||||
*out_len = (size_t)count;
|
||||
return output;
|
||||
}
|
||||
|
||||
/* ── assert.match / assert.doesNotMatch ──────────────────────────────────
|
||||
* Lives here because it needs the matcher. Assertion matching follows
|
||||
* RegExp.test, including global and sticky lastIndex transitions. */
|
||||
|
||||
@@ -1455,6 +1455,16 @@ ScrStr *scr_str_trim(ScrStr *s);
|
||||
ScrStr *scr_str_to_lower(const ScrStr *s);
|
||||
ScrStr *scr_str_to_upper(const ScrStr *s);
|
||||
ScrStr *scr_str_normalize(const ScrStr *s, const ScrStr *form);
|
||||
/* NFC over a code point buffer (scr_regex.c, beside normalize): a fresh
|
||||
* malloc'd buffer the caller frees, its length in *out_len. Borrows input. */
|
||||
uint32_t *scr_nfc_code_points(const uint32_t *input, size_t length, size_t *out_len);
|
||||
|
||||
/* UTS #46 domain conversion (scr_idna.c — the url unit's IDNA tables; it
|
||||
* rides the regex switch for NFC). to_ascii answers "" on failure;
|
||||
* to_unicode decodes each valid Punycode label and never fails. Both
|
||||
* borrow the UTF-8 span and return +1. */
|
||||
ScrStr *scr_idna_to_ascii(const char *p, size_t n);
|
||||
ScrStr *scr_idna_to_unicode(const char *p, size_t n);
|
||||
|
||||
/* charAt(i): 1-code-unit string; out of range → empty string. Half of an
|
||||
* astral pair → that lone surrogate. Returns +1. */
|
||||
@@ -1575,6 +1585,12 @@ ScrStr *scr_str_encode_uri_component(ScrStr *s);
|
||||
/* querystring.escape: the component encoder, throwing Node's URIError
|
||||
* [ERR_INVALID_URI] on a lone surrogate. Borrows s; result +1. */
|
||||
ScrStr *scr_qs_escape(ScrStr *s);
|
||||
/* lib/internal/querystring.js encodeStr over UTF-16 code units: ASCII
|
||||
* outside `no_escape` and every non-ASCII code unit percent-encode as
|
||||
* UTF-8, and ANY surrogate pairs with the code unit after it (Node's own
|
||||
* rule); only a surrogate in last position throws URIError
|
||||
* [ERR_INVALID_URI] and returns NULL. Borrows s; +1. */
|
||||
ScrStr *scr_qs_encode_str(const ScrStr *s, const signed char no_escape[128]);
|
||||
|
||||
/* decodeURIComponent — ECMA-262 Decode with the empty reserved set: %XX
|
||||
* escapes decode bytewise (raw non-escape bytes copy through) and escaped
|
||||
@@ -1627,6 +1643,10 @@ typedef struct ScrMap ScrMap; /* full definition below (C11 repeat) */
|
||||
void scr_qs_parse_into(ScrMap *out, const ScrStr *qs, const ScrStr *sep,
|
||||
const ScrStr *eq, double max_keys, uint32_t str_tag,
|
||||
uint32_t arr_tag);
|
||||
/* querystring.parse(qs) with the default separators and maxKeys, as a
|
||||
* fresh checked-dynamic null-prototype object (url.parse's parsed query).
|
||||
* Borrows qs; +1; never throws. */
|
||||
struct ScrDyn *scr_qs_parse_dyn(const ScrStr *qs);
|
||||
|
||||
/* node:util.parseArgs — the static checked-dynamic boundary. `config` is
|
||||
* the documented JSON-safe configuration object; the returned dyn tree is
|
||||
@@ -4361,6 +4381,19 @@ ScrStr *scr_url_href(ScrUrl *u); /* +1; also toString() */
|
||||
void scr_url_set(ScrUrl *u, ScrStr *field, ScrStr *value); /* borrowed; href may throw */
|
||||
bool scr_url_set_component(ScrUrl *u, ScrStr *field, ScrStr *value); /* borrowed; false for ignored writes */
|
||||
void scr_url_set_checked(ScrUrl *u, ScrStr *field, const ScrDyn *value); /* borrowed; conversion may throw */
|
||||
/* node:url's legacy surface (scr_url_legacy.c — linked with the regex,
|
||||
* querystring and dyn-async units). url.parse() answers a checked-dynamic
|
||||
* Url instance; format() accepts a Url-shaped object, a string, or a
|
||||
* native URL (with its options bag); resolve() takes two strings. `warn`
|
||||
* emits DEP0169 once per process (false at node_modules call sites).
|
||||
* domainToASCII/Unicode take the argument and whether it was omitted
|
||||
* (ERR_MISSING_ARGS). All borrow; +1, or NULL with Node's error pending. */
|
||||
ScrDyn *scr_url_legacy_parse(const ScrDyn *url, const ScrDyn *parse_query, const ScrDyn *slashes,
|
||||
bool warn);
|
||||
ScrStr *scr_url_legacy_format(const ScrDyn *object, const ScrDyn *options, bool warn);
|
||||
ScrStr *scr_url_legacy_resolve(const ScrDyn *from, const ScrDyn *to, bool warn);
|
||||
ScrStr *scr_url_domain_to_ascii(const ScrDyn *domain, bool missing);
|
||||
ScrStr *scr_url_domain_to_unicode(const ScrDyn *domain, bool missing);
|
||||
ScrStr *scr_url_to_path(ScrUrl *u); /* +1, or throws */
|
||||
ScrStr *scr_url_str_to_path(ScrStr *s); /* +1, or throws */
|
||||
ScrUrl *scr_url_from_path(ScrStr *path); /* +1; throws on win32 UNC malformations only */
|
||||
@@ -4865,6 +4898,12 @@ struct ScrDyn {
|
||||
void *source_identity;
|
||||
ScrDyn *(*source_access)(void *, bool materialize);
|
||||
struct ScrDynObjectIndex *index; /* optional wide-object lookup; owns no values */
|
||||
/* On a runtime-built class prototype (e.g. url.Url.prototype): the
|
||||
* constructor name (static text) util.inspect prints for objects
|
||||
* inheriting from it (scr_dyn_class_name). NULL otherwise. Only
|
||||
* prototypes that live until their thread exits set it, and they
|
||||
* clear it before release, so freelist reuse never sees it. */
|
||||
const char *class_name;
|
||||
} obj; /* owned */
|
||||
/* SCR_DYN_FUNC: the boxed closure (owned) + its call descriptor. `sig`
|
||||
* and `name` are static compiler-emitted literals (never freed); name
|
||||
@@ -4937,6 +4976,14 @@ static inline ScrDyn *scr_dyn_retain(ScrDyn *d) {
|
||||
|
||||
void scr_dyn_release(ScrDyn *d); /* releases the tree recursively; NULL-tolerant */
|
||||
|
||||
/* The runtime class an object inherits from (util.inspect's constructor
|
||||
* name, found on its prototype chain), or NULL. */
|
||||
static inline const char *scr_dyn_class_name(const ScrDyn *d) {
|
||||
for (const ScrDyn *p = d->prototype; p; p = p->prototype)
|
||||
if (p->kind == SCR_DYN_OBJ && p->v.obj.class_name) return p->v.obj.class_name;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* The element hooks of a live array capsule, or NULL (including for record
|
||||
* and class capsules, which keep their hooks in the same slot). */
|
||||
static inline const ScrDynTypedArrayOps *scr_dyn_typed_ref_array_ops(const ScrDyn *d) {
|
||||
|
||||
@@ -3056,12 +3056,92 @@ static void scr_uri_malformed(void) {
|
||||
scr_str_new("URI malformed", 13));
|
||||
}
|
||||
|
||||
/* querystring.escape: the component encoder, whose lone-surrogate failure
|
||||
* is Node's URIError [ERR_INVALID_URI]. */
|
||||
ScrStr *scr_qs_encode_str(const ScrStr *s, const signed char no_escape[128]) {
|
||||
static const char hex[] = "0123456789ABCDEF";
|
||||
/* UTF-16 code units: each 4-byte sequence is a surrogate pair. */
|
||||
uint16_t *units = malloc((s->len ? s->len : 1) * sizeof *units);
|
||||
if (!units) scr_trap("scriptc: out of memory\n");
|
||||
size_t n = 0;
|
||||
for (size_t i = 0; i < s->len;) {
|
||||
unsigned char c = (unsigned char)s->data[i];
|
||||
uint32_t cp;
|
||||
size_t len = c < 0x80 ? 1 : c < 0xE0 ? 2 : c < 0xF0 ? 3 : 4;
|
||||
if (len > s->len - i) len = s->len - i;
|
||||
if (len == 1) cp = c;
|
||||
else if (len == 2) cp = ((uint32_t)(c & 0x1F) << 6) | ((unsigned char)s->data[i + 1] & 0x3F);
|
||||
else if (len == 3)
|
||||
cp = ((uint32_t)(c & 0x0F) << 12) | ((uint32_t)((unsigned char)s->data[i + 1] & 0x3F) << 6) |
|
||||
((unsigned char)s->data[i + 2] & 0x3F);
|
||||
else
|
||||
cp = ((uint32_t)(c & 0x07) << 18) | ((uint32_t)((unsigned char)s->data[i + 1] & 0x3F) << 12) |
|
||||
((uint32_t)((unsigned char)s->data[i + 2] & 0x3F) << 6) | ((unsigned char)s->data[i + 3] & 0x3F);
|
||||
i += len;
|
||||
if (cp >= 0x10000) {
|
||||
units[n++] = (uint16_t)(0xD800 + ((cp - 0x10000) >> 10));
|
||||
units[n++] = (uint16_t)(0xDC00 + ((cp - 0x10000) & 0x3FF));
|
||||
} else {
|
||||
units[n++] = (uint16_t)cp;
|
||||
}
|
||||
}
|
||||
ScrJsonBuf out;
|
||||
scr_jb_init(&out);
|
||||
char esc[12];
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
uint32_t c = units[i];
|
||||
size_t k = 0;
|
||||
if (c < 0x80) {
|
||||
if (no_escape[c] == 1) {
|
||||
scr_jb_putc(&out, (char)c);
|
||||
continue;
|
||||
}
|
||||
esc[k++] = '%', esc[k++] = hex[c >> 4], esc[k++] = hex[c & 0xF];
|
||||
} else {
|
||||
uint8_t bytes[4];
|
||||
size_t nb;
|
||||
if (c < 0x800) {
|
||||
bytes[0] = (uint8_t)(0xC0 | (c >> 6)), bytes[1] = (uint8_t)(0x80 | (c & 0x3F));
|
||||
nb = 2;
|
||||
} else if (c < 0xD800 || c >= 0xE000) {
|
||||
bytes[0] = (uint8_t)(0xE0 | (c >> 12)), bytes[1] = (uint8_t)(0x80 | ((c >> 6) & 0x3F));
|
||||
bytes[2] = (uint8_t)(0x80 | (c & 0x3F));
|
||||
nb = 3;
|
||||
} else {
|
||||
if (++i >= n) {
|
||||
free(units);
|
||||
scr_str_release(scr_jb_finish(&out));
|
||||
scr_throw_error_msg_code(SCR_ERR_URI, "URI malformed", 13, "ERR_INVALID_URI");
|
||||
return NULL;
|
||||
}
|
||||
c = 0x10000 + (((c & 0x3FF) << 10) | (units[i] & 0x3FF));
|
||||
bytes[0] = (uint8_t)(0xF0 | (c >> 18)), bytes[1] = (uint8_t)(0x80 | ((c >> 12) & 0x3F));
|
||||
bytes[2] = (uint8_t)(0x80 | ((c >> 6) & 0x3F)), bytes[3] = (uint8_t)(0x80 | (c & 0x3F));
|
||||
nb = 4;
|
||||
}
|
||||
for (size_t b = 0; b < nb; b++) esc[k++] = '%', esc[k++] = hex[bytes[b] >> 4], esc[k++] = hex[bytes[b] & 0xF];
|
||||
}
|
||||
scr_jb_write_utf8(&out, esc, k);
|
||||
}
|
||||
free(units);
|
||||
return scr_jb_finish(&out);
|
||||
}
|
||||
|
||||
/* querystring.escape: the component encoder, with Node's encodeStr
|
||||
* surrogate handling when the string holds a lone surrogate. */
|
||||
ScrStr *scr_qs_escape(ScrStr *s) {
|
||||
if (scr_str_has_surrogate(s)) {
|
||||
scr_throw_error_msg_code(SCR_ERR_URI, "URI malformed", 13, "ERR_INVALID_URI");
|
||||
return scr_str_empty();
|
||||
/* lib/querystring.js noEscape: the component unreserved set. */
|
||||
static const signed char unreserved[128] = {
|
||||
['!'] = 1, ['\''] = 1, ['('] = 1, [')'] = 1, ['*'] = 1, ['-'] = 1, ['.'] = 1, ['_'] = 1, ['~'] = 1,
|
||||
['0'] = 1, ['1'] = 1, ['2'] = 1, ['3'] = 1, ['4'] = 1, ['5'] = 1, ['6'] = 1, ['7'] = 1, ['8'] = 1,
|
||||
['9'] = 1, ['A'] = 1, ['B'] = 1, ['C'] = 1, ['D'] = 1, ['E'] = 1, ['F'] = 1, ['G'] = 1, ['H'] = 1,
|
||||
['I'] = 1, ['J'] = 1, ['K'] = 1, ['L'] = 1, ['M'] = 1, ['N'] = 1, ['O'] = 1, ['P'] = 1, ['Q'] = 1,
|
||||
['R'] = 1, ['S'] = 1, ['T'] = 1, ['U'] = 1, ['V'] = 1, ['W'] = 1, ['X'] = 1, ['Y'] = 1, ['Z'] = 1,
|
||||
['a'] = 1, ['b'] = 1, ['c'] = 1, ['d'] = 1, ['e'] = 1, ['f'] = 1, ['g'] = 1, ['h'] = 1, ['i'] = 1,
|
||||
['j'] = 1, ['k'] = 1, ['l'] = 1, ['m'] = 1, ['n'] = 1, ['o'] = 1, ['p'] = 1, ['q'] = 1, ['r'] = 1,
|
||||
['s'] = 1, ['t'] = 1, ['u'] = 1, ['v'] = 1, ['w'] = 1, ['x'] = 1, ['y'] = 1, ['z'] = 1,
|
||||
};
|
||||
ScrStr *out = scr_qs_encode_str(s, unreserved);
|
||||
return out ? out : scr_str_empty();
|
||||
}
|
||||
return scr_str_encode_uri_component(s);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,592 @@
|
||||
// Generates node:url legacy-API and domain test cases with Node as the
|
||||
// oracle.
|
||||
//
|
||||
// node gen-url-legacy-cases.mjs > url-legacy-cases.txt
|
||||
//
|
||||
// Lines are tab-separated:
|
||||
// a <domain> <url.domainToASCII(domain)>
|
||||
// u <domain> <url.domainToUnicode(domain)>
|
||||
// p <flags> <input> <JSON of url.parse(input, flags & 1, flags & 2)>
|
||||
// f <input> <url.format(input)>
|
||||
// o <JSON object> <url.format(object)>
|
||||
// r <from> <to> <url.resolve(from, to)>
|
||||
// A throwing call answers "E:" and the error's code (or name). Strings keep
|
||||
// printable ASCII and spell everything else (and "\") as \uXXXX UTF-16 code
|
||||
// units, so lone surrogates survive. Node v24 is required.
|
||||
import url from "node:url";
|
||||
import { stdout } from "node:process";
|
||||
|
||||
process.noDeprecation = true;
|
||||
|
||||
const escape = (s) =>
|
||||
s.replace(/[^\x20-\x5b\x5d-\x7e]/g, (c) => "\\u" + c.charCodeAt(0).toString(16).padStart(4, "0"));
|
||||
const attempt = (f) => {
|
||||
try {
|
||||
return f();
|
||||
} catch (e) {
|
||||
return `E:${e.code ?? e.name}`;
|
||||
}
|
||||
};
|
||||
const lines = [];
|
||||
const seen = new Set();
|
||||
const emit = (...fields) => {
|
||||
const line = fields.map(escape).join("\t");
|
||||
if (seen.has(line)) return;
|
||||
seen.add(line);
|
||||
lines.push(line);
|
||||
};
|
||||
const domain = (d) => {
|
||||
emit(
|
||||
"a",
|
||||
d,
|
||||
attempt(() => url.domainToASCII(d)),
|
||||
);
|
||||
emit(
|
||||
"u",
|
||||
d,
|
||||
attempt(() => url.domainToUnicode(d)),
|
||||
);
|
||||
};
|
||||
const parse = (input) => {
|
||||
for (const flags of [0, 1, 2, 3])
|
||||
emit(
|
||||
"p",
|
||||
String(flags),
|
||||
input,
|
||||
attempt(() => JSON.stringify(url.parse(input, (flags & 1) !== 0, (flags & 2) !== 0))),
|
||||
);
|
||||
emit(
|
||||
"f",
|
||||
input,
|
||||
attempt(() => url.format(input)),
|
||||
);
|
||||
};
|
||||
const resolve = (from, to) =>
|
||||
emit(
|
||||
"r",
|
||||
from,
|
||||
to,
|
||||
attempt(() => url.resolve(from, to)),
|
||||
);
|
||||
const format = (object) =>
|
||||
emit(
|
||||
"o",
|
||||
JSON.stringify(object),
|
||||
attempt(() => url.format(object)),
|
||||
);
|
||||
|
||||
// ── curated domains ─────────────────────────────────────────────────
|
||||
const domains = [
|
||||
"",
|
||||
"example.com",
|
||||
"EXAMPLE.COM",
|
||||
"español.com",
|
||||
"xn--espaol-zwa.com",
|
||||
"XN--ESPAOL-ZWA.COM",
|
||||
"中文.com",
|
||||
"xn--fiq228c.com",
|
||||
"日本語。JP",
|
||||
"ff.com",
|
||||
"faß.de",
|
||||
"xn--fa-hia.de",
|
||||
"ab--c.com",
|
||||
"-x.com",
|
||||
"x-.com",
|
||||
"xn--.com",
|
||||
"xn--a.com",
|
||||
"xn--xn--a.com",
|
||||
"xn--ls8h.la",
|
||||
"💩.la",
|
||||
"a\u200cb.com",
|
||||
"\u0915\u094d\u200c.com",
|
||||
"a\u200db.com",
|
||||
"\u0628\u200c\u0628.com",
|
||||
"\u0628.com",
|
||||
"\u05d0\u05d1.com",
|
||||
"a\u05d0.com",
|
||||
"\u05d01.com",
|
||||
"1\u05d0.com",
|
||||
"\u0628\u0661\u0031.com",
|
||||
"\u0301a.com",
|
||||
"a\u0301.com",
|
||||
"a.b.c.",
|
||||
".",
|
||||
"..",
|
||||
"a..b",
|
||||
".a",
|
||||
"1.2.3.4",
|
||||
"1.2.3",
|
||||
"0x7f.1",
|
||||
"0300.0250.1.1",
|
||||
"4294967295",
|
||||
"4294967296",
|
||||
"1.2.3.256",
|
||||
"1.2.3.4.5",
|
||||
"1.2.3.4.",
|
||||
"08.1.1.1",
|
||||
"0x.1.1.1",
|
||||
"a.1",
|
||||
"1.a",
|
||||
"a.0x",
|
||||
"[::1]",
|
||||
"[::]",
|
||||
"[1:2:3:4:5:6:7:8]",
|
||||
"[1:0:0:0:0:0:0:1]",
|
||||
"[1::2:0:0:3]",
|
||||
"[0:0:1:0:0:0:0:0]",
|
||||
"[::ffff:1.2.3.4]",
|
||||
"[::1.2.3]",
|
||||
"[1:2:3:4:5:6:7:8:9]",
|
||||
"[:1]",
|
||||
"[1:]",
|
||||
"[::1",
|
||||
"[ABCD::EF]",
|
||||
"a%2Eb",
|
||||
"%E4%B8%AD.com",
|
||||
"%zz.com",
|
||||
"a%",
|
||||
"a b.com",
|
||||
"a:80",
|
||||
"a/b",
|
||||
"a?b",
|
||||
"a#b",
|
||||
"a\\b",
|
||||
"a\tb\nc\rd",
|
||||
"a@b",
|
||||
"a^b",
|
||||
"a|b",
|
||||
"a<b",
|
||||
"a_b.com",
|
||||
"a\u00adb.com",
|
||||
"\u00ad",
|
||||
"a\ufffdb",
|
||||
"a\ud800b",
|
||||
"\u2488.com",
|
||||
"\u2474.com",
|
||||
"Ⅻ.com",
|
||||
"$.com",
|
||||
"\u0130stanbul.com",
|
||||
"Dž.com",
|
||||
"\u3002",
|
||||
"a\u3002b\uff0ec\uff61d",
|
||||
"\uff21\uff22\uff23",
|
||||
"\u{1d400}.com",
|
||||
"localhost",
|
||||
"LOCALHOST",
|
||||
"x".repeat(70) + ".com",
|
||||
"é".repeat(70) + ".com",
|
||||
];
|
||||
for (const d of domains) domain(d);
|
||||
|
||||
// ── curated legacy URLs ─────────────────────────────────────────────
|
||||
const urls = [
|
||||
"",
|
||||
" ",
|
||||
"\t",
|
||||
"http://user:pw@Example.com:8080/p/a/t/h?query=string#hash",
|
||||
"HTTP://www.Example.COM/",
|
||||
"http://www.example.com",
|
||||
"http://x.com/path?that's#all, folks",
|
||||
"http://X.COM/Y",
|
||||
"http://x.y.com+a/b/c",
|
||||
"HtTp://x.y.cOm;a/b/c?d=e#f g<h>i",
|
||||
"HtTp://x.y.cOm;A/b/c?d=e#f g<h>i",
|
||||
"http://x...y...#p",
|
||||
'http://x/p/"quoted"',
|
||||
"<http://goo.corn/bread> Is a URL!",
|
||||
"http://www.narwhaljs.org/blog/categories?id=news",
|
||||
"http://mt0.google.com/vt/lyrs=m@114&hl=en&src=api&x=2&y=2&z=3&s=",
|
||||
"http://mt0.google.com/vt/lyrs=m@114???&hl=en&src=api&x=2&y=2&z=3&s=",
|
||||
"http://user:pass@mt0.google.com/vt/lyrs=m@114???&hl=en&src=api&x=2&y=2&z=3&s=",
|
||||
"file:///etc/passwd",
|
||||
"file://localhost/etc/passwd",
|
||||
"file://foo/etc/passwd",
|
||||
"file:///etc/node/",
|
||||
"file://localhost/etc/node/",
|
||||
"file://foo/etc/node/",
|
||||
"file:.",
|
||||
"file:",
|
||||
"http:/baz/../foo/bar",
|
||||
"http://user:pass@example.com:8000/foo/bar?baz=quux#frag",
|
||||
"//user:pass@example.com:8000/foo/bar?baz=quux#frag",
|
||||
"/foo/bar?baz=quux#frag",
|
||||
"http:/foo/bar?baz=quux#frag",
|
||||
"mailto:foo@bar.com?subject=hello",
|
||||
"javascript:alert('hello');",
|
||||
"xmpp:isaacschlueter@jabber.org",
|
||||
"http://atpass:foo%40bar@127.0.0.1:8080/path?search=foo#bar",
|
||||
"svn+ssh://foo/bar",
|
||||
"dash-test://foo/bar",
|
||||
"dash-test:foo/bar",
|
||||
"dot.test://foo/bar",
|
||||
"dot.test:foo/bar",
|
||||
"http://www.日本語.com/",
|
||||
"http://example.Bücher.com/",
|
||||
"http://www.Äffchen.com/",
|
||||
"http://www.Äffchen.cOm;A/b/c?d=e#f g<h>i",
|
||||
"http://SÉLIER.COM/",
|
||||
"http://ﻞﻴﻬﻣﺎﻠﻤﻳﺓ.ﻱ؟/",
|
||||
"http://➡.ws/➡",
|
||||
"http://bucket_name.s3.amazonaws.com/image.jpg",
|
||||
"git+http://github.com/joyent/node.git",
|
||||
"local1@domain1",
|
||||
"www.example.com",
|
||||
"[fe80::1]",
|
||||
"coap://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]",
|
||||
"coap://[1080:0:0:0:8:800:200C:417A]:61616/",
|
||||
"http://[2001:db8:85a3:8d3:1319:8a2e:370:7348]:8080/",
|
||||
"http://[::1]/x",
|
||||
"http://[x]",
|
||||
"http://[::1",
|
||||
"http://[::1]x/",
|
||||
"http://user:password@[3ffe:2a00:100:7031::1]:8080",
|
||||
"coap://u:p@[::192.9.5.5]:61616/.well-known/r?n=Temperature",
|
||||
"http://example.com:",
|
||||
"http://example.com:/a/b.html",
|
||||
"http://example.com?foo=bar#frag",
|
||||
"http://example.com?foo=@bar#frag",
|
||||
"http://example.com?foo=/bar/#frag",
|
||||
"http://example.com?foo=?bar/#frag",
|
||||
"http://example.com#frag=?bar/#frag",
|
||||
'http://google.com" onload="alert(42)/',
|
||||
"http://a.com/a/b/c?s#h",
|
||||
"http://atpass:foo%40bar@127.0.0.1/",
|
||||
"http://atslash%2F%40:%2F%40@foo/",
|
||||
"http://www.example.com/?a=1&b=2&a=3&c&=d&e=&%20f=%25+g",
|
||||
"http://a.com/?__proto__=1&constructor=2&2=x&1=y",
|
||||
"http://a.com?x[]=1&x[]=2",
|
||||
"http://a\r\" \t\n<'b:b@c\r\nd/e?f",
|
||||
"https://*",
|
||||
"http://a@b@c/",
|
||||
"http://a@b?@c",
|
||||
"http://a.b\\c/d",
|
||||
"http:\\\\evil.com\\foo\\bar",
|
||||
"http://a\\b@c/",
|
||||
"\\\\server\\share\\file",
|
||||
"/foo\\bar\\baz?x\\y#z\\w",
|
||||
" http://example.com/ ",
|
||||
"\u00a0http://example.com/\ufeff",
|
||||
"\ufeff\u00a0",
|
||||
"http://a:b:c/",
|
||||
"http://a:1:2/",
|
||||
"http://a.com:65536/",
|
||||
"http://a.com:123456789012345/",
|
||||
"http://a%20b/",
|
||||
"http://xn--zz/",
|
||||
"http://xn--espaol-zwa.com/",
|
||||
"http://\u0130.com/",
|
||||
"http://a\u00adb/",
|
||||
"http://\u00ad/",
|
||||
"http://a\ud800b/",
|
||||
"http://\u{1f4a9}/",
|
||||
"http://" + "a".repeat(254) + "/",
|
||||
"http://" + "a".repeat(256) + "/",
|
||||
"http://" + "é".repeat(255) + "/",
|
||||
"http://a.com/\u00e9?\u00e9#\u00e9",
|
||||
"http://a.com/\ud800",
|
||||
"http://a.com/a b?c d#e f",
|
||||
"http://a.com/{}|\\^`?{}|\\^`#{}|\\^`",
|
||||
"javascript:a b{}",
|
||||
"JavaScript://a/b",
|
||||
"javascript://a/b c",
|
||||
"HTTP:foo",
|
||||
"http:foo",
|
||||
"Mailto:x@y",
|
||||
"file:///C:/x",
|
||||
"file://C:/x",
|
||||
"//a.com/b",
|
||||
"///a",
|
||||
"////a",
|
||||
"//",
|
||||
"/",
|
||||
"/?",
|
||||
"?",
|
||||
"#",
|
||||
"?a=1",
|
||||
"#a",
|
||||
"a",
|
||||
"a/b",
|
||||
"a?b",
|
||||
"./a",
|
||||
"../a",
|
||||
"/a b",
|
||||
"/a\u2028b",
|
||||
"/a\u3000b?c",
|
||||
"//a b",
|
||||
"//a/b c",
|
||||
"/\u00a0",
|
||||
"@",
|
||||
"a@b",
|
||||
"//a@b/c",
|
||||
"//@/",
|
||||
"http://@/",
|
||||
"http://%zz@x/",
|
||||
"http://a%40b@c/",
|
||||
"http://\u00e9@c/",
|
||||
"http://a:@c/",
|
||||
"http://:b@c/",
|
||||
"http://@c/",
|
||||
"http://;@c/",
|
||||
"http:///x",
|
||||
"http://",
|
||||
"https:",
|
||||
"ftp://a/",
|
||||
"gopher://a",
|
||||
"ws://a?b",
|
||||
"wss://a#b",
|
||||
"foo://a",
|
||||
"foo:",
|
||||
"1foo://a",
|
||||
"+a:b",
|
||||
"a+b.c-d:e",
|
||||
"-:x",
|
||||
];
|
||||
for (const u of urls) parse(u);
|
||||
|
||||
const bases = [
|
||||
"http://a/b/c/d;p?q",
|
||||
"http://a.com/b/c",
|
||||
"http://a.com/b/c/",
|
||||
"http://user:pw@a.com:81/b?x#y",
|
||||
"http://a.com",
|
||||
"https://a.com/b",
|
||||
"mailto:local1@domain1",
|
||||
"mailto:a@b/c",
|
||||
"file:///a/b",
|
||||
"file://host/a/b",
|
||||
"javascript:x",
|
||||
"/a/b/c",
|
||||
"a/b/c",
|
||||
"",
|
||||
"#x",
|
||||
"?q",
|
||||
"//a/b",
|
||||
"foo://a/b",
|
||||
"foo:a/b",
|
||||
"http://[::1]/a",
|
||||
];
|
||||
const relatives = [
|
||||
"",
|
||||
"g",
|
||||
"./g",
|
||||
"g/",
|
||||
"/g",
|
||||
"//g",
|
||||
"?y",
|
||||
"g?y",
|
||||
"#s",
|
||||
"g#s",
|
||||
"g?y#s",
|
||||
";x",
|
||||
"g;x",
|
||||
".",
|
||||
"./",
|
||||
"..",
|
||||
"../",
|
||||
"../g",
|
||||
"../..",
|
||||
"../../",
|
||||
"../../g",
|
||||
"../../../g",
|
||||
"../../../../g",
|
||||
"/./g",
|
||||
"/../g",
|
||||
"g.",
|
||||
".g",
|
||||
"g..",
|
||||
"..g",
|
||||
"./../g",
|
||||
"./g/.",
|
||||
"g/./h",
|
||||
"g/../h",
|
||||
"http:g",
|
||||
"http://b/c",
|
||||
"https://b",
|
||||
"https:b",
|
||||
"mailto:x@y",
|
||||
"mailto:local2@domain2",
|
||||
"local2@domain2",
|
||||
"file:///x",
|
||||
"file:x",
|
||||
"foo:x",
|
||||
"foo://x/y",
|
||||
"//user:pw@b/c",
|
||||
"//b",
|
||||
"///c",
|
||||
"http://u@x",
|
||||
"\\g",
|
||||
" g ",
|
||||
];
|
||||
for (const b of bases) for (const r of relatives) resolve(b, r);
|
||||
|
||||
const objects = [
|
||||
{},
|
||||
{ protocol: "http", host: "a.com" },
|
||||
{ protocol: "http:", hostname: "a.com", port: 8080, pathname: "x" },
|
||||
{ protocol: "http:", hostname: "a.com", port: "8080", pathname: "/x" },
|
||||
{ protocol: "file", pathname: "/x" },
|
||||
{ protocol: "file:" },
|
||||
{ protocol: "files:" },
|
||||
{ protocol: "mailto", auth: "a", hostname: "b" },
|
||||
{ protocol: "mailto", pathname: "a@b" },
|
||||
{ protocol: "foo", slashes: true, host: "h", pathname: "p" },
|
||||
{ protocol: "foo", host: "h", pathname: "p" },
|
||||
{ hostname: "::1", port: 80 },
|
||||
{ hostname: "[::1]", port: 80 },
|
||||
{ host: "h", hostname: "ignored", port: 1 },
|
||||
{ auth: "user:p@ss w/rd", host: "h" },
|
||||
{ auth: "\u00e9\u{1f4a9}", host: "h" },
|
||||
{ auth: "a", pathname: "/p" },
|
||||
{ pathname: "a#b?c" },
|
||||
{ pathname: "#a", protocol: "http:", host: "h" },
|
||||
{ search: "a=1" },
|
||||
{ search: "?a=1#b" },
|
||||
{ search: "#x" },
|
||||
{ query: { a: 1, b: [2, 3], c: null, d: true, e: "x y" } },
|
||||
{ query: "a=1" },
|
||||
{ query: {}, search: "" },
|
||||
{ query: { a: "1" }, search: "?b=2" },
|
||||
{ hash: "x" },
|
||||
{ hash: "#x" },
|
||||
{ protocol: "http:", host: "a", pathname: "/", search: "q", hash: "h" },
|
||||
{ protocol: "HTTP", host: "a", pathname: "b" },
|
||||
{ protocol: "Http:", slashes: false, host: "a", pathname: "b" },
|
||||
{ protocol: 5, host: 6, pathname: 7 },
|
||||
{ protocol: "http", hostname: "", port: 1, pathname: "/x" },
|
||||
{ slashes: true, pathname: "x" },
|
||||
{ slashes: 1, hostname: "h" },
|
||||
{ protocol: "http", auth: "", host: "", hash: "", search: "", pathname: "" },
|
||||
{ href: "http://ignored/", pathname: "/kept" },
|
||||
];
|
||||
for (const o of objects) format(o);
|
||||
|
||||
// ── seeded fuzz ─────────────────────────────────────────────────────
|
||||
let seed = 0x5ca1ab1e;
|
||||
const random = () => {
|
||||
seed ^= seed << 13;
|
||||
seed ^= seed >>> 17;
|
||||
seed ^= seed << 5;
|
||||
return (seed >>> 0) / 0x100000000;
|
||||
};
|
||||
const pick = (list) => list[Math.floor(random() * list.length)];
|
||||
const urlPieces = [
|
||||
"http:",
|
||||
"HTTP:",
|
||||
"https:",
|
||||
"file:",
|
||||
"mailto:",
|
||||
"javascript:",
|
||||
"foo:",
|
||||
"ws:",
|
||||
"//",
|
||||
"/",
|
||||
"\\",
|
||||
"?",
|
||||
"#",
|
||||
"@",
|
||||
":",
|
||||
";",
|
||||
".",
|
||||
"..",
|
||||
"a",
|
||||
"B",
|
||||
"c.com",
|
||||
"x-y",
|
||||
"8080",
|
||||
"%",
|
||||
"%41",
|
||||
"%zz",
|
||||
"%C3%A9",
|
||||
" ",
|
||||
"\t",
|
||||
"\n",
|
||||
"\u00a0",
|
||||
"\ufeff",
|
||||
"\u3000",
|
||||
"é",
|
||||
"ß",
|
||||
"中",
|
||||
"\u{1f4a9}",
|
||||
"\u200d",
|
||||
"\u0301",
|
||||
"[",
|
||||
"]",
|
||||
"::1",
|
||||
"1.2.3.4",
|
||||
"xn--",
|
||||
"xn--fiq228c",
|
||||
'"',
|
||||
"'",
|
||||
"<",
|
||||
">",
|
||||
"{",
|
||||
"}",
|
||||
"|",
|
||||
"^",
|
||||
"`",
|
||||
"=",
|
||||
"&",
|
||||
"+",
|
||||
"\ud800",
|
||||
"\u0130",
|
||||
"\u05d0",
|
||||
"\u0628",
|
||||
];
|
||||
const domainPieces = [
|
||||
"a",
|
||||
"B",
|
||||
"-",
|
||||
"_",
|
||||
".",
|
||||
"。",
|
||||
"xn--",
|
||||
"xn--zz",
|
||||
"xn--fiq228c",
|
||||
"1",
|
||||
"0x",
|
||||
"255",
|
||||
"256",
|
||||
"%",
|
||||
"%2e",
|
||||
"%41",
|
||||
"é",
|
||||
"ß",
|
||||
"ẞ",
|
||||
"中",
|
||||
"\u{1f4a9}",
|
||||
"\u200c",
|
||||
"\u200d",
|
||||
"\u094d",
|
||||
"\u0915",
|
||||
"\u0301",
|
||||
"\u05d0",
|
||||
"\u0628",
|
||||
"\u0661",
|
||||
"1",
|
||||
"\u00ad",
|
||||
":",
|
||||
"[",
|
||||
"]",
|
||||
"::",
|
||||
" ",
|
||||
"\t",
|
||||
"/",
|
||||
"?",
|
||||
"#",
|
||||
"@",
|
||||
"\ud800",
|
||||
"A",
|
||||
"ff",
|
||||
"\u2488",
|
||||
];
|
||||
const build = (pieces, max) => {
|
||||
let s = "";
|
||||
const count = 1 + Math.floor(random() * max);
|
||||
for (let i = 0; i < count; i++) s += pick(pieces);
|
||||
return s;
|
||||
};
|
||||
for (let i = 0; i < 1500; i++) domain(build(domainPieces, 8));
|
||||
for (let i = 0; i < 1500; i++) parse(build(urlPieces, 10));
|
||||
for (let i = 0; i < 1500; i++) resolve(pick([...bases, build(urlPieces, 6)]), build(urlPieces, 6));
|
||||
|
||||
stdout.write(lines.join("\n") + "\n");
|
||||
@@ -0,0 +1,208 @@
|
||||
/* Oracle test for node:url's legacy API and the domain pair.
|
||||
* Reads the case file given as argv[1] (line format in
|
||||
* gen-url-legacy-cases.mjs) and compares url.domainToASCII/domainToUnicode,
|
||||
* url.parse (as its JSON), url.format over strings and objects, and
|
||||
* url.resolve with Node's answers; a throwing call answers "E:" and the
|
||||
* error's code (or name).
|
||||
*
|
||||
* Exit 0 = all pass; prints each mismatch (capped) and exits 1 otherwise.
|
||||
*/
|
||||
#include "../src/scr_runtime.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
long scr_str_live_count(void); /* provided by scr_string.c */
|
||||
|
||||
/* Warnings are not part of the answers (DEP0169 is off: warn = false;
|
||||
* DEP0170 still fires); the deprecation unit stays out. */
|
||||
ScrDyn *scr_warning_new(ScrStr *message, const char *type, const ScrStr *code) {
|
||||
(void)message;
|
||||
(void)type;
|
||||
(void)code;
|
||||
return scr_dyn_new_null();
|
||||
}
|
||||
void scr_warning_emit_deferred(ScrDyn *warning) { (void)warning; }
|
||||
|
||||
static int hex_val(char c) {
|
||||
if (c >= '0' && c <= '9') return c - '0';
|
||||
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* An escaped case string (printable ASCII, \uXXXX code units) to a string
|
||||
* in the runtime's WTF-8 storage. */
|
||||
static ScrStr *decode(const char *text) {
|
||||
size_t len = strlen(text);
|
||||
uint16_t *units = malloc((len ? len : 1) * sizeof *units);
|
||||
size_t n = 0;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
if (text[i] == '\\' && text[i + 1] == 'u') {
|
||||
const char *h = text + i + 2;
|
||||
units[n++] = (uint16_t)(hex_val(h[0]) << 12 | hex_val(h[1]) << 8 | hex_val(h[2]) << 4 | hex_val(h[3]));
|
||||
i += 5;
|
||||
} else {
|
||||
units[n++] = (uint8_t)text[i];
|
||||
}
|
||||
}
|
||||
char *out = malloc(3 * n + 1);
|
||||
size_t w = 0;
|
||||
bool lone = false;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
uint32_t cp = units[i];
|
||||
if (cp >= 0xD800 && cp < 0xDC00 && i + 1 < n && units[i + 1] >= 0xDC00 && units[i + 1] < 0xE000) {
|
||||
cp = 0x10000 + ((cp - 0xD800) << 10) + (units[i + 1] - 0xDC00);
|
||||
i++;
|
||||
} else if (cp >= 0xD800 && cp < 0xE000) {
|
||||
lone = true;
|
||||
}
|
||||
if (cp < 0x80) {
|
||||
out[w++] = (char)cp;
|
||||
} else if (cp < 0x800) {
|
||||
out[w++] = (char)(0xC0 | (cp >> 6));
|
||||
out[w++] = (char)(0x80 | (cp & 0x3F));
|
||||
} else if (cp < 0x10000) {
|
||||
out[w++] = (char)(0xE0 | (cp >> 12));
|
||||
out[w++] = (char)(0x80 | ((cp >> 6) & 0x3F));
|
||||
out[w++] = (char)(0x80 | (cp & 0x3F));
|
||||
} else {
|
||||
out[w++] = (char)(0xF0 | (cp >> 18));
|
||||
out[w++] = (char)(0x80 | ((cp >> 12) & 0x3F));
|
||||
out[w++] = (char)(0x80 | ((cp >> 6) & 0x3F));
|
||||
out[w++] = (char)(0x80 | (cp & 0x3F));
|
||||
}
|
||||
}
|
||||
if (lone) scr_wtf8_note();
|
||||
ScrStr *s = scr_str_new(out, w);
|
||||
free(out);
|
||||
free(units);
|
||||
return s;
|
||||
}
|
||||
|
||||
/* Splits a line at tabs in place; empty fields are kept. */
|
||||
static int split(char *line, char **fields, int max) {
|
||||
int count = 0;
|
||||
fields[count++] = line;
|
||||
for (char *c = line; *c; c++)
|
||||
if (*c == '\t') {
|
||||
*c = 0;
|
||||
if (count == max) return -1;
|
||||
fields[count++] = c + 1;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/* "E:<code or name>" for the pending exception (+1). */
|
||||
static ScrStr *error_outcome(void) {
|
||||
if (!scr_exc_pending()) return scr_str_new("E:<none pending>", 16);
|
||||
ScrCaught *c = scr_exc_take();
|
||||
ScrError *error = c->payload;
|
||||
ScrStr *label = error->code ? scr_str_retain(error->code) : scr_error_default_name(error);
|
||||
ScrStr *prefix = scr_str_new("E:", 2);
|
||||
ScrStr *out = scr_str_concat(prefix, label);
|
||||
scr_str_release(prefix);
|
||||
scr_str_release(label);
|
||||
scr_caught_release(c);
|
||||
return out;
|
||||
}
|
||||
|
||||
static ScrStr *string_outcome(ScrStr *result) { return result ? result : error_outcome(); }
|
||||
|
||||
static ScrStr *json_outcome(ScrDyn *result) {
|
||||
if (!result) return error_outcome();
|
||||
ScrJsonBuf b;
|
||||
scr_jb_init(&b);
|
||||
scr_jb_put_dyn(&b, result);
|
||||
scr_dyn_release(result);
|
||||
return scr_jb_finish(&b);
|
||||
}
|
||||
|
||||
static long total, failed;
|
||||
|
||||
static void check(ScrStr *got, const char *expected_text, const char *kind, const char *a, const char *b) {
|
||||
ScrStr *expected = decode(expected_text);
|
||||
total++;
|
||||
if (got->len != expected->len || memcmp(got->data, expected->data, got->len) != 0) {
|
||||
failed++;
|
||||
if (failed <= 40)
|
||||
fprintf(stderr, "MISMATCH %s \"%s\"%s%s%s\n got %.*s\n want %.*s\n", kind, a, b ? " \"" : "",
|
||||
b ? b : "", b ? "\"" : "", (int)got->len, got->data, (int)expected->len, expected->data);
|
||||
}
|
||||
scr_str_release(expected);
|
||||
scr_str_release(got);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc < 2) return 2;
|
||||
FILE *f = fopen(argv[1], "r");
|
||||
if (!f) return 2;
|
||||
static char line[1 << 20];
|
||||
ScrDyn *no = scr_dyn_new_bool(false);
|
||||
ScrDyn *yes = scr_dyn_new_bool(true);
|
||||
while (fgets(line, sizeof line, f)) {
|
||||
size_t len = strlen(line);
|
||||
if (len && line[len - 1] == '\n') line[--len] = 0;
|
||||
if (!len) continue;
|
||||
char *fields[5];
|
||||
int count = split(line, fields, 5);
|
||||
const char *kind = fields[0];
|
||||
if ((strcmp(kind, "a") == 0 || strcmp(kind, "u") == 0) && count == 3) {
|
||||
ScrStr *input = decode(fields[1]);
|
||||
ScrDyn *boxed = scr_dyn_new_str(input);
|
||||
ScrStr *got = kind[0] == 'a' ? scr_url_domain_to_ascii(boxed, false)
|
||||
: scr_url_domain_to_unicode(boxed, false);
|
||||
check(string_outcome(got), fields[2], kind, fields[1], NULL);
|
||||
scr_dyn_release(boxed);
|
||||
scr_str_release(input);
|
||||
} else if (strcmp(kind, "p") == 0 && count == 4) {
|
||||
int flags = atoi(fields[1]);
|
||||
ScrStr *input = decode(fields[2]);
|
||||
ScrDyn *boxed = scr_dyn_new_str(input);
|
||||
ScrDyn *got = scr_url_legacy_parse(boxed, flags & 1 ? yes : no, flags & 2 ? yes : no, false);
|
||||
check(json_outcome(got), fields[3], flags & 1 ? (flags & 2 ? "p3" : "p1") : flags & 2 ? "p2" : "p0",
|
||||
fields[2], NULL);
|
||||
scr_dyn_release(boxed);
|
||||
scr_str_release(input);
|
||||
} else if (strcmp(kind, "f") == 0 && count == 3) {
|
||||
ScrStr *input = decode(fields[1]);
|
||||
ScrDyn *boxed = scr_dyn_new_str(input);
|
||||
check(string_outcome(scr_url_legacy_format(boxed, scr_dyn_undefined(), false)), fields[2], kind,
|
||||
fields[1], NULL);
|
||||
scr_dyn_release(boxed);
|
||||
scr_str_release(input);
|
||||
} else if (strcmp(kind, "o") == 0 && count == 3) {
|
||||
ScrStr *text = decode(fields[1]);
|
||||
ScrDyn *object = scr_json_parse(text);
|
||||
check(string_outcome(scr_url_legacy_format(object, scr_dyn_undefined(), false)), fields[2], kind,
|
||||
fields[1], NULL);
|
||||
scr_dyn_release(object);
|
||||
scr_str_release(text);
|
||||
} else if (strcmp(kind, "r") == 0 && count == 4) {
|
||||
ScrStr *from = decode(fields[1]);
|
||||
ScrStr *to = decode(fields[2]);
|
||||
ScrDyn *from_boxed = scr_dyn_new_str(from);
|
||||
ScrDyn *to_boxed = scr_dyn_new_str(to);
|
||||
check(string_outcome(scr_url_legacy_resolve(from_boxed, to_boxed, false)), fields[3], kind, fields[1],
|
||||
fields[2]);
|
||||
scr_dyn_release(from_boxed);
|
||||
scr_dyn_release(to_boxed);
|
||||
scr_str_release(from);
|
||||
scr_str_release(to);
|
||||
} else {
|
||||
fprintf(stderr, "bad line: %s\n", kind);
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
fclose(f);
|
||||
scr_dyn_release(no);
|
||||
scr_dyn_release(yes);
|
||||
#ifdef SCR_RC_AUDIT
|
||||
if (scr_str_live_count() != 0) {
|
||||
fprintf(stderr, "live strings at exit: %ld\n", scr_str_live_count());
|
||||
failed++;
|
||||
}
|
||||
#endif
|
||||
fprintf(stderr, "%ld/%ld cases passed\n", total - failed, total);
|
||||
return failed == 0 ? 0 : 1;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,73 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { beforeAll, expect, test } from "vitest";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const testDir = import.meta.dirname;
|
||||
const bin = join(testDir, "build", "test_url_legacy");
|
||||
const vendorDir = join(testDir, "../vendor/quickjs-ng");
|
||||
|
||||
// Compiled once with ASan + the RC audit: the oracle run also proves the
|
||||
// legacy url port neither leaks nor double-frees across the case file's
|
||||
// calls, thrown errors included. Hostnames lowercase and normalize through
|
||||
// the vendored libunicode, as in programs.
|
||||
beforeAll(async () => {
|
||||
await mkdir(join(testDir, "build"), { recursive: true });
|
||||
await execFileAsync("clang", [
|
||||
// no -Wall/-Wextra: the vendored libunicode.c is not warning-clean
|
||||
"-std=c11",
|
||||
"-O1",
|
||||
"-fsanitize=address",
|
||||
"-DSCR_RC_AUDIT",
|
||||
"-I",
|
||||
vendorDir,
|
||||
"-o",
|
||||
bin,
|
||||
join(testDir, "test_url_legacy.c"),
|
||||
join(testDir, "../src/scr_url_legacy.c"),
|
||||
join(testDir, "../src/scr_idna.c"),
|
||||
join(testDir, "../src/scr_qs.c"),
|
||||
join(testDir, "../src/scr_regex.c"),
|
||||
// scr_regex.c hosts assert.match and calls the assert throw/inspect
|
||||
// helpers — regex-linking binaries always carry scr_assert.c.
|
||||
join(testDir, "../src/scr_assert.c"),
|
||||
join(vendorDir, "libregexp.c"),
|
||||
join(vendorDir, "libunicode.c"),
|
||||
join(testDir, "../src/scr_string.c"),
|
||||
join(testDir, "../src/scr_map.c"),
|
||||
join(testDir, "../src/scr_array.c"),
|
||||
join(testDir, "../src/scr_json.c"),
|
||||
join(testDir, "../src/scr_bigint.c"),
|
||||
join(testDir, "../src/scr_exception.c"),
|
||||
join(testDir, "../src/scr_error.c"),
|
||||
join(testDir, "../src/scr_number.c"),
|
||||
join(testDir, "../src/scr_console.c"),
|
||||
join(testDir, "../src/scr_closure.c"),
|
||||
join(testDir, "../src/scr_object.c"),
|
||||
join(testDir, "../src/scr_union.c"),
|
||||
join(testDir, "../src/scr_cycle.c"),
|
||||
join(testDir, "../src/scr_alloc.c"),
|
||||
join(testDir, "../src/scr_lib.c"),
|
||||
join(testDir, "../src/scr_url.c"),
|
||||
join(testDir, "../src/scr_url_params.c"),
|
||||
join(testDir, "../src/scr_path.c"),
|
||||
join(testDir, "../src/scr_bytes.c"),
|
||||
join(testDir, "../src/scr_shared.c"),
|
||||
...(process.platform === "linux" ? ["-D_GNU_SOURCE", "-lm"] : []),
|
||||
]);
|
||||
}, 120_000);
|
||||
|
||||
// Runs against the committed case file, generated from Node v24 itself
|
||||
// (gen-url-legacy-cases.mjs): curated domains (IDNA mapping, Punycode,
|
||||
// ContextJ and bidi rules, IPv4 and IPv6 hosts), legacy URLs (hosts,
|
||||
// auth, ports, backslashes, whitespace, autoEscape, IPv6, IDNA hostnames,
|
||||
// parsed queries), url.resolve over a base/relative grid, url.format over
|
||||
// objects, and a seeded fuzz corpus of each.
|
||||
test("node:url legacy API and domain conversion match Node on committed oracle cases", async () => {
|
||||
const { stderr } = await execFileAsync(bin, [join(testDir, "url-legacy-cases.txt")], {
|
||||
maxBuffer: 16 * 1024 * 1024,
|
||||
});
|
||||
expect(stderr.trim()).toMatch(/^(\d+)\/\1 cases passed$/);
|
||||
}, 120_000);
|
||||
@@ -0,0 +1,201 @@
|
||||
#!/usr/bin/env node
|
||||
// UTS #46 tables for url.domainToASCII, url.domainToUnicode and url.parse's
|
||||
// hostname conversion, matching the pinned Node release. The mapping table
|
||||
// is IDNA 17.0.0. Node's label validity checks (leading combining marks,
|
||||
// virama, bidi classes) use Unicode 13.0.0 character data, and its ContextJ
|
||||
// rule uses the fixed joining-type sets below. Normal builds use the
|
||||
// checked-in header; pass a directory holding the input files to skip the
|
||||
// download.
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { join } from "node:path";
|
||||
|
||||
const root = fileURLToPath(new URL("..", import.meta.url));
|
||||
const inputs = [
|
||||
[
|
||||
"17.0.0/idna/IdnaMappingTable.txt",
|
||||
"87f05505dc026fdb2bff16132bdc68a8014675836882a9a2b1844540ad3be382",
|
||||
],
|
||||
[
|
||||
"13.0.0/ucd/UnicodeData.txt",
|
||||
"bdbffbbfc8ad4d3a6d01b5891510458f3d36f7170422af4ea2bed3211a73e8bb",
|
||||
],
|
||||
];
|
||||
const [mappingText, unicodeDataText] = await Promise.all(
|
||||
inputs.map(async ([path, hash]) => {
|
||||
const bytes = process.argv[2]
|
||||
? await readFile(join(process.argv[2], path.split("/").at(-1)))
|
||||
: await (async () => {
|
||||
const response = await fetch(`https://www.unicode.org/Public/${path}`);
|
||||
if (!response.ok) throw new Error(`${path}: ${response.status}`);
|
||||
return Buffer.from(await response.arrayBuffer());
|
||||
})();
|
||||
if (createHash("sha256").update(bytes).digest("hex") !== hash)
|
||||
throw new Error(`${path}: checksum mismatch`);
|
||||
return bytes.toString("utf8");
|
||||
}),
|
||||
);
|
||||
|
||||
// The ContextJ joining types Node's validity check recognizes (Joining_Type
|
||||
// R, L and D); every other code point joins as neither.
|
||||
const JOINING = {
|
||||
R: "622..625 627 629 62f..632 648 671..673 675..677 688..699 6c0 6c3..6cb 6cd 6cf 6d2..6d3 6d5 6ee..6ef 710 715..719 71e 728 72a 72c 72f 74d 759..75b 854 8aa..8ac",
|
||||
L: "a872",
|
||||
D: "620 626 628 62a..62e 633..63f 641..647 649..64a 66e..66f 678..687 69a..6bf 6c1..6c2 6cc 6ce 6d0..6d1 6fa..6fc 6ff 712..714 71a..71d 71f..727 729 72b 72d..72e 74e..758 75c..766 850..853 855 8a0 8a2..8a9 1807 1820..1877 1887..18a8 18aa a840..a871",
|
||||
};
|
||||
|
||||
const hex = (n) => `0x${n.toString(16)}`;
|
||||
const fields = (line) =>
|
||||
line
|
||||
.split("#")[0]
|
||||
.trim()
|
||||
.split(";")
|
||||
.map((x) => x.trim());
|
||||
|
||||
// Status codes: 0 ignored, 1 valid (deviation included: nontransitional
|
||||
// processing keeps those code points), 2 disallowed, 3 mapped. A mapped
|
||||
// descriptor packs the code point count (bits 24-31) and the mappings index
|
||||
// (bits 8-23) above the status byte.
|
||||
const mappings = [];
|
||||
const mappingIndex = new Map();
|
||||
const ranges = [];
|
||||
for (const line of mappingText.split("\n")) {
|
||||
const f = fields(line);
|
||||
if (f.length < 2) continue;
|
||||
const [start] = f[0].split("..").map((x) => parseInt(x, 16));
|
||||
let descriptor;
|
||||
if (f[1] === "valid" || f[1] === "deviation") descriptor = 1;
|
||||
else if (f[1] === "ignored") descriptor = 0;
|
||||
else if (f[1] === "disallowed") descriptor = 2;
|
||||
else if (f[1] === "mapped") {
|
||||
const target = f[2].split(/\s+/).map((x) => parseInt(x, 16));
|
||||
const key = target.join(",");
|
||||
let index = mappingIndex.get(key);
|
||||
if (index === undefined) {
|
||||
index = mappings.length;
|
||||
mappingIndex.set(key, index);
|
||||
mappings.push(...target);
|
||||
}
|
||||
if (target.length > 0xff || index > 0xffff) throw new Error(`mapping overflow at ${f[0]}`);
|
||||
descriptor = ((target.length << 24) | (index << 8) | 3) >>> 0;
|
||||
} else throw new Error(`unknown status ${f[1]}`);
|
||||
if (ranges.length > 0 && ranges.at(-1)[1] === descriptor) continue;
|
||||
ranges.push([start, descriptor]);
|
||||
}
|
||||
|
||||
// Bidi classes, General_Category=Mark, and Canonical_Combining_Class=9
|
||||
// from UnicodeData.txt (assigned code points only).
|
||||
const BIDI = [
|
||||
"NONE",
|
||||
"L",
|
||||
"R",
|
||||
"AL",
|
||||
"EN",
|
||||
"ES",
|
||||
"ET",
|
||||
"AN",
|
||||
"CS",
|
||||
"NSM",
|
||||
"BN",
|
||||
"B",
|
||||
"S",
|
||||
"WS",
|
||||
"ON",
|
||||
"LRE",
|
||||
"LRO",
|
||||
"RLE",
|
||||
"RLO",
|
||||
"PDF",
|
||||
"LRI",
|
||||
"RLI",
|
||||
"FSI",
|
||||
"PDI",
|
||||
];
|
||||
const bidi = new Uint8Array(0x110000);
|
||||
const marks = new Uint8Array(0x110000);
|
||||
const virama = [];
|
||||
const rows = unicodeDataText.split("\n").filter(Boolean);
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const f = rows[i].split(";");
|
||||
const start = parseInt(f[0], 16);
|
||||
const end = f[1].endsWith(", First>") ? parseInt(rows[++i].split(";")[0], 16) : start;
|
||||
const cls = BIDI.indexOf(f[4]);
|
||||
if (cls <= 0) throw new Error(`unknown bidi class ${f[4]}`);
|
||||
for (let cp = start; cp <= end; cp++) {
|
||||
bidi[cp] = cls;
|
||||
if (f[2][0] === "M") marks[cp] = 1;
|
||||
if (f[3] === "9") virama.push(cp);
|
||||
}
|
||||
}
|
||||
|
||||
const runs = (data) => {
|
||||
const out = [];
|
||||
for (let start = 0; start < data.length;) {
|
||||
let end = start + 1;
|
||||
while (end < data.length && data[end] === data[start]) end++;
|
||||
if (data[start]) out.push([start, end - 1, data[start]]);
|
||||
start = end;
|
||||
}
|
||||
return out;
|
||||
};
|
||||
const joining = new Uint8Array(0x110000);
|
||||
for (const [type, value] of [
|
||||
["R", 1],
|
||||
["L", 2],
|
||||
["D", 3],
|
||||
]) {
|
||||
for (const item of JOINING[type].split(" ")) {
|
||||
const [start, end = start] = item.split("..").map((x) => parseInt(x, 16));
|
||||
for (let cp = start; cp <= end; cp++) joining[cp] = value;
|
||||
}
|
||||
}
|
||||
|
||||
const wrap = (items, perLine) => {
|
||||
const lines = [];
|
||||
for (let i = 0; i < items.length; i += perLine)
|
||||
lines.push(` ${items.slice(i, i + perLine).join(", ")},`);
|
||||
return lines;
|
||||
};
|
||||
const header = [
|
||||
"/* Generated by scripts/gen-idna-tables.mjs; IDNA 17.0.0 mapping table and",
|
||||
" * Unicode 13.0.0 validity data. Unicode data is licensed under",
|
||||
" * vendor/unicode/LICENSE. Do not edit. */",
|
||||
"#ifndef SCR_IDNA_DATA_H",
|
||||
"#define SCR_IDNA_DATA_H",
|
||||
"#include <stdint.h>",
|
||||
"enum { " + BIDI.map((name, index) => `SCR_BIDI_${name} = ${index}`).join(", ") + " };",
|
||||
"enum { SCR_JOIN_R = 1, SCR_JOIN_L = 2, SCR_JOIN_D = 3 };",
|
||||
"typedef struct { uint32_t start, end; uint8_t value; } ScrIdnaRange;",
|
||||
"/* { first code point, descriptor } — the run extends to the next entry. */",
|
||||
`static const uint32_t scr_idna_status[${ranges.length}][2] = {`,
|
||||
...wrap(
|
||||
ranges.map(([start, descriptor]) => `{${hex(start)}, ${hex(descriptor)}}`),
|
||||
4,
|
||||
),
|
||||
"};",
|
||||
`static const uint32_t scr_idna_mappings[${mappings.length}] = {`,
|
||||
...wrap(mappings.map(hex), 10),
|
||||
"};",
|
||||
...[
|
||||
["scr_idna_bidi", runs(bidi)],
|
||||
["scr_idna_marks", runs(marks)],
|
||||
["scr_idna_joining", runs(joining)],
|
||||
].flatMap(([name, list]) => [
|
||||
`static const ScrIdnaRange ${name}[${list.length}] = {`,
|
||||
...wrap(
|
||||
list.map(([start, end, value]) => `{${hex(start)}, ${hex(end)}, ${value}}`),
|
||||
4,
|
||||
),
|
||||
"};",
|
||||
]),
|
||||
`static const uint32_t scr_idna_virama[${virama.length}] = {`,
|
||||
...wrap(virama.map(hex), 10),
|
||||
"};",
|
||||
"#endif",
|
||||
"",
|
||||
].join("\n");
|
||||
await writeFile(join(root, "packages/runtime/src/scr_idna_data.h"), header);
|
||||
console.log(
|
||||
`wrote ${ranges.length} status ranges, ${mappings.length} mapped code points, ${runs(bidi).length} bidi ranges`,
|
||||
);
|
||||
@@ -0,0 +1,29 @@
|
||||
// @exit: 1
|
||||
// url.parse()'s DEP0169 deprecation and the DEP0170 invalid-port warning:
|
||||
// Node emits each once per process on the next tick (after the current
|
||||
// synchronous turn, before promise reactions), url.format() of a string and
|
||||
// url.resolve() raise DEP0169 through the same parse, and 'warning'
|
||||
// listeners receive the name, code and message. The default stderr report
|
||||
// carries the pid, which can never match, so the @exit directive keeps
|
||||
// stderr out of the comparison; the listener pins the text.
|
||||
import * as url from "node:url";
|
||||
|
||||
process.on("warning", (w) => {
|
||||
console.log(`warning ${w.name} ${(w as { code?: string }).code} ${w.message}`);
|
||||
});
|
||||
|
||||
Promise.resolve().then(() => console.log("promise reaction"));
|
||||
console.log(url.parse("http://example.com/a").host);
|
||||
console.log(url.parse("http://example.com:8080/b").port);
|
||||
console.log(url.format("http://example.com/c"));
|
||||
console.log(url.resolve("http://example.com/d/e", "../f"));
|
||||
console.log(url.parse("http://a:b:c/").pathname);
|
||||
console.log(url.parse("http://x:y:z/").pathname);
|
||||
console.log(url.domainToASCII("example.org"));
|
||||
console.log(url.format({ protocol: "https", host: "example.net" }));
|
||||
console.log("sync end");
|
||||
setTimeout(() => {
|
||||
console.log(url.parse("http://later.example/").href);
|
||||
console.log(url.parse("http://p:q:r/").href);
|
||||
process.exitCode = 1;
|
||||
}, 0);
|
||||
@@ -0,0 +1,54 @@
|
||||
// @exit: 1
|
||||
// url.parse() answers Node's Url instance: twelve own properties in Node's
|
||||
// order with null for every part the input lacks, util.inspect's "Url"
|
||||
// class name (nested and beyond the depth limit too), JSON and key
|
||||
// enumeration (for...in also visits Url.prototype's five methods), and
|
||||
// the parsed fields' typed reads. The @exit directive keeps the DEP0169
|
||||
// report (its pid never matches) out of the stderr comparison.
|
||||
import { inspect } from "node:util";
|
||||
import { parse, type Url } from "node:url";
|
||||
|
||||
const inputs = [
|
||||
"http://user:pw@Example.com:8080/p/a/t/h?query=string#hash",
|
||||
"https://example.com",
|
||||
"//example.com/no/protocol",
|
||||
"/just/a/path?x=1",
|
||||
"relative/path#frag",
|
||||
"",
|
||||
" ",
|
||||
"mailto:someone@example.org?subject=hi",
|
||||
"javascript:alert(1)",
|
||||
"file:///etc/hosts",
|
||||
"ftp://files.example.com/pub/",
|
||||
"?only=query",
|
||||
"#only-hash",
|
||||
];
|
||||
for (const input of inputs) console.log(JSON.stringify(input), parse(input));
|
||||
|
||||
const full = parse("http://a.b/c?d#e");
|
||||
console.log(JSON.stringify(full));
|
||||
console.log(Object.keys(full).join(","));
|
||||
const enumerated: string[] = [];
|
||||
for (const key in full) enumerated.push(key);
|
||||
console.log(enumerated.join(","));
|
||||
console.log(inspect({ nested: { deeper: { url: full } } }));
|
||||
console.log(inspect({ nested: { url: full } }, { depth: 1 }));
|
||||
console.log([full, parse("x")]);
|
||||
|
||||
function summary(u: Url): string {
|
||||
const { protocol, slashes, auth, host, port, hostname, hash, search, pathname, path, href } = u;
|
||||
const text = (part: string | null): string => (part === null ? "null" : JSON.stringify(part));
|
||||
const query = typeof u.query === "string" ? u.query : u.query === null ? "null" : "object";
|
||||
return [protocol, auth, host, port, hostname, hash, search, query, pathname, path, href]
|
||||
.map(text)
|
||||
.join(" | ") + ` | slashes=${slashes}`;
|
||||
}
|
||||
for (const input of inputs) console.log(summary(parse(input)));
|
||||
|
||||
const parsed = parse("HTTP://Mixed.Case.Example/Path");
|
||||
if (parsed.hostname !== null) console.log(parsed.hostname.length, parsed.hostname.toUpperCase());
|
||||
console.log(parsed.slashes === true, parsed.port ?? "default port", parsed.href.length);
|
||||
parsed.pathname = "/edited";
|
||||
parsed.hash = "#added";
|
||||
console.log(parsed.pathname, parsed.hash, parsed.href);
|
||||
process.exitCode = 1;
|
||||
@@ -0,0 +1,80 @@
|
||||
// @exit: 1
|
||||
// url.parse()'s authority handling: auth (decoded, the last @ before the
|
||||
// host ends wins), host/port splitting, lowercasing, the RFC 2396 non-host
|
||||
// characters that end a hostname early, a ':' moving the rest into the
|
||||
// path, the 255-unit hostname limit, bracketed IPv6, IDNA hostnames
|
||||
// (Punycode), backslashes before the query, whitespace trimming (U+00A0
|
||||
// and U+FEFF too), autoEscape of the rest, the protocol rules (case,
|
||||
// slashed and hostless protocols, slashesDenoteHost), and Node's
|
||||
// ERR_INVALID_URL and URIError throws. DEP0170 and DEP0169 print with a
|
||||
// pid, so the @exit directive keeps stderr out of the comparison.
|
||||
import { parse } from "node:url";
|
||||
|
||||
const show = (input: string, slashesDenoteHost = false): void => {
|
||||
try {
|
||||
const u = parse(input, false, slashesDenoteHost);
|
||||
console.log(
|
||||
JSON.stringify(input),
|
||||
JSON.stringify([u.protocol, u.slashes, u.auth, u.host, u.port, u.hostname, u.pathname, u.href]),
|
||||
);
|
||||
} catch (e) {
|
||||
const err = e as Error & { code?: string };
|
||||
console.log(JSON.stringify(input), "throws", err.name, err.code, err.message);
|
||||
}
|
||||
};
|
||||
|
||||
[
|
||||
"http://user:pass@host.com:8080/p",
|
||||
"http://a@b@c/d",
|
||||
"http://a@b?@c",
|
||||
"http://atpass:foo%40bar@127.0.0.1:8080/path?search=foo#bar",
|
||||
"http://%E2%98%83:%F0%9F%92%A9@snow.man/",
|
||||
"http://:@host/",
|
||||
"http://user@/",
|
||||
"HTTP://WWW.EXAMPLE.COM:0080/A/B",
|
||||
"http://example.com:/",
|
||||
"http://example.com:65536/",
|
||||
"http://x.y.com+a/b/c",
|
||||
"http://x.y.cOm;a/b/c?d=e#f g<h>i",
|
||||
"http://a b.com/",
|
||||
"http://a\"b/",
|
||||
"http://a{b}/",
|
||||
"http://a:b:c/",
|
||||
"http://host:1:2/x",
|
||||
"http://" + "a".repeat(255) + "/",
|
||||
"http://" + "a".repeat(256) + "/",
|
||||
"http://[::1]/",
|
||||
"http://[2001:db8::7]:8080/x",
|
||||
"http://user:pw@[fe80::1%25eth0]/",
|
||||
"coap://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]",
|
||||
"http://[::1",
|
||||
"http://www.Äffchen.com/",
|
||||
"http://例え.テスト/パス",
|
||||
"http://xn--espaol-zwa.com/",
|
||||
"http://ff.example/",
|
||||
"http://\u2488example.com/",
|
||||
"http://a\u00adb.com/",
|
||||
"http:\\\\evil.com\\foo\\bar?q=\\x",
|
||||
"/back\\slash\\path?q=\\x",
|
||||
" \t http://trimmed.example/ \n",
|
||||
"\u00a0\ufeffhttp://nbsp.example/\ufeff",
|
||||
"http://tab\tbed\nhost\r/x",
|
||||
"http://autoescape.example/a b'c\"d<e>f`g{h|i}j^k",
|
||||
"javascript:alert('x y')",
|
||||
"JavaScript://host/path",
|
||||
"mailto:user@example.com",
|
||||
"Mailto:User@Example.com",
|
||||
"HTTP:no-slashes",
|
||||
"http:no-slashes",
|
||||
"foo:bar",
|
||||
"svn+ssh://foo/bar",
|
||||
"dot.test://foo/bar",
|
||||
"//protocol-relative.example/x",
|
||||
"file:///C:/dir/file",
|
||||
"file://localhost/etc",
|
||||
"http://xn--a.com/",
|
||||
"http://a%zz@x/",
|
||||
"http://\u00ad/",
|
||||
].forEach((input) => show(input));
|
||||
for (const input of ["//host.example/x", "//user@h/p", "/not/host", "///triple"]) show(input, true);
|
||||
process.exitCode = 1;
|
||||
@@ -0,0 +1,39 @@
|
||||
// @exit: 1
|
||||
// url.parse(urlString, parseQueryString): the query string form, the
|
||||
// parsed null-prototype object (querystring.parse's decoding, '+' as
|
||||
// space, repeated keys as arrays, array-index keys first), the empty
|
||||
// object when no query exists, the simple-path fast path, and typed reads
|
||||
// through UrlWithParsedQuery's ParsedUrlQuery. The @exit directive keeps
|
||||
// the DEP0169 report (its pid never matches) out of the stderr comparison.
|
||||
import { inspect } from "node:util";
|
||||
import { parse } from "node:url";
|
||||
|
||||
const inputs = [
|
||||
"http://example.com/search?q=node+js&lang=en&lang=fr&empty=&flag",
|
||||
"/fast/path?a=1&b=%20two%20&c=%E2%9C%93",
|
||||
"/fast/path",
|
||||
"http://example.com",
|
||||
"?2=two&1=one&b=bee&0=zero",
|
||||
"http://example.com/?__proto__=x&constructor=y&hasOwnProperty=z",
|
||||
"http://example.com/?a[]=1&a[]=2&a=3",
|
||||
"http://example.com/?%zz=bad&ok=%41",
|
||||
"http://example.com/p?x=1#frag?not=query",
|
||||
"mailto:me@example.org?subject=Hello%20World&body=a+b",
|
||||
];
|
||||
for (const input of inputs) {
|
||||
const raw = parse(input);
|
||||
const parsed = parse(input, true);
|
||||
console.log(JSON.stringify(input));
|
||||
console.log(" string:", JSON.stringify(raw.query), raw.search);
|
||||
console.log(" parsed:", inspect(parsed.query), parsed.search, parsed.href);
|
||||
console.log(" keys:", Object.keys(parsed.query).join(","), JSON.stringify(parsed.query));
|
||||
}
|
||||
|
||||
const typed = parse("http://example.com/?single=1&multi=a&multi=b", true);
|
||||
const single = typed.query.single;
|
||||
const multi = typed.query.multi;
|
||||
const missing = typed.query.missing;
|
||||
console.log(typeof single, single, Array.isArray(multi), multi, missing === undefined);
|
||||
if (Array.isArray(multi)) console.log(multi.length, multi.join("+"));
|
||||
console.log(parse("/p?x=1", false, true).query, parse("//h/p?x=1", true, true).query);
|
||||
process.exitCode = 1;
|
||||
@@ -0,0 +1,71 @@
|
||||
// url.format(urlObject) over plain objects: Url.prototype.format's rules —
|
||||
// the ':' and '//' protocol completion (slashed protocols, file:'s bare
|
||||
// '//', explicit slashes), host winning over hostname and port, IPv6
|
||||
// bracketing, auth's percent-encoding, query objects through
|
||||
// querystring.stringify when search is empty, the '?' and '#' prefixes,
|
||||
// '#' and '?' escapes in the pathname and '#' in the search, number and
|
||||
// boolean fields, and the ERR_INVALID_ARG_TYPE throw for other values.
|
||||
// No string is parsed, so no deprecation warning prints.
|
||||
import { format, type UrlObject } from "node:url";
|
||||
|
||||
const objects: UrlObject[] = [
|
||||
{},
|
||||
{ protocol: "http", host: "example.com" },
|
||||
{ protocol: "https:", hostname: "example.com", port: 8443, pathname: "x/y" },
|
||||
{ protocol: "http", hostname: "example.com", port: "80", pathname: "/a" },
|
||||
{ protocol: "http", host: "host.example:1", hostname: "ignored", port: 2 },
|
||||
{ protocol: "file", pathname: "/etc/hosts" },
|
||||
{ protocol: "file:" },
|
||||
{ protocol: "files" },
|
||||
{ protocol: "mailto", auth: "me", hostname: "example.org" },
|
||||
{ protocol: "mailto:", pathname: "me@example.org" },
|
||||
{ protocol: "custom", slashes: true, host: "h", pathname: "p" },
|
||||
{ protocol: "custom", host: "h", pathname: "p" },
|
||||
{ protocol: "HTTP", host: "h", pathname: "p" },
|
||||
{ hostname: "::1", port: 3000 },
|
||||
{ hostname: "[::1]", port: 3000 },
|
||||
{ auth: "user:p@ss w/rd", host: "secure.example" },
|
||||
{ auth: "naïve💩", host: "h" },
|
||||
{ auth: "dropped", pathname: "/without-host" },
|
||||
{ pathname: "a#b?c" },
|
||||
{ protocol: "http:", host: "h", pathname: "#starts-with-hash" },
|
||||
{ search: "a=1" },
|
||||
{ search: "?a=1#b" },
|
||||
{ search: "#x" },
|
||||
{ protocol: "http", host: "h", query: { a: "1", b: ["2", "3"], c: "x y", d: "" } },
|
||||
{ query: "ignored=string" },
|
||||
{ protocol: "http", host: "h", query: { a: "1" }, search: "?b=2" },
|
||||
{ protocol: "http", host: "h", query: {}, search: "" },
|
||||
{ hash: "frag" },
|
||||
{ hash: "#frag" },
|
||||
{ protocol: "http:", host: "a", pathname: "/", search: "q", hash: "h" },
|
||||
{ protocol: "http", hostname: "", port: 1, pathname: "/x" },
|
||||
{ slashes: true, pathname: "x" },
|
||||
{ slashes: true, hostname: "h" },
|
||||
{ protocol: "http", auth: "", host: "", hash: "", search: "", pathname: "" },
|
||||
{ protocol: null, host: null, pathname: null },
|
||||
{ href: "http://ignored.example/", pathname: "/kept" },
|
||||
];
|
||||
for (const o of objects) console.log(JSON.stringify(o), "->", JSON.stringify(format(o)));
|
||||
|
||||
const built: UrlObject = { protocol: "https", hostname: "api.example" };
|
||||
built.pathname = "/v1/items";
|
||||
built.query = { page: 2, tags: ["a", "b"], on: true };
|
||||
console.log(format(built));
|
||||
console.log(format({ protocol: "ftp", host: "files.example", pathname: "pub/file.txt" }));
|
||||
|
||||
for (const bad of [42, null, undefined, true]) {
|
||||
try {
|
||||
format(bad as unknown as UrlObject);
|
||||
} catch (e) {
|
||||
const err = e as Error & { code?: string };
|
||||
console.log(err.name, err.code, err.message);
|
||||
}
|
||||
}
|
||||
try {
|
||||
format({ protocol: "http", host: "h", auth: "lone\ud800" });
|
||||
} catch (e) {
|
||||
const err = e as Error & { code?: string };
|
||||
console.log(err.name, err.code, err.message);
|
||||
}
|
||||
console.log(format({ protocol: "http", host: "h", auth: "\ud800pair" }));
|
||||
@@ -0,0 +1,49 @@
|
||||
// url.format(URL, options): the WHATWG serialization with the fragment,
|
||||
// search and auth parts optionally dropped and the host optionally
|
||||
// converted to Unicode (Punycode labels decoded, other labels kept);
|
||||
// absent, null and undefined options keep their defaults, other values
|
||||
// are read with JS truthiness, and a non-object options argument throws
|
||||
// ERR_INVALID_ARG_TYPE. No legacy parse runs, so no warning prints.
|
||||
import { format, type URLFormatOptions } from "node:url";
|
||||
|
||||
const urls = [
|
||||
"https://user:secret@xn--espaol-zwa.com:8080/p/a/t/h?query=string#hash",
|
||||
"http://xn--fiq228c.xn--fiqs8s/?q#f",
|
||||
"http://plain.example/path",
|
||||
"file:///home/user/file.txt",
|
||||
"mailto:someone@example.org",
|
||||
"data:text/plain,hello",
|
||||
"http://[::1]:3000/x?y#z",
|
||||
"foo://user@host/path?q=1#frag",
|
||||
];
|
||||
const options: (URLFormatOptions | undefined)[] = [
|
||||
undefined,
|
||||
{},
|
||||
{ fragment: false },
|
||||
{ search: false },
|
||||
{ auth: false },
|
||||
{ unicode: true },
|
||||
{ fragment: false, search: false, auth: false, unicode: true },
|
||||
{ fragment: undefined, unicode: undefined },
|
||||
];
|
||||
for (const href of urls) {
|
||||
const u = new URL(href);
|
||||
console.log(href);
|
||||
for (const o of options) console.log(" ", JSON.stringify(o) ?? "undefined", format(u, o));
|
||||
}
|
||||
|
||||
const loose = { fragment: 0, search: "", auth: null, unicode: 1 } as unknown as URLFormatOptions;
|
||||
console.log(format(new URL("https://u:p@xn--espaol-zwa.com/?s#h"), loose));
|
||||
for (const bad of ["x", 42, true, [1]]) {
|
||||
try {
|
||||
console.log(format(new URL("http://a.example/#f"), bad as unknown as URLFormatOptions));
|
||||
} catch (e) {
|
||||
const err = e as Error & { code?: string };
|
||||
console.log(err.name, err.code, err.message);
|
||||
}
|
||||
}
|
||||
console.log(format(new URL("http://a.example/#f"), null as unknown as URLFormatOptions));
|
||||
const mutated = new URL("http://example.com/a?b#c");
|
||||
mutated.hostname = "xn--bcher-kva.example";
|
||||
mutated.username = "reader";
|
||||
console.log(format(mutated, { unicode: true }), format(mutated, { auth: false, unicode: true }));
|
||||
@@ -0,0 +1,46 @@
|
||||
// @exit: 1
|
||||
// url.resolve(from, to): Url.prototype.resolveObject's rules over the RFC
|
||||
// 1808/3986 grid (dot segments, trailing slashes, query-only and
|
||||
// fragment-only references, network-path references), protocol changes
|
||||
// (slashed protocols, file:, hostless and non-slashed schemes), the
|
||||
// mailto:-style host folding, auth carried and dropped, and empty bases.
|
||||
// The @exit directive keeps the DEP0169 report (its pid never matches)
|
||||
// out of the stderr comparison.
|
||||
import { resolve } from "node:url";
|
||||
|
||||
const bases = [
|
||||
"http://a/b/c/d;p?q",
|
||||
"http://user:pw@host.example:81/dir/file?x#y",
|
||||
"https://example.com",
|
||||
"http://example.com/one/two/",
|
||||
"mailto:local1@domain1",
|
||||
"mailto:a@b/c",
|
||||
"file:///a/b/c",
|
||||
"file://host/share/x",
|
||||
"javascript:void(0)",
|
||||
"/absolute/path/file",
|
||||
"relative/path/file",
|
||||
"",
|
||||
"//network.example/p",
|
||||
"foo://host/a/b",
|
||||
"foo:a/b",
|
||||
"http://[::1]:8080/a/b",
|
||||
];
|
||||
const references = [
|
||||
"", "g", "./g", "g/", "/g", "//g", "?y", "g?y", "#s", "g#s", "g?y#s", ";x", "g;x",
|
||||
".", "./", "..", "../", "../g", "../..", "../../", "../../g", "../../../g", "/./g",
|
||||
"/../g", "g.", ".g", "g..", "..g", "./../g", "./g/.", "g/./h", "g/../h", "http:g",
|
||||
"http://other/c", "https:other", "mailto:x@y", "local2@domain2", "file:///z", "foo:z",
|
||||
"//user:pw@other/c", "///triple", "\\back\\slash", " spaced ",
|
||||
];
|
||||
for (const base of bases) {
|
||||
console.log(JSON.stringify(base));
|
||||
console.log(" " + references.map((ref) => `${JSON.stringify(ref)}=${resolve(base, ref)}`).join(" "));
|
||||
}
|
||||
try {
|
||||
resolve("http://xn--a.example/", "x");
|
||||
} catch (e) {
|
||||
const err = e as Error & { code?: string };
|
||||
console.log(err.name, err.code, err.message);
|
||||
}
|
||||
process.exitCode = 1;
|
||||
@@ -0,0 +1,16 @@
|
||||
// The legacy url API called from a dependency under node_modules: Node's
|
||||
// isInsideNodeModules rule skips url.parse()'s DEP0169 warning for those
|
||||
// call sites (url.format() of a string and url.resolve() included), so
|
||||
// stderr stays empty and is compared. The program's own
|
||||
// url.format() of an object and url.domainToASCII() never warn either.
|
||||
import { domainToASCII, format } from "node:url";
|
||||
import { canonical, describe, joinLink, parseLink } from "legacy-links";
|
||||
|
||||
const u = parseLink("HTTP://User@Example.COM:8080/a/b?x=1#top");
|
||||
console.log(u);
|
||||
console.log(u.host, u.port, u.auth, u.hash);
|
||||
console.log(parseLink("/path?k=v&k=w", true).query);
|
||||
console.log(canonical("http://example.com/a b?c d#e f"));
|
||||
console.log(joinLink("http://example.com/docs/guide/", "../api/index.html"));
|
||||
console.log(describe("https://example.org/search?q=scriptc&page=2"));
|
||||
console.log(format({ protocol: "https", hostname: domainToASCII("bücher.example"), pathname: "/x" }));
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import type { Url } from "node:url";
|
||||
|
||||
export function parseLink(href: string, parseQuery?: boolean): Url;
|
||||
export function canonical(href: string): string;
|
||||
export function joinLink(base: string, reference: string): string;
|
||||
export function describe(href: string): string;
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { parse, format, resolve } from "node:url";
|
||||
|
||||
export function parseLink(href, parseQuery) {
|
||||
return parse(href, parseQuery);
|
||||
}
|
||||
|
||||
export function canonical(href) {
|
||||
return format(href);
|
||||
}
|
||||
|
||||
export function joinLink(base, reference) {
|
||||
return resolve(base, reference);
|
||||
}
|
||||
|
||||
export function describe(href) {
|
||||
const u = parse(href, true);
|
||||
return `${u.protocol} ${u.host} ${u.pathname} ${JSON.stringify(u.query)}`;
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "legacy-links",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts"
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// url.domainToASCII() and url.domainToUnicode(): the WHATWG host parser
|
||||
// over a special-scheme hostname — UTS #46 mapping (case folding, width and
|
||||
// compatibility forms, ignored soft hyphens, ideographic full stops),
|
||||
// Punycode labels validated and decoded, the ContextJ and bidi label rules,
|
||||
// percent-decoding, IPv4 numbers (hex, octal, shorthand) and IPv6
|
||||
// serialization, a delimiter ending the host, and "" for every failure.
|
||||
// Neither function warns, so stderr is compared.
|
||||
import { domainToASCII, domainToUnicode } from "node:url";
|
||||
|
||||
const domains = [
|
||||
"", "example.com", "EXAMPLE.COM", "español.com", "xn--espaol-zwa.com", "XN--ESPAOL-ZWA.COM",
|
||||
"中文.中国", "日本語。JP", "ff.com", "faß.de", "Ⅻ.com", "$.com", "⑴.com", "⒈.com",
|
||||
"a\u00adb.com", "\u00ad", "İstanbul.com", "Dž.com", "\u{1d400}.com", "💩.la", "xn--ls8h.la",
|
||||
"xn--a.com", "xn--.com", "xn--xn--a.com", "xn--zz.com", "ab--c.com", "-x.com", "x-.com",
|
||||
"a\u200cb.com", "क\u094d\u200c.com", "a\u200db.com", "\u0628\u200c\u0628.com",
|
||||
"\u05d0\u05d1.com", "a\u05d0.com", "\u05d01.com", "1\u05d0.com", "\u0628\u0661.com",
|
||||
"\u0628\u06611.com", "\u0301a.com", "a\u0301.com", "a.b.c.", ".", "..", "a..b", ".a",
|
||||
"1.2.3.4", "1.2.3", "0x7f.1", "0300.0250.1.1", "4294967295", "4294967296", "1.2.3.256",
|
||||
"1.2.3.4.5", "1.2.3.4.", "08.1.1.1", "0x.1.1.1", "a.1", "1.a", "a.0x", "[::1]", "[::]",
|
||||
"[1:2:3:4:5:6:7:8]", "[1:0:0:0:0:0:0:1]", "[1::2:0:0:3]", "[0:0:1:0:0:0:0:0]",
|
||||
"[::ffff:1.2.3.4]", "[::1.2.3]", "[1:2:3:4:5:6:7:8:9]", "[ABCD::EF]", "[::1", "a%2Eb",
|
||||
"%E4%B8%AD.com", "%zz.com", "a%", "a b.com", "a:80", "a/b", "a?b", "a#b", "a\\b",
|
||||
"a\tb\nc\rd", "a@b", "a^b", "a|b", "a<b", "a_b.com", "a\ufffdb", "a\ud800b", "localhost",
|
||||
"x".repeat(70) + ".com", "é".repeat(70) + ".com",
|
||||
];
|
||||
for (const d of domains) {
|
||||
console.log(JSON.stringify(d), JSON.stringify(domainToASCII(d)), JSON.stringify(domainToUnicode(d)));
|
||||
}
|
||||
for (const d of ["xn--espaol-zwa.com", "español.com"]) {
|
||||
console.log(domainToUnicode(domainToASCII(d)) === domainToUnicode(d));
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// @exit: 1
|
||||
// The legacy url API from CommonJS, where arguments are unchecked: Node's
|
||||
// validateString TypeErrors for url.parse()/url.resolve(), url.format()'s
|
||||
// urlObject type ladder, ERR_MISSING_ARGS and String() conversion for the
|
||||
// domain pair, JS truthiness for parseQueryString/slashesDenoteHost, a Url
|
||||
// instance (or an object inheriting from one) passed back to url.parse(),
|
||||
// the inherited "Url" inspection, and Url.prototype's enumerable methods
|
||||
// (format() included). The @exit directive keeps the DEP0169 report (its
|
||||
// pid never matches) out of the stderr comparison.
|
||||
'use strict';
|
||||
const url = require('url');
|
||||
const util = require('util');
|
||||
|
||||
// Unannotated, so any value reaches the call unchecked.
|
||||
const loose = (value) => value;
|
||||
const attempt = (label, f) => {
|
||||
try {
|
||||
console.log(label, JSON.stringify(f()));
|
||||
} catch (e) {
|
||||
console.log(label, e.name, e.code, e.message);
|
||||
}
|
||||
};
|
||||
|
||||
attempt('parse(42)', () => url.parse(42));
|
||||
attempt('parse()', () => url.parse());
|
||||
attempt('parse(null)', () => url.parse(null));
|
||||
attempt('parse({})', () => url.parse({ href: 'http://x/' }));
|
||||
attempt('resolve(1, "a")', () => url.resolve(1, 'a'));
|
||||
attempt('resolve("http://a/", 2)', () => url.resolve('http://a/', 2));
|
||||
attempt('format(42)', () => url.format(42));
|
||||
attempt('format(null)', () => url.format(null));
|
||||
attempt('format()', () => url.format());
|
||||
attempt('format(fn)', () => url.format(() => 'x'));
|
||||
attempt('format([])', () => url.format([]));
|
||||
attempt('format(Map)', () => url.format(new Map([['host', 'm']])));
|
||||
attempt('format(URL, "x")', () => url.format(new URL('http://a/b?c#d'), 'x'));
|
||||
attempt('format(URL, [])', () => url.format(new URL('http://a/b?c#d'), loose([])));
|
||||
attempt('format(URL, 0)', () => url.format(new URL('http://a/b?c#d'), loose(0)));
|
||||
attempt('format({ port: 8080 })', () => url.format({ hostname: 'h', port: 8080 }));
|
||||
attempt('format({ protocol: 7 })', () => url.format({ protocol: 7, host: 8, pathname: 9 }));
|
||||
attempt('domainToASCII()', () => url.domainToASCII());
|
||||
attempt('domainToUnicode()', () => url.domainToUnicode());
|
||||
attempt('domainToASCII(42)', () => url.domainToASCII(42));
|
||||
attempt('domainToASCII(obj)', () => url.domainToASCII({ toString() { return 'ÄB.example'; } }));
|
||||
attempt('domainToUnicode(null)', () => url.domainToUnicode(null));
|
||||
attempt('domainToASCII(undefined)', () => url.domainToASCII(undefined));
|
||||
attempt('parse(?a=1, 1)', () => url.parse('?a=1', 1).query);
|
||||
attempt('parse(?a=1, "")', () => url.parse('?a=1', '').query);
|
||||
attempt('parse(//h/p, 0, "yes")', () => url.parse('//h/p', 0, 'yes').host);
|
||||
attempt('parse(//h/p, 0, 0)', () => url.parse('//h/p', 0, 0).host);
|
||||
|
||||
const u = url.parse('http://user@example.com/a?b=c#d');
|
||||
console.log(url.parse(u) === u);
|
||||
const child = Object.create(u);
|
||||
console.log(url.parse(child) === child, util.inspect(child), child.host);
|
||||
console.log(util.inspect(Object.getPrototypeOf(u)));
|
||||
const keys = [];
|
||||
for (const key in u) keys.push(key);
|
||||
console.log(keys.join(','));
|
||||
console.log(Object.keys(u).length, 'format' in u, 'resolveObject' in u, typeof u.parseHost);
|
||||
console.log(u.format(), u.format.name, u.format.length, u.parse.length, u.resolve.length);
|
||||
u.pathname = '/changed';
|
||||
u.search = '?x=y';
|
||||
console.log(u.format(), url.format(u));
|
||||
console.log(JSON.stringify({ ...u }));
|
||||
process.exitCode = 1;
|
||||
@@ -0,0 +1,37 @@
|
||||
// url.parse() under process.noDeprecation: while it is truthy, the DEP0169
|
||||
// and DEP0170 warnings are dropped (no report, no 'warning' listener call),
|
||||
// and each still uses up its one warning, so clearing the flag later
|
||||
// reports neither. Other warnings are unaffected. A second child sets the
|
||||
// flag after the first url.parse() call, in the same turn: the queued
|
||||
// warning still reaches the 'warning' listener, and the stderr report,
|
||||
// which reads the flag when the tick runs, is skipped. The program re-runs
|
||||
// itself and prints each child's output with process ids normalized.
|
||||
"use strict";
|
||||
const { spawnSync } = require("child_process");
|
||||
const url = require("url");
|
||||
|
||||
const mode = process.argv[2];
|
||||
if (mode === "silenced") {
|
||||
process.on("warning", (w) => console.log("listener:", w.name, w.code, w.message));
|
||||
process.noDeprecation = true;
|
||||
console.log(url.parse("http://example.com:8080/a").port);
|
||||
console.log(url.parse("http://example.com:x/b").pathname);
|
||||
process.emitWarning("ordinary warning while silenced");
|
||||
setTimeout(() => {
|
||||
process.noDeprecation = false;
|
||||
console.log(url.parse("http://example.com:y/c").hostname, process.noDeprecation);
|
||||
process.emitWarning("direct deprecation after restore", "DeprecationWarning", "DEP_URL_1");
|
||||
}, 0);
|
||||
} else if (mode === "late") {
|
||||
process.on("warning", (w) => console.log("listener:", w.name, w.code));
|
||||
console.log(url.parse("http://example.com/late").path);
|
||||
process.noDeprecation = 1;
|
||||
console.log("end of turn", process.noDeprecation);
|
||||
} else {
|
||||
for (const child of ["silenced", "late"]) {
|
||||
const run = spawnSync(process.execPath, [process.argv[1], child], { encoding: "utf8" });
|
||||
console.log(`${child} status: ${run.status}`);
|
||||
console.log(run.stdout);
|
||||
console.log(run.stderr.replace(/\(node:\d+\)/g, "(node:<pid>)"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// url.parse() under process.throwDeprecation: DEP0169 and DEP0170 are
|
||||
// thrown from their own ticks, after the turn's output and in order,
|
||||
// instead of being reported, so they reach the 'uncaughtException'
|
||||
// handler as DeprecationWarning errors with their codes, and no 'warning'
|
||||
// listener sees them. Without a handler the first one ends the process
|
||||
// with exit status 1. The program re-runs itself and prints each child's
|
||||
// stdout and status.
|
||||
"use strict";
|
||||
const { spawnSync } = require("child_process");
|
||||
const url = require("url");
|
||||
|
||||
const mode = process.argv[2];
|
||||
if (mode === "handled") {
|
||||
process.on("warning", (warning) => console.log("listener:", warning.name));
|
||||
process.on("uncaughtException", (error) => {
|
||||
console.log("uncaught:", error.name, error.code, error instanceof Error);
|
||||
console.log(error.message);
|
||||
});
|
||||
process.throwDeprecation = true;
|
||||
console.log(url.parse("http://example.com:x/a").pathname);
|
||||
console.log(process.throwDeprecation, "end of turn");
|
||||
} else if (mode === "unhandled") {
|
||||
process.throwDeprecation = true;
|
||||
console.log(url.parse("http://example.com/b").href);
|
||||
setTimeout(() => console.log("not reached"), 0);
|
||||
} else {
|
||||
for (const child of ["handled", "unhandled"]) {
|
||||
const run = spawnSync(process.execPath, [process.argv[1], child], { encoding: "utf8" });
|
||||
console.log(`${child} status: ${run.status}`);
|
||||
console.log(run.stdout);
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import * as url from "node:url";
|
||||
import { format, parse, resolve, type UrlObject, type UrlWithParsedQuery } from "node:url";
|
||||
|
||||
const u = parse("http://user:pw@Example.com:8080/p/a/t/h?query=string&a=1&a=2#hash", true);
|
||||
console.log(u);
|
||||
console.log(JSON.stringify(u), Object.keys(u).length);
|
||||
const q: UrlWithParsedQuery["query"] = u.query;
|
||||
console.log(q.a, q.query, u.host, u.port, u.slashes);
|
||||
u.pathname = "/changed";
|
||||
u.search = null;
|
||||
console.log(url.format(u));
|
||||
const plain = parse("/a/b?c=d");
|
||||
console.log(plain.pathname, plain.query, plain.href);
|
||||
const target: UrlObject = { protocol: "https", hostname: "example.com", pathname: "/x" };
|
||||
target.query = { a: "1", b: ["2", "3"] };
|
||||
console.log(format(target));
|
||||
console.log(format("http://a.com/b c"));
|
||||
console.log(format(new URL("http://user:pw@xn--espaol-zwa.com/a?b#c"), { unicode: true, auth: false }));
|
||||
console.log(resolve("/one/two/three", "four"), resolve("http://example.com/one", "/two"));
|
||||
console.log(url.domainToASCII("español.com"), url.domainToUnicode("xn--espaol-zwa.com"));
|
||||
try {
|
||||
parse("http://xn--a.com/");
|
||||
} catch (e) {
|
||||
console.log((e as Error).name, (e as NodeJS.ErrnoException).code, (e as Error).message);
|
||||
}
|
||||
@@ -389,6 +389,23 @@ test("node-types: URL factories match Node under @types/node", async () => {
|
||||
expect(comparableStderr(native.stderr)).toBe(node.stderr);
|
||||
});
|
||||
|
||||
test("node-types: the legacy url API matches Node under @types/node", async () => {
|
||||
const outDir = outDirFor("node-url-legacy");
|
||||
const entry = join(nodeTypesDir, "url-legacy.ts");
|
||||
const result = await compile(entry, { outPath: join(outDir, "url-legacy"), outDir, sanitize });
|
||||
expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true);
|
||||
if (!result.ok) return;
|
||||
const [native, node] = await Promise.all([
|
||||
execFileAsync(result.binaryPath),
|
||||
execFileAsync(process.execPath, ["--disable-warning=MODULE_TYPELESS_PACKAGE_JSON", entry]),
|
||||
]);
|
||||
expect(native.stdout).toBe(node.stdout);
|
||||
// The DEP0169 report names each process's own pid.
|
||||
const withoutPid = (text: string): string => text.replace(/^\(node:\d+\) /gm, "(node:pid) ");
|
||||
expect(withoutPid(comparableStderr(native.stderr))).toBe(withoutPid(node.stderr));
|
||||
expect(node.stderr).toContain("[DEP0169] DeprecationWarning");
|
||||
});
|
||||
|
||||
test("node-types: no-error assertions match Node under @types/node", async () => {
|
||||
const outDir = outDirFor("node-no-error-assertions");
|
||||
const entry = join(nodeTypesDir, "no-error-assertions.ts");
|
||||
|
||||
Reference in New Issue
Block a user