test(host/library): the art tests build a file:// URL Windows can read

`local_art_bytes_is_confined_and_image_only` and `posix_local_art_is_classified_and_proxied`
built their `file://` values as `format!("file://{path}")`. On Windows that yields
`file://C:\covers\cover.png`, whose authority is `C:` — a UNC reference, not a local
file — so the read half failed on the box and the host suite was red there.

The parser is right and the tests were wrong: `@punktfunk/plugin-kit/library`'s `fileUrl`
emits `file:///C:/covers/cover.png` (three slashes, forward separators) and
`file_url_to_path` documents exactly that. A shared `file_url` helper now builds the value
the way the kit does, so both tests exercise the real plugin contract on both platforms
rather than a shape no plugin ever sends.

Found while gating the Playnite launch kinds on .173 — Linux CI never compiles these arms,
so the failure had gone unnoticed. Test-only: no product code changes.
This commit is contained in:
2026-08-06 14:36:04 +02:00
parent 8ff2c2e1c6
commit dd20a17edb
+21 -5
View File
@@ -671,9 +671,10 @@ mod tests {
} else {
"/home/u/.cache/lutris/coverart/cover.jpg".to_string()
};
let url = file_url(std::path::Path::new(&path));
let mut art = Artwork {
portrait: Some(path.clone()),
hero: Some(format!("file://{path}")),
hero: Some(url),
logo: Some("https://cdn/l.png".into()),
header: None,
};
@@ -757,7 +758,7 @@ mod tests {
// half that matters for the extracted scanners: they emit `file://` values, so if the
// conversion happened after the confinement check the check would be inspecting a string
// that is not the path being read.
let as_url = format!("file://{}", cover.to_str().unwrap());
let as_url = file_url(&cover);
assert_eq!(
local_art_bytes(&as_url)
.expect("file:// reads the same cover")
@@ -766,15 +767,15 @@ mod tests {
);
// …and a `file://` value is confined exactly like a bare one — no bypass by spelling.
assert!(
local_art_bytes(&format!("file://{}", elsewhere.to_str().unwrap())).is_none(),
local_art_bytes(&file_url(&elsewhere)).is_none(),
"file:// must not escape the art roots"
);
// Percent-encoded traversal is decoded BEFORE canonicalization, so it cannot hide from the
// `..` check.
assert!(
local_art_bytes(&format!(
"file://{}/%2e%2e/{}/cover.png",
dir.to_str().unwrap(),
"{}/%2e%2e/{}/cover.png",
file_url(&dir),
outside.file_name().unwrap().to_str().unwrap()
))
.is_none(),
@@ -789,6 +790,21 @@ mod tests {
let _ = std::fs::remove_dir_all(&outside);
}
/// Build a `file://` value the way the kit's `fileUrl` does, so these tests exercise the real
/// plugin contract on both platforms. A POSIX path keeps the two-slash form
/// (`file:///home/u/c.png` — empty authority, then the leading `/`); a Windows path becomes
/// `file:///C:/covers/c.png`, i.e. three slashes and forward separators. Building it as
/// `format!("file://{path}")` on Windows yields `file://C:\covers\c.png`, whose authority is
/// `C:` — that is a UNC reference, not a local file, and the parser is right to refuse it.
fn file_url(p: &std::path::Path) -> String {
let posix = p.to_str().unwrap().replace('\\', "/");
if posix.starts_with('/') {
format!("file://{posix}")
} else {
format!("file:///{posix}")
}
}
/// Write-time validation refuses what read-time would refuse, so an unservable path never even
/// reaches `library.json`. URLs are none of its business.
#[test]