fix(paths): one file:// helper and a shape-based absolute-path test
Seven sites built format!("file://{p}") by hand or tested starts_with('/')
for "absolute path". Both are Unix-only: file://C:/... parses the drive
letter as the URL HOST, and C:\... does not start with '/'. On Windows
that silently cost every album cover, every runtime-tinted icon and the
local folder tree - a URL that resolves to nothing renders as an empty
rectangle, not as an error, so nothing complained.
qbz_models::fs_url now owns file_url / local_path / is_local_abs_path,
deciding by path SHAPE (POSIX, drive, UNC) rather than by host, so the
tests run and fail everywhere rather than only on Windows.
Linux behaviour is unchanged, deliberately: the backslash-to-slash rewrite
fires only for a Windows-shaped input, because a backslash is a legal
filename character on Unix and rewriting it would point /home/v/AC\DC at
a directory that does not exist. There is a test for exactly that.
Three of the seven sites were not in the plan's list and were found by its
step 8 grep: local_ephemeral.rs, local_playlist_qt.rs, playback_qt.rs.
Two starts_with('/') hits stay and are not local-path tests:
qbz-library/src/mount_info.rs is cfg(linux) mount-point arithmetic, and
qbz-theme's is parsing a COSMIC desktop config that Windows never reads.
cc
This commit is contained in:
9 files changed
+203
-42
No files matched your search
@@ -219,7 +219,7 @@ pub fn classify(url: &str) -> ArtUrl {
|
||||
ArtUrl::Plex(fetch)
|
||||
};
|
||||
}
|
||||
if url.starts_with('/') {
|
||||
if qbz_models::fs_url::is_local_abs_path(url) {
|
||||
return ArtUrl::LocalFile(url.to_string());
|
||||
}
|
||||
ArtUrl::Empty
|
||||
@@ -240,27 +240,11 @@ pub fn classify(url: &str) -> ArtUrl {
|
||||
/// result (a tint decode, a copy) for exactly the files the escaping exists to
|
||||
/// support.
|
||||
pub fn local_path(url: &str) -> String {
|
||||
let raw = url.strip_prefix("file://").unwrap_or(url);
|
||||
raw.replace("%23", "#")
|
||||
.replace("%3F", "?")
|
||||
.replace("%25", "%")
|
||||
qbz_models::fs_url::local_path(url)
|
||||
}
|
||||
|
||||
pub fn file_url(path: &str) -> String {
|
||||
if path.starts_with("file://") {
|
||||
return path.to_string();
|
||||
}
|
||||
let mut out = String::with_capacity(path.len() + 8);
|
||||
out.push_str("file://");
|
||||
for ch in path.chars() {
|
||||
match ch {
|
||||
'%' => out.push_str("%25"),
|
||||
'#' => out.push_str("%23"),
|
||||
'?' => out.push_str("%3F"),
|
||||
_ => out.push(ch),
|
||||
}
|
||||
}
|
||||
out
|
||||
qbz_models::fs_url::file_url(path)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -622,8 +622,12 @@ impl qbz_icon_tint::QbzIconTint {
|
||||
}
|
||||
}
|
||||
|
||||
// Absolute paths start with '/', so this yields file:///home/...
|
||||
QString::from(&format!("file://{}", root.join(&hex).display()))
|
||||
// NOT format!("file://{path}"): on Windows that yields
|
||||
// file://C:/... where the drive parses as the URL HOST, and the
|
||||
// icon renders as an empty rectangle rather than an error.
|
||||
QString::from(&qbz_models::fs_url::file_url(
|
||||
&root.join(&hex).to_string_lossy(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -791,7 +791,7 @@ pub fn set_session_artwork(path: &str) {
|
||||
// `file://`), because that is the form the whole taxonomy downstream
|
||||
// expects.
|
||||
let ids: Vec<u64> = tracks.iter().map(|t| t.id as u64).collect();
|
||||
let url = format!("file://{path}");
|
||||
let url = qbz_models::fs_url::file_url(&path);
|
||||
crate::spawn(async move {
|
||||
let runtime = crate::app();
|
||||
if runtime.core().patch_queue_artwork(&ids, &url).await {
|
||||
|
||||
@@ -976,9 +976,13 @@ mod tests {
|
||||
assert!(one.artwork_url.as_deref().unwrap().contains("Disc 01"));
|
||||
assert!(five.artwork_url.as_deref().unwrap().contains("Disc 05"));
|
||||
let one_resolved = crate::artwork_qt::cached_path(one.artwork_url.as_deref().unwrap());
|
||||
// Compare separator-normalised: a `file://` URL is always `/`-separated,
|
||||
// so the round trip through it cannot hand back a host's backslashes.
|
||||
// Both forms open the same file on Windows.
|
||||
let slash = |p: &str| p.replace('\\', "/");
|
||||
assert_eq!(
|
||||
crate::artwork_qt::local_path(&one_resolved),
|
||||
disc_one.join("cover.jpg").to_string_lossy()
|
||||
slash(&crate::artwork_qt::local_path(&one_resolved)),
|
||||
slash(&disc_one.join("cover.jpg").to_string_lossy())
|
||||
);
|
||||
|
||||
std::fs::remove_dir_all(&box_dir).unwrap();
|
||||
|
||||
@@ -719,13 +719,9 @@ pub(crate) fn row_to_display(item: &RowItem) -> (PlaylistTrackRow, Option<QueueT
|
||||
album: album.clone(),
|
||||
album_version: None,
|
||||
duration_secs: *duration_secs,
|
||||
artwork_url: artwork_path.clone().map(|path| {
|
||||
if path.starts_with("file://") {
|
||||
path
|
||||
} else {
|
||||
format!("file://{path}")
|
||||
}
|
||||
}),
|
||||
artwork_url: artwork_path
|
||||
.clone()
|
||||
.map(|path| qbz_models::fs_url::file_url(&path)),
|
||||
hires: bit_depth.map(|depth| depth >= 24).unwrap_or(false),
|
||||
bit_depth: *bit_depth,
|
||||
sample_rate: *sample_rate,
|
||||
|
||||
@@ -1168,13 +1168,7 @@ async fn offline_album_queue(album_id: &str) -> Result<Vec<QueueTrack>, String>
|
||||
let artwork = local
|
||||
.and_then(|row| row.artwork_path.clone())
|
||||
.or_else(|| cached.resolve_cover_path(&cache_path))
|
||||
.map(|path| {
|
||||
if path.starts_with("file://") {
|
||||
path
|
||||
} else {
|
||||
format!("file://{path}")
|
||||
}
|
||||
});
|
||||
.map(|path| qbz_models::fs_url::file_url(&path));
|
||||
let album = cached.album.clone().unwrap_or_default();
|
||||
let sample_rate = cached
|
||||
.sample_rate
|
||||
|
||||
Reference in new issue
Block a user