Skip to content

Commit 924b437

Browse files
committed
feat(ext/url): implement more URL functions + fix wpt
1 parent e555f30 commit 924b437

File tree

9 files changed

+427
-147
lines changed

9 files changed

+427
-147
lines changed

Cargo.lock

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ authors = ["the Andromeda team"]
77
edition = "2024"
88
license = "Mozilla Public License 2.0"
99
repository = "https://github.com/tryandromeda/andromeda"
10-
version = "0.1.0-draft47"
10+
version = "0.1.0-draft48"
1111

1212
[workspace.dependencies]
1313
andromeda-core = { path = "core" }
@@ -31,7 +31,7 @@ dprint-plugin-json = "0.20.0"
3131
env_logger = "0.11.8"
3232
futures = "0.3.31"
3333
glob = "0.3.3"
34-
hotpath = { version = "0.3.0" }
34+
hotpath = { version = "0.3.1" }
3535
indexmap = "2.11.4"
3636
image = "0.25.8"
3737
lazy_static = "1.5.0"

runtime/src/ext/url/mod.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -225,10 +225,13 @@ impl URLExt {
225225
let url = args.get(0).to_string(agent, gc.reborrow()).unbind()?;
226226
match Url::parse(url.as_str(agent).expect("String is not valid UTF-8")) {
227227
Ok(url) => {
228-
Ok(
229-
Value::from_string(agent, url.host_str().unwrap_or("").to_string(), gc.nogc())
230-
.unbind(),
231-
)
228+
let host_str = url.host_str().unwrap_or("").to_string();
229+
let result = if let Some(port) = url.port() {
230+
format!("{}:{}", host_str, port)
231+
} else {
232+
host_str
233+
};
234+
Ok(Value::from_string(agent, result, gc.nogc()).unbind())
232235
}
233236
Err(e) => Ok(Value::from_string(agent, format!("Error: {e}"), gc.nogc()).unbind()),
234237
}

runtime/src/ext/url/mod.ts

Lines changed: 84 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -65,15 +65,24 @@ class URL {
6565
constructor(url: string, base?: string) {
6666
this.url = url;
6767
this.base = base;
68-
this.serialized = base ?
68+
const parsed = base ?
6969
__andromeda__.internal_url_parse(url, base) :
7070
__andromeda__.internal_url_parse_no_base(url);
71+
72+
if (parsed.startsWith("Error:")) {
73+
throw new TypeError("Invalid URL");
74+
}
75+
this.serialized = parsed;
7176
}
7277

7378
toString() {
7479
return this.serialized;
7580
}
7681

82+
toJSON() {
83+
return this.serialized;
84+
}
85+
7786
get searchParams(): URLSearchParams {
7887
return new URLSearchParams(this);
7988
}
@@ -82,8 +91,29 @@ class URL {
8291
return this.serialized;
8392
}
8493

85-
static parse(url: string, base?: string) {
86-
return new this(url, base);
94+
set href(value: string) {
95+
const parsed = __andromeda__.internal_url_parse_no_base(value);
96+
if (parsed.startsWith("Error:")) {
97+
throw new TypeError("Invalid URL");
98+
}
99+
this.serialized = parsed;
100+
}
101+
102+
static parse(url: string, base?: string): URL | null {
103+
try {
104+
return new this(url, base);
105+
} catch {
106+
return null;
107+
}
108+
}
109+
110+
static canParse(url: string, base?: string): boolean {
111+
try {
112+
new URL(url, base);
113+
return true;
114+
} catch {
115+
return false;
116+
}
87117
}
88118

89119
get protocol(): string {
@@ -117,7 +147,24 @@ class URL {
117147
}
118148

119149
get host(): string {
120-
return __andromeda__.internal_url_get_host(this.serialized);
150+
const hostname = __andromeda__.internal_url_get_hostname(this.serialized);
151+
const port = __andromeda__.internal_url_get_port(this.serialized);
152+
if (port) {
153+
return `${hostname}:${port}`;
154+
}
155+
return hostname;
156+
}
157+
158+
set host(v: string) {
159+
const colonIndex = v.lastIndexOf(":");
160+
if (colonIndex !== -1) {
161+
const hostname = v.substring(0, colonIndex);
162+
const port = v.substring(colonIndex + 1);
163+
this.serialized = __andromeda__.internal_url_set_hostname(this.serialized, hostname);
164+
this.serialized = __andromeda__.internal_url_set_port(this.serialized, port);
165+
} else {
166+
this.serialized = __andromeda__.internal_url_set_hostname(this.serialized, v);
167+
}
121168
}
122169

123170
get hostname(): string {
@@ -151,15 +198,17 @@ class URL {
151198
}
152199

153200
get search(): string {
154-
return __andromeda__.internal_url_get_search(this.serialized);
201+
const query = __andromeda__.internal_url_get_search(this.serialized);
202+
return query ? `?${query}` : "";
155203
}
156204

157205
set search(v: string) {
158206
this.serialized = __andromeda__.internal_url_set_search(this.serialized, v);
159207
}
160208

161209
get hash(): string {
162-
return __andromeda__.internal_url_get_hash(this.serialized);
210+
const fragment = __andromeda__.internal_url_get_hash(this.serialized);
211+
return fragment ? `#${fragment}` : "";
163212
}
164213

165214
set hash(v: string) {
@@ -171,6 +220,10 @@ class URLSearchParams {
171220
#pairs: Array<[string, string]> = [];
172221
#url?: URL;
173222

223+
get size(): number {
224+
return this.#pairs.length;
225+
}
226+
174227
constructor(
175228
init?: string | Array<[string, string]> | Record<string, string> | URL,
176229
) {
@@ -246,9 +299,13 @@ class URLSearchParams {
246299
this.#updateURL();
247300
}
248301

249-
delete(name: string) {
302+
delete(name: string, value?: string) {
250303
const before = this.#pairs.length;
251-
this.#pairs = this.#pairs.filter(([k, _]) => k !== name);
304+
if (value !== undefined) {
305+
this.#pairs = this.#pairs.filter(([k, v]) => !(k === name && v === value));
306+
} else {
307+
this.#pairs = this.#pairs.filter(([k, _]) => k !== name);
308+
}
252309
if (this.#pairs.length !== before) this.#updateURL();
253310
}
254311

@@ -263,9 +320,16 @@ class URLSearchParams {
263320
return res;
264321
}
265322

266-
has(name: string) {
267-
for (const [k, _] of this.#pairs) if (k === name) return true;
268-
return false;
323+
has(name: string, value?: string): boolean {
324+
if (value !== undefined) {
325+
for (const [k, v] of this.#pairs) {
326+
if (k === name && v === value) return true;
327+
}
328+
return false;
329+
} else {
330+
for (const [k, _] of this.#pairs) if (k === name) return true;
331+
return false;
332+
}
269333
}
270334

271335
toString() {
@@ -289,9 +353,16 @@ class URLSearchParams {
289353
*values() {
290354
for (const [, v] of this.#pairs) yield v;
291355
}
292-
}
293356

294-
// searchParams is defined per-instance in the constructor above.
357+
sort() {
358+
this.#pairs.sort((a, b) => {
359+
if (a[0] < b[0]) return -1;
360+
if (a[0] > b[0]) return 1;
361+
return 0;
362+
});
363+
this.#updateURL();
364+
}
365+
}
295366

296367
// @ts-ignore globalThis is not readonly
297368
globalThis.URL = URL;

tests/expectation.json

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@
104104
"css-images.sub.tentative.html": "CRASH",
105105
"current.html": "CRASH",
106106
"dangling-markup-mitigation-allowed-apis.https.html": "CRASH",
107-
"dangling-markup-mitigation-data-url.tentative.sub.html": "FAIL",
107+
"dangling-markup-mitigation-data-url.tentative.sub.html": "CRASH",
108108
"dangling-markup-mitigation.tentative.html": "FAIL",
109109
"dangling-markup-mitigation.tentative.https.html": "FAIL",
110110
"data-url-iframe.html": "FAIL",
@@ -213,7 +213,7 @@
213213
"form-submission.sub.html": "CRASH",
214214
"formdata.any.js": "FAIL",
215215
"freshness.any.js": "CRASH",
216-
"gc.any.js": "FAIL",
216+
"gc.any.js": "CRASH",
217217
"general.any.js": "FAIL",
218218
"general.window.js": "FAIL",
219219
"gzip-body.any.js": "FAIL",
@@ -239,7 +239,7 @@
239239
"header-values.any.js": "CRASH",
240240
"headers-basic.any.js": "FAIL",
241241
"headers-casing.any.js": "FAIL",
242-
"headers-combine.any.js": "FAIL",
242+
"headers-combine.any.js": "CRASH",
243243
"headers-errors.any.js": "FAIL",
244244
"headers-no-cors.any.js": "FAIL",
245245
"headers-normalize.any.js": "FAIL",
@@ -370,7 +370,7 @@
370370
"redirect-origin.any.js": "CRASH",
371371
"redirect-referrer-override.any.js": "CRASH",
372372
"redirect-referrer.any.js": "CRASH",
373-
"redirect-schemes.any.js": "FAIL",
373+
"redirect-schemes.any.js": "CRASH",
374374
"redirect-to-dataurl.any.js": "CRASH",
375375
"redirect-to-url-with-credentials.https.html": "CRASH",
376376
"redirect-upload.h2.any.js": "CRASH",
@@ -408,7 +408,7 @@
408408
"request-constructor-init-body-override.any.js": "FAIL",
409409
"request-consume-empty.any.js": "FAIL",
410410
"request-consume.any.js": "FAIL",
411-
"request-disturbed.any.js": "CRASH",
411+
"request-disturbed.any.js": "FAIL",
412412
"request-error.any.js": "CRASH",
413413
"request-error.js": "CRASH",
414414
"request-forbidden-headers.any.js": "CRASH",
@@ -443,17 +443,17 @@
443443
"response-clone.any.js": "CRASH",
444444
"response-consume-empty.any.js": "FAIL",
445445
"response-consume-stream.any.js": "FAIL",
446-
"response-consume.html": "FAIL",
446+
"response-consume.html": "TIMEOUT",
447447
"response-error-from-stream.any.js": "FAIL",
448448
"response-error.any.js": "FAIL",
449449
"response-from-stream.any.js": "FAIL",
450450
"response-headers-guard.any.js": "FAIL",
451-
"response-init-001.any.js": "CRASH",
451+
"response-init-001.any.js": "FAIL",
452452
"response-init-002.any.js": "CRASH",
453453
"response-init-contenttype.any.js": "FAIL",
454454
"response-null-body.any.js": "FAIL",
455455
"response-static-error.any.js": "FAIL",
456-
"response-static-json.any.js": "CRASH",
456+
"response-static-json.any.js": "FAIL",
457457
"response-static-redirect.any.js": "FAIL",
458458
"response-stream-bad-chunk.any.js": "FAIL",
459459
"response-stream-disturbed-1.any.js": "FAIL",
@@ -470,7 +470,7 @@
470470
"response_block.tentative.https.html": "CRASH",
471471
"response_block_probe.js": "SKIP",
472472
"revalidate-not-blocked-by-csp.html": "CRASH",
473-
"sandboxed-iframe.html": "FAIL",
473+
"sandboxed-iframe.html": "CRASH",
474474
"scheme-about.any.js": "FAIL",
475475
"scheme-blob.sub.any.js": "CRASH",
476476
"scheme-data.any.js": "FAIL",
@@ -621,7 +621,7 @@
621621
"a-element-origin.js": "FAIL",
622622
"a-element.html": "FAIL",
623623
"a-element.js": "FAIL",
624-
"data-uri-fragment.html": "FAIL",
624+
"data-uri-fragment.html": "CRASH",
625625
"failure.html": "FAIL",
626626
"historical.any.js": "CRASH",
627627
"idlharness.any.js": "CRASH",
@@ -630,16 +630,16 @@
630630
"toascii.window.js": "FAIL",
631631
"url-constructor.any.js": "FAIL",
632632
"url-origin.any.js": "FAIL",
633-
"url-searchparams.any.js": "FAIL",
633+
"url-searchparams.any.js": "CRASH",
634634
"url-setters-a-area.window.js": "FAIL",
635635
"url-setters-stripping.any.js": "FAIL",
636636
"url-setters.any.js": "FAIL",
637637
"url-statics-canparse.any.js": "FAIL",
638638
"url-statics-parse.any.js": "FAIL",
639-
"url-tojson.any.js": "FAIL",
639+
"url-tojson.any.js": "CRASH",
640640
"urlencoded-parser.any.js": "CRASH",
641641
"urlsearchparams-append.any.js": "CRASH",
642-
"urlsearchparams-constructor.any.js": "FAIL",
642+
"urlsearchparams-constructor.any.js": "CRASH",
643643
"urlsearchparams-delete.any.js": "FAIL",
644644
"urlsearchparams-foreach.any.js": "FAIL",
645645
"urlsearchparams-get.any.js": "CRASH",
@@ -653,4 +653,4 @@
653653
}
654654
},
655655
"global_expectations": {}
656-
}
656+
}

tests/js/result_output.js

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
1-
setTimeout(function() {
2-
console.log("WPT_RESULTS:" + JSON.stringify(_test_results));
3-
if (typeof process !== "undefined" && process.exit) {
4-
process.exit(0);
1+
const outputResults = () => {
2+
try {
3+
const resultsJson = JSON.stringify(_test_results);
4+
console.log("WPT_RESULTS:" + resultsJson);
5+
} catch (e) {
6+
console.error("Failed to output test results:", e);
57
}
6-
}, 10);
8+
};
9+
10+
outputResults();
11+
12+
setTimeout(outputResults, 10);
13+
14+
setTimeout(() => {
15+
outputResults();
16+
Andromeda.exit(0);
17+
}, 50);

0 commit comments

Comments
 (0)