Add support for more platforms via web browser fallback backend#309
Add support for more platforms via web browser fallback backend#309yeicor wants to merge 4 commits intoPolyMeilex:masterfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This pull request adds a web fallback backend for the rfd file dialog library, enabling support for platforms that lack native file dialog APIs (primarily Android and other non-standard platforms). The implementation uses a local HTTP server (Axum) combined with the system's web browser to present file dialogs to users.
Changes:
- Adds a new
web_fallbackbackend module with full dialog implementations (message, file picker, folder picker, and save dialogs) - Updates documentation to reflect support for additional platforms via web fallback
- Adds platform-conditional dependencies for the fallback backend (axum, tokio, webbrowser, tempfile)
Reviewed changes
Copilot reviewed 6 out of 7 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| src/backend/web_fallback.rs | New 1240-line backend implementation using HTTP server + browser approach for dialogs |
| src/backend.rs | Added conditional compilation to include web_fallback module for unsupported platforms |
| src/lib.rs | Updated feature matrix documentation to show "Others (Web)" column with full support |
| src/oneshot.rs | Added #[allow(dead_code)] attributes (module unused by web_fallback, which uses tokio::sync::oneshot) |
| Cargo.toml | Added platform-conditional dependencies for web fallback (webbrowser, tempfile, tokio, axum) |
| Cargo.lock | Lock file updates for new dependencies |
| README.md | Added "Others (via web fallback)" to supported platforms list |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| fn build_accept(filters: &[crate::file_dialog::Filter]) -> String { | ||
| let mut exts: Vec<String> = Vec::new(); | ||
| for filter in filters { | ||
| for ext in &filter.extensions { | ||
| if !ext.is_empty() { | ||
| exts.push(format!(".{}", ext)); | ||
| } | ||
| } | ||
| } | ||
| if exts.is_empty() { | ||
| String::new() | ||
| } else { | ||
| format!(r#"accept="{}""#, exts.join(",")) |
There was a problem hiding this comment.
The build_accept function generates an HTML accept attribute by directly concatenating file extensions without escaping them (line 65). If a malicious or malformed filter contains special characters (quotes, angle brackets, etc.), it could break out of the attribute and inject arbitrary HTML/JavaScript. File extensions should be HTML-escaped or validated to contain only safe characters before being inserted into the attribute.
|
|
||
| // Read the body with increased limit for many files | ||
| let read_start = Instant::now(); | ||
| let body_bytes = match axum::body::to_bytes(request.into_body(), 500 * 1024 * 1024).await { |
There was a problem hiding this comment.
The 500MB body size limit (line 715) may be excessive for a file dialog operation and could enable denial-of-service attacks by allowing users to consume large amounts of memory. Consider whether this limit is necessary for the intended use case, and if a smaller limit (e.g., 100MB) would be more appropriate. Also consider adding configuration to make this limit adjustable.
| let (tx, rx) = oneshot::channel(); | ||
| std::thread::spawn(move || { | ||
| let result = MessageDialogImpl::show(self); | ||
| let _ = tx.send(result); | ||
| }); | ||
| Box::pin(async move { rx.await.unwrap_or(MessageDialogResult::Cancel) }) |
There was a problem hiding this comment.
The async implementation spawns a new thread for each dialog operation (line 630), which then creates a new tokio runtime. This can be inefficient for applications that show multiple dialogs. Consider using a shared tokio runtime or a thread pool, especially if the async operations are expected to be used frequently. The current approach could lead to resource exhaustion in scenarios with many concurrent dialogs.
| let (tx, rx) = oneshot::channel(); | |
| std::thread::spawn(move || { | |
| let result = MessageDialogImpl::show(self); | |
| let _ = tx.send(result); | |
| }); | |
| Box::pin(async move { rx.await.unwrap_or(MessageDialogResult::Cancel) }) | |
| Box::pin(async move { | |
| match tokio::task::spawn_blocking(move || MessageDialogImpl::show(self)).await { | |
| Ok(result) => result, | |
| Err(_) => MessageDialogResult::Cancel, | |
| } | |
| }) |
| Err(_) => { | ||
| debug!("Web fallback: Timeout"); | ||
| None |
There was a problem hiding this comment.
When a timeout occurs (line 535-537), the spawned server task (line 515) will continue running indefinitely since no shutdown signal can be sent (the shutdown_rx is consumed by the graceful_shutdown handler at line 511). This leads to a resource leak - the server will keep listening on the port even after the dialog has timed out and the runtime is dropped. Consider using a timeout-aware shutdown mechanism or ensuring the server task is properly cancelled.
| let url = format!("http://127.0.0.1:{}/", addr.port()); | ||
| debug!("Web fallback: Server listening on {}", url); |
There was a problem hiding this comment.
The web server has no CSRF protection. Although the server binds to localhost (127.0.0.1), a malicious website could potentially submit forms to the local server if it can predict the port or scan for it. While the risk is limited since the server is short-lived and port is random, consider adding a random token to URLs and form submissions, or implementing origin checking to prevent cross-site requests from attacking the local server.
| if let Ok(byte) = u8::from_str_radix(&hex, 16) { | ||
| result.push(byte as char); |
There was a problem hiding this comment.
The url_decode function has a critical security vulnerability. Converting bytes directly to chars using byte as char (line 261) is incorrect for non-ASCII bytes and can lead to invalid UTF-8 sequences or security issues. URL-encoded bytes should be collected and then converted to a UTF-8 string. This could allow malformed input to bypass validation or cause unexpected behavior. Consider using a proper URL decoding library or implementing correct UTF-8 byte sequence handling.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This PR adds support for building and running the rfd crate on additional targets by introducing a web fallback backend.
The web fallback works by spawning a local HTTP server and opening the system browser to present file dialogs. This approach enables broad cross-platform compatibility in environments where native backends are unavailable.
Demo: Android, running without any special permissions
output.mp4