From 67322e3ebf6c595e5fc48315b0de25f952341686 Mon Sep 17 00:00:00 2001 From: Anders Conbere Date: Mon, 25 May 2026 01:50:31 -0700 Subject: [PATCH] Improve FileLoader file uri to path handling for windows (#8163) Current behavior fails when translating file uris that contain windows UNC paths. This commit attempts to fix that behavior by looking at the hostname attribute of the uri and changing behavior if the hostname is present. * Closes * [x] I have followed the instructions in the PR template --- crates/egui/src/load.rs | 2 +- crates/egui_extras/src/loaders/file_loader.rs | 102 +++++++++++++++--- 2 files changed, 89 insertions(+), 15 deletions(-) diff --git a/crates/egui/src/load.rs b/crates/egui/src/load.rs index 99909eca1..c02f37c5c 100644 --- a/crates/egui/src/load.rs +++ b/crates/egui/src/load.rs @@ -72,7 +72,7 @@ use crate::Context; pub use self::{bytes_loader::DefaultBytesLoader, texture_loader::DefaultTextureLoader}; /// Represents a failed attempt at loading an image. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum LoadError { /// Programmer error: There are no image loaders installed. NoImageLoaders, diff --git a/crates/egui_extras/src/loaders/file_loader.rs b/crates/egui_extras/src/loaders/file_loader.rs index 3377476dd..bdafb6553 100644 --- a/crates/egui_extras/src/loaders/file_loader.rs +++ b/crates/egui_extras/src/loaders/file_loader.rs @@ -3,7 +3,7 @@ use egui::{ load::{Bytes, BytesLoadResult, BytesLoader, BytesPoll, LoadError}, mutex::Mutex, }; -use std::{sync::Arc, task::Poll, thread}; +use std::{path::PathBuf, sync::Arc, task::Poll, thread}; #[derive(Clone)] struct File { @@ -25,17 +25,39 @@ impl FileLoader { const PROTOCOL: &str = "file://"; -/// Remove the leading slash from the path if the target OS is Windows. +/// Converts a hopefully uri encoded string into a `PathBuf` /// -/// This is because Windows paths are not supposed to start with a slash. -/// For example, `file:///C:/path/to/file` is a valid URI, but `/C:/path/to/file` is not a valid path. -#[inline] -fn trim_extra_slash(s: &str) -> &str { +/// Note that there is only minimal translation of the uri string into a path to support windows +/// file and unc paths. Other translations like percent un-encoding are not handled. +fn convert_uri_to_path(s: &str) -> Result { + // File loader only supports the `file` protocol. + let s = s + .strip_prefix(PROTOCOL) + .ok_or(egui::load::LoadError::NotSupported)?; + if cfg!(target_os = "windows") { - s.trim_start_matches('/') - } else { - s + // Standard windows file uris should have the form + // + // file:///c:/path/to/the%20file.txt + // + // in which the hostname field is left out. Check for this by looking at the next character + // after the schema, if it's a slash then we likely have a standard file path. + if let Some(stripped) = s.strip_prefix("/") { + let path = PathBuf::from(stripped); + return Ok(path); + } + + // If it's not a standard file uri, it might be a UNC network path of the form + // + // file://hostname/path/to/the%20file.txt + // + // These file uris need to be converted into UNC correct and so need to have the leading + // two backslashes prepended. + let path = PathBuf::from(format!("\\\\{s}")); + return Ok(path); } + + Ok(PathBuf::from(s)) } impl BytesLoader for FileLoader { @@ -44,10 +66,7 @@ impl BytesLoader for FileLoader { } fn load(&self, ctx: &egui::Context, uri: &str) -> BytesLoadResult { - // File loader only supports the `file` protocol. - let Some(path) = uri.strip_prefix(PROTOCOL).map(trim_extra_slash) else { - return Err(LoadError::NotSupported); - }; + let path = convert_uri_to_path(uri)?; let mut cache = self.cache.lock(); if let Some(entry) = cache.get(uri).cloned() { @@ -66,7 +85,6 @@ impl BytesLoader for FileLoader { // We need to load the file at `path`. // Set the file to `pending` until we finish loading it. - let path = path.to_owned(); cache.insert(uri.to_owned(), Poll::Pending); drop(cache); @@ -146,3 +164,59 @@ impl BytesLoader for FileLoader { self.cache.lock().values().any(|entry| entry.is_pending()) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn check_convert_uri_to_path() { + let mut checks: Vec<(&str, Result, &str)> = vec![ + ( + "http://host/path/to/image.jpg", + Err(egui::load::LoadError::NotSupported), + "Schemas other than file are rejected.", + ), + ( + "https://host/path/to/image.jpg", + Err(egui::load::LoadError::NotSupported), + "Schemas other than file are rejected.", + ), + ( + "ftp://host/path/to/image.jpg", + Err(egui::load::LoadError::NotSupported), + "Schemas other than file are rejected.", + ), + ]; + if cfg!(target_os = "windows") { + let mut windows_checks = vec![ + ( + "file:///path/to/image.jpg", + Ok(PathBuf::from("path\\to\\image.jpg")), + "file uris with no hosts and no drive letter are turned into bare paths on windows.", + ), + ( + "file:///c:/path/to/image.jpg", + Ok(PathBuf::from("c:\\path\\to\\image.jpg")), + "file uris with no hosts and drive letters are turned into absolute paths on windows.", + ), + ( + "file://host/share/path/to/image.jpg", + Ok(PathBuf::from("\\\\host\\share\\path\\to\\image.jpg")), + "file uris with a host are turned into UNC paths with leading backslashes on windows.", + ), + ]; + checks.append(&mut windows_checks); + } else { + let mut more_checks = vec![( + "file://path/to/image.jpg", + Ok(PathBuf::from("path/to/image.jpg")), + "file uris are turned into bare paths.", + )]; + checks.append(&mut more_checks); + } + for (uri_s, path, reason) in checks { + assert_eq!(convert_uri_to_path(uri_s), path, "{reason}"); + } + } +}