fix(core): rewrite GitHub blob avatar URLs to raw.githubusercontent.com

Pack authors often paste the browsable github.com/.../blob/... URL
instead of the raw one. That endpoint only serves the image after a
two-hop redirect and is subject to GitHub's anti-abuse rate limiting,
so some avatars in a pack would intermittently fail to load on-device
even though the same URL worked fine in a browser or curl. Normalize
these to the direct raw.githubusercontent.com URL during pack parsing
so platforms never depend on the redirect.

Also restore the rquickjs "bindgen" feature, needed to cross-compile
for aarch64-linux-android since rquickjs-sys doesn't ship prebuilt
bindings for that target (host builds don't need it).
This commit is contained in:
KhooLy 2026-07-28 02:23:20 +03:00
parent 02e50bc92e
commit 6c1e6fa4e8
3 changed files with 186 additions and 5 deletions

133
Cargo.lock generated
View file

@ -367,6 +367,26 @@ dependencies = [
"virtue",
]
[[package]]
name = "bindgen"
version = "0.72.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
dependencies = [
"bitflags",
"cexpr",
"clang-sys",
"itertools 0.10.5",
"log",
"prettyplease",
"proc-macro2",
"quote",
"regex",
"rustc-hash",
"shlex 1.3.0",
"syn",
]
[[package]]
name = "bitflags"
version = "2.11.1"
@ -521,7 +541,7 @@ dependencies = [
"find-msvc-tools",
"jobserver",
"libc",
"shlex",
"shlex 2.0.1",
]
[[package]]
@ -530,6 +550,15 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
[[package]]
name = "cexpr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
@ -614,6 +643,17 @@ dependencies = [
"inout 0.2.2",
]
[[package]]
name = "clang-sys"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
dependencies = [
"glob",
"libc",
"libloading",
]
[[package]]
name = "clap"
version = "4.5.60"
@ -725,6 +765,15 @@ dependencies = [
"typewit",
]
[[package]]
name = "convert_case"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "core-foundation"
version = "0.10.1"
@ -1317,6 +1366,12 @@ dependencies = [
"url",
]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foldhash"
version = "0.1.5"
@ -2158,6 +2213,16 @@ version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "libloading"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
dependencies = [
"cfg-if",
"windows-link",
]
[[package]]
name = "libm"
version = "0.2.16"
@ -2914,6 +2979,15 @@ dependencies = [
"wnaf",
]
[[package]]
name = "proc-macro-crate"
version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
dependencies = [
"toml_edit",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
@ -3317,6 +3391,7 @@ dependencies = [
"either",
"indexmap 2.14.0",
"rquickjs-core",
"rquickjs-macro",
]
[[package]]
@ -3335,12 +3410,30 @@ dependencies = [
"rquickjs-sys",
]
[[package]]
name = "rquickjs-macro"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7204086521740ca03f33036579801871aa5b714a0e7f68b4c2e98d68b0380bfb"
dependencies = [
"convert_case",
"fnv",
"ident_case",
"indexmap 2.14.0",
"proc-macro-crate",
"proc-macro2",
"quote",
"rquickjs-core",
"syn",
]
[[package]]
name = "rquickjs-sys"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eef73520804cf5aa4876097ac5733058d628c769eba9678e0f4dba5a4ef79703"
dependencies = [
"bindgen",
"cc",
]
@ -3789,6 +3882,12 @@ dependencies = [
"digest 0.11.3",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "shlex"
version = "2.0.1"
@ -4230,7 +4329,7 @@ dependencies = [
"indexmap 2.14.0",
"serde_core",
"serde_spanned",
"toml_datetime",
"toml_datetime 0.7.5+spec-1.1.0",
"toml_parser",
"toml_writer",
"winnow 0.7.15",
@ -4245,6 +4344,27 @@ dependencies = [
"serde_core",
]
[[package]]
name = "toml_datetime"
version = "1.1.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
dependencies = [
"serde_core",
]
[[package]]
name = "toml_edit"
version = "0.25.13+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b"
dependencies = [
"indexmap 2.14.0",
"toml_datetime 1.1.1+spec-1.1.0",
"toml_parser",
"winnow 1.0.3",
]
[[package]]
name = "toml_parser"
version = "1.1.2+spec-1.1.0"
@ -4368,6 +4488,12 @@ version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "unicode-segmentation"
version = "1.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
[[package]]
name = "unicode-width"
version = "0.2.2"
@ -5107,6 +5233,9 @@ name = "winnow"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1"
dependencies = [
"memchr",
]
[[package]]
name = "wit-bindgen"

View file

@ -96,7 +96,7 @@ p256 = { version = "0.14.0", optional = true }
pbkdf2 = { version = "0.13.0", optional = true }
rand = { version = "0.10.2", optional = true }
regex = "1"
rquickjs = { version = "0.12.1", default-features = false, features = ["std", "chrono", "loader", "dyn-load", "either", "indexmap", "futures"], optional = true }
rquickjs = { version = "0.12.1", default-features = false, features = ["std", "chrono", "loader", "dyn-load", "either", "indexmap", "futures", "bindgen"], optional = true }
rsa = { version = "0.9.10", features = ["sha2"], optional = true }
scraper = { version = "0.27.0", optional = true }
sha1 = { version = "0.11.0", optional = true }

View file

@ -150,10 +150,14 @@ pub(crate) fn profile_avatar_pack_json(request_json: &str) -> Option<String> {
let mut avatars = Vec::new();
let mut seen_urls = HashSet::new();
for image in images {
let Some(url) = image.get("url").and_then(Value::as_str).map(str::trim) else {
let Some(raw_url) = image.get("url").and_then(Value::as_str).map(str::trim) else {
continue;
};
if !is_https_url(url) || !seen_urls.insert(url.to_string()) {
if !is_https_url(raw_url) {
continue;
}
let url = normalize_avatar_url(raw_url);
if !seen_urls.insert(url.clone()) {
continue;
}
let name = image
@ -246,6 +250,34 @@ fn percent_encode(value: &str) -> String {
.collect()
}
fn normalize_avatar_url(url: &str) -> String {
let Some(rest) = url.strip_prefix("https://github.com/") else {
return url.to_string();
};
let mut blob_segments = rest.splitn(2, "/blob/");
let Some(owner_repo) = blob_segments.next() else {
return url.to_string();
};
let Some(ref_and_path) = blob_segments.next() else {
return url.to_string();
};
let mut owner_repo_parts = owner_repo.splitn(2, '/');
let (Some(owner), Some(repo)) = (owner_repo_parts.next(), owner_repo_parts.next()) else {
return url.to_string();
};
if owner.is_empty() || repo.is_empty() {
return url.to_string();
}
let ref_and_path = ref_and_path.split('?').next().unwrap_or(ref_and_path);
let Some((reference, path)) = ref_and_path.split_once('/') else {
return url.to_string();
};
if reference.is_empty() || path.is_empty() {
return url.to_string();
}
format!("https://raw.githubusercontent.com/{owner}/{repo}/{reference}/{path}")
}
fn is_https_url(value: &str) -> bool {
let value = value.trim();
let Some(rest) = value.strip_prefix("https://") else {
@ -320,4 +352,24 @@ mod tests {
assert_eq!(output["avatars"].as_array().unwrap().len(), 1);
assert_eq!(output["avatars"][0]["name"], "A");
}
#[test]
fn pack_parser_rewrites_github_blob_urls_to_raw() {
let output: Value = serde_json::from_str(
&profile_avatar_pack_json(
r#"{
"manifestUrl":"https://example.com/pack.json",
"pack":{"title":"Hell's Paradise","images":[
{"name":"Choubei","url":"https://github.com/eueueue292/Fusion-Profile-Avatars/blob/main/Hells%20Paradise/Choubei.PNG?raw=true&v=3"}
]}
}"#,
)
.unwrap(),
)
.unwrap();
assert_eq!(
output["avatars"][0]["url"],
"https://raw.githubusercontent.com/eueueue292/Fusion-Profile-Avatars/main/Hells%20Paradise/Choubei.PNG"
);
}
}