From dd20a17edb0325be31f56b7e8dae2f323cfba28a Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Thu, 6 Aug 2026 14:36:04 +0200 Subject: [PATCH] test(host/library): the art tests build a file:// URL Windows can read MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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. --- crates/punktfunk-host/src/library/art.rs | 26 +++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/crates/punktfunk-host/src/library/art.rs b/crates/punktfunk-host/src/library/art.rs index 0db6660e..f5324497 100644 --- a/crates/punktfunk-host/src/library/art.rs +++ b/crates/punktfunk-host/src/library/art.rs @@ -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]