mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-30 13:19:44 +02:00
fix(extensions): restore URL globals for provider link resolution
This commit is contained in:
1 parent
aa99726439
commit
eb717a6e7b
6 files changed
+429
-1
No files matched your search
@@ -17,6 +17,7 @@ pub(crate) fn register<'js>(
|
||||
services: &ExtensionServices,
|
||||
) -> rquickjs::Result<Object<'js>> {
|
||||
let host = Object::new(ctx.clone())?;
|
||||
crate::url_host::register(ctx, &host)?;
|
||||
crate::utility_host::register(ctx, &host, Arc::clone(&control), services)?;
|
||||
crate::legacy_host::register(ctx, &host, Arc::clone(&control), services)?;
|
||||
let item_control = Arc::clone(&control);
|
||||
|
||||
@@ -30,6 +30,7 @@ mod session_host;
|
||||
pub mod signed_session;
|
||||
pub mod storage;
|
||||
pub mod transfer_policy;
|
||||
mod url_host;
|
||||
mod utility_host;
|
||||
|
||||
pub use runtime::{ExtensionError, ExtensionRuntime, ExtensionServices, RuntimeLimits};
|
||||
@@ -67,6 +67,46 @@
|
||||
}
|
||||
return goString(value);
|
||||
}
|
||||
function queryMethods(values, mutable) {
|
||||
const result = {};
|
||||
for (const method of mutable ? ["append", "delete", "get", "getAll", "has", "set"] : ["get", "getAll", "has"]) {
|
||||
if (method === "append" || method === "set" || method === "delete") {
|
||||
result[method] = function(key, value) {
|
||||
if (arguments.length >= (method === "delete" ? 1 : 2)) {
|
||||
values.write(method, goString(key), method === "delete" ? "" : goString(value));
|
||||
}
|
||||
};
|
||||
} else {
|
||||
result[method] = function(key) {
|
||||
if (!arguments.length) return method === "getAll" ? [] : method === "has" ? false : null;
|
||||
return values.read(method, goString(key));
|
||||
};
|
||||
}
|
||||
}
|
||||
result.toString = function() { return values.encode(); };
|
||||
return result;
|
||||
}
|
||||
globalThis.URL = function URL(input, base) {
|
||||
if (!new.target) throw new TypeError("URL requires new");
|
||||
if (!arguments.length) { this.href = ""; return; }
|
||||
const parsed = host.parseURL(goString(input), base === undefined ? undefined : goString(base));
|
||||
Object.assign(this, parsed);
|
||||
if (parsed.searchParams) {
|
||||
this.searchParams = queryMethods(parsed.searchParams, false);
|
||||
this.toString = this.toJSON = function() { return parsed.href; };
|
||||
}
|
||||
};
|
||||
globalThis.URLSearchParams = function URLSearchParams(init) {
|
||||
if (!new.target) throw new TypeError("URLSearchParams requires new");
|
||||
const values = host.parseQuery(typeof init === "string" ? goString(init).replace(/^\?/, "") : "");
|
||||
const boxed = [boxedStringValue, boxedNumberValue, boxedBooleanValue].some(valueOf => {
|
||||
try { stringApply(valueOf, init, []); return true; } catch (_) { return false; }
|
||||
});
|
||||
if (isMap(init) && !boxed) {
|
||||
for (const key of Object.keys(init)) values.write("set", key.toWellFormed(), formatGo(init[key]));
|
||||
}
|
||||
Object.assign(this, queryMethods(values, true));
|
||||
};
|
||||
const byteArrays = new WeakSet();
|
||||
const responseByteArrays = new WeakMap();
|
||||
function emptyBytes() {
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
use crate::host::decode_go_utf8;
|
||||
use rquickjs::{Ctx, Function, IntoJs, Object, Value};
|
||||
use spotiflac_network::{
|
||||
query,
|
||||
url::{UrlParts, unescape_path},
|
||||
};
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
pub(crate) fn register<'js>(ctx: &Ctx<'js>, host: &Object<'js>) -> rquickjs::Result<()> {
|
||||
host.set("parseURL", Function::new(ctx.clone(), parse_url)?)?;
|
||||
host.set("parseQuery", Function::new(ctx.clone(), query_object)?)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_url<'js>(
|
||||
ctx: Ctx<'js>,
|
||||
input: String,
|
||||
base: Option<String>,
|
||||
) -> rquickjs::Result<Object<'js>> {
|
||||
let object = Object::new(ctx.clone())?;
|
||||
let parsed = base
|
||||
.as_deref()
|
||||
.and_then(UrlParts::parse)
|
||||
.and_then(|base| base.resolve_reference(&input))
|
||||
.or_else(|| UrlParts::parse(&input));
|
||||
let Some(parsed) = parsed else {
|
||||
object.set("href", input)?;
|
||||
return Ok(object);
|
||||
};
|
||||
object.set("href", parsed.reference_string())?;
|
||||
object.set("protocol", format!("{}:", parsed.scheme))?;
|
||||
object.set("host", decode_go_utf8(&parsed.host))?;
|
||||
let host_end = parsed.host.len() - parsed.port.as_ref().map_or(0, |port| port.len() + 1);
|
||||
let hostname = &parsed.host[..host_end];
|
||||
let hostname = hostname
|
||||
.strip_prefix(b"[")
|
||||
.and_then(|host| host.strip_suffix(b"]"))
|
||||
.unwrap_or(hostname);
|
||||
object.set("hostname", decode_go_utf8(hostname))?;
|
||||
object.set("port", parsed.port.as_deref().unwrap_or_default())?;
|
||||
object.set(
|
||||
"pathname",
|
||||
if parsed.raw_path.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
decode_go_utf8(&parsed.path)
|
||||
},
|
||||
)?;
|
||||
object.set(
|
||||
"search",
|
||||
if parsed.raw_query.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("?{}", parsed.raw_query)
|
||||
},
|
||||
)?;
|
||||
let fragment = decode_go_utf8(&unescape_path(&parsed.fragment).unwrap_or_default());
|
||||
object.set(
|
||||
"hash",
|
||||
if fragment.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("#{fragment}")
|
||||
},
|
||||
)?;
|
||||
object.set(
|
||||
"origin",
|
||||
format!("{}://{}", parsed.scheme, decode_go_utf8(&parsed.host)),
|
||||
)?;
|
||||
object.set("username", decode_go_utf8(&parsed.username))?;
|
||||
object.set(
|
||||
"password",
|
||||
decode_go_utf8(parsed.password.as_deref().unwrap_or_default()),
|
||||
)?;
|
||||
object.set("searchParams", query_object(ctx, parsed.raw_query)?)?;
|
||||
Ok(object)
|
||||
}
|
||||
|
||||
fn query_object<'js>(ctx: Ctx<'js>, input: String) -> rquickjs::Result<Object<'js>> {
|
||||
let object = Object::new(ctx.clone())?;
|
||||
let values = Rc::new(RefCell::new(query::parse(&input)));
|
||||
let read = Rc::clone(&values);
|
||||
object.set(
|
||||
"read",
|
||||
Function::new(
|
||||
ctx.clone(),
|
||||
move |ctx: Ctx<'js>, method: String, key: String| {
|
||||
let values = read.borrow();
|
||||
let found = values.get(key.as_bytes());
|
||||
match method.as_str() {
|
||||
"has" => found.is_some().into_js(&ctx),
|
||||
"getAll" => match found {
|
||||
Some(values) => values
|
||||
.iter()
|
||||
.map(|value| decode_go_utf8(value))
|
||||
.collect::<Vec<_>>()
|
||||
.into_js(&ctx),
|
||||
None => Ok(Value::new_null(ctx)),
|
||||
},
|
||||
_ => match found
|
||||
.and_then(|values| values.first())
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
Some(value) => decode_go_utf8(value).into_js(&ctx),
|
||||
None => Ok(Value::new_null(ctx)),
|
||||
},
|
||||
}
|
||||
},
|
||||
)?,
|
||||
)?;
|
||||
let write = Rc::clone(&values);
|
||||
object.set(
|
||||
"write",
|
||||
Function::new(
|
||||
ctx.clone(),
|
||||
move |method: String, key: String, value: String| {
|
||||
let mut values = write.borrow_mut();
|
||||
match method.as_str() {
|
||||
"append" => values
|
||||
.entry(key.into_bytes())
|
||||
.or_default()
|
||||
.push(value.into_bytes()),
|
||||
"set" => query::set(&mut values, &key, &value),
|
||||
_ => {
|
||||
values.remove(key.as_bytes());
|
||||
}
|
||||
}
|
||||
},
|
||||
)?,
|
||||
)?;
|
||||
object.set(
|
||||
"encode",
|
||||
Function::new(ctx, move || query::encode(&values.borrow()))?,
|
||||
)?;
|
||||
Ok(object)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{ExtensionRuntime, RuntimeLimits};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn url_globals_resolve_track_paths_and_album_query_ids() {
|
||||
let runtime = ExtensionRuntime::load(
|
||||
r#"
|
||||
registerExtension({probe() {
|
||||
return [typeof URL, typeof URLSearchParams, ...[
|
||||
"https://music.example.test/tracks/TRACK123/",
|
||||
"https://music.example.test/albums/ALBUM123?trackAsin=TRACK456",
|
||||
"https://music.example.test/artists/ARTIST123"
|
||||
].map(link => {
|
||||
try {
|
||||
const url = new URL(link);
|
||||
if (url.hostname !== "music.example.test") return null;
|
||||
const track = url.searchParams.get("trackAsin");
|
||||
if (track) return track;
|
||||
const match = url.pathname.match(/^\/tracks\/([^/]+)/);
|
||||
return match ? match[1] : null;
|
||||
} catch (_) { return null; }
|
||||
})];
|
||||
}});
|
||||
"#,
|
||||
"{}",
|
||||
RuntimeLimits::default(),
|
||||
)
|
||||
.unwrap();
|
||||
let result: serde_json::Value =
|
||||
serde_json::from_str(&runtime.call("probe", "[]", None, 1000).unwrap()).unwrap();
|
||||
assert_eq!(
|
||||
result,
|
||||
json!(["function", "function", "TRACK123", "TRACK456", null])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_and_query_objects_preserve_legacy_constructor_contracts() {
|
||||
let runtime = ExtensionRuntime::load(r#"
|
||||
registerExtension({probe() {
|
||||
const url = new URL("../a%2Fb?z=2&z=3&empty&bad=%Q&semi=a;b#hi%20there", "https://u:p@EXAMPLE.test:443/x/y");
|
||||
const original = url.toString();
|
||||
url.href = "changed";
|
||||
const params = new URLSearchParams("?z=first&empty=&z=second&raw=%FF&bad=%Q&semi=a;b");
|
||||
const before = [params.get("z"), params.getAll("z"), params.get("empty"), params.has("empty"), params.getAll("missing"), params.getAll(), params.toString()];
|
||||
params.append("z", "last"); params.set("empty", "a b"); params.delete("raw"); params.set("ignored");
|
||||
const object = new URLSearchParams({z: [1, 2], nil: null, ok: true});
|
||||
return [original, url.protocol, url.host, url.hostname, url.port, url.pathname, url.search, url.hash, url.origin, url.username, url.password,
|
||||
url.toString(), JSON.stringify(url), url.searchParams.toString(), typeof url.searchParams.set,
|
||||
before, params.toString(), object.toString(),
|
||||
Object.keys(new URL()), Object.keys(new URL("http://[bad]")),
|
||||
new URL("https://example.test?").toString(), new URL("mailto:a@example.test").toString(),
|
||||
new URL("/a", "broken base").toString(), new URL("https://[fe80::1%25en0]:80/a").host,
|
||||
Object.keys(params), new URLSearchParams(new String("?x=y")).toString(),
|
||||
new URL("https://%FF%E0%A4.test/").hostname,
|
||||
new URLSearchParams("x=\ud800&\udfff=v").toString()];
|
||||
}});
|
||||
"#, "{}", RuntimeLimits::default()).unwrap();
|
||||
let result: serde_json::Value =
|
||||
serde_json::from_str(&runtime.call("probe", "[]", None, 1000).unwrap()).unwrap();
|
||||
let href = "https://u:p@EXAMPLE.test:443/a%2Fb?z=2&z=3&empty&bad=%Q&semi=a;b#hi%20there";
|
||||
assert_eq!(
|
||||
result,
|
||||
json!([
|
||||
href,
|
||||
"https:",
|
||||
"EXAMPLE.test:443",
|
||||
"EXAMPLE.test",
|
||||
"443",
|
||||
"/a/b",
|
||||
"?z=2&z=3&empty&bad=%Q&semi=a;b",
|
||||
"#hi there",
|
||||
"https://EXAMPLE.test:443",
|
||||
"u",
|
||||
"p",
|
||||
href,
|
||||
serde_json::to_string(href).unwrap(),
|
||||
"empty=&z=2&z=3",
|
||||
"undefined",
|
||||
[
|
||||
"first",
|
||||
["first", "second"],
|
||||
null,
|
||||
true,
|
||||
null,
|
||||
[],
|
||||
"empty=&raw=%FF&z=first&z=second"
|
||||
],
|
||||
"empty=a+b&z=first&z=second&z=last",
|
||||
"nil=%3Cnil%3E&ok=true&z=%5B1+2%5D",
|
||||
["href"],
|
||||
["href"],
|
||||
"https://example.test?",
|
||||
"mailto:a@example.test",
|
||||
"/a",
|
||||
"[fe80::1%en0]:80",
|
||||
[
|
||||
"append", "delete", "get", "getAll", "has", "set", "toString"
|
||||
],
|
||||
"",
|
||||
"���.test",
|
||||
"x=%EF%BF%BD&%EF%BF%BD=v"
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user