mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-04-21 11:26:15 +02:00
fix(opener): allow open network share locations (#3343)
* fix(opener): allow open network share locations * Clippy * Move to a seperate file * Add license header * test(opener): add unit tests for shell_parent_path and absolute functions (#1) * Keep `absolute` non pub in `windows_shell_path` * Add change file --------- Co-authored-by: Mark Gandolfo <mark@gandolfo.com.au>
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"opener": patch
|
||||
"opener-js": patch
|
||||
---
|
||||
|
||||
Fix `revealItemInDir`/`reveal_items_in_dir` can't reveal network paths like `\\wsl.localhost\Ubuntu\etc` on Windows
|
||||
@@ -41,6 +41,7 @@ features = [
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
"Win32_System_Com",
|
||||
"Win32_System_Registry",
|
||||
"Win32_Storage_FileSystem",
|
||||
]
|
||||
|
||||
[target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"netbsd\", target_os = \"openbsd\"))".dependencies]
|
||||
|
||||
@@ -31,6 +31,7 @@ pub enum Error {
|
||||
Win32Error(#[from] windows::core::Error),
|
||||
#[error("Path doesn't have a parent: {0}")]
|
||||
NoParent(PathBuf),
|
||||
// TODO: Add the underlying io::Error to this variant
|
||||
#[cfg(windows)]
|
||||
#[error("Failed to convert path '{0}' to ITEMIDLIST")]
|
||||
FailedToConvertPathToItemIdList(PathBuf),
|
||||
|
||||
@@ -20,6 +20,8 @@ mod open;
|
||||
mod reveal_item_in_dir;
|
||||
mod scope;
|
||||
mod scope_entry;
|
||||
#[cfg(windows)]
|
||||
mod windows_shell_path;
|
||||
|
||||
pub use error::Error;
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Reveal a path the system's default explorer.
|
||||
/// Reveal a path in the system's default explorer.
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
/// - **Android / iOS:** Unsupported.
|
||||
pub fn reveal_item_in_dir<P: AsRef<Path>>(path: P) -> crate::Result<()> {
|
||||
let path = dunce::canonicalize(path.as_ref())?;
|
||||
let path = canonicalize(path.as_ref())?;
|
||||
|
||||
#[cfg(any(
|
||||
windows,
|
||||
@@ -35,7 +35,7 @@ pub fn reveal_item_in_dir<P: AsRef<Path>>(path: P) -> crate::Result<()> {
|
||||
Err(crate::Error::UnsupportedPlatform)
|
||||
}
|
||||
|
||||
/// Reveal the paths the system's default explorer.
|
||||
/// Reveal multiple paths in the system's default explorer.
|
||||
///
|
||||
/// ## Platform-specific:
|
||||
///
|
||||
@@ -48,7 +48,7 @@ where
|
||||
let mut canonicalized = vec![];
|
||||
|
||||
for path in paths {
|
||||
let path = dunce::canonicalize(path.as_ref())?;
|
||||
let path = canonicalize(path.as_ref())?;
|
||||
canonicalized.push(path);
|
||||
}
|
||||
|
||||
@@ -75,10 +75,21 @@ where
|
||||
Err(crate::Error::UnsupportedPlatform)
|
||||
}
|
||||
|
||||
fn canonicalize(path: &Path) -> crate::Result<PathBuf> {
|
||||
#[cfg(windows)]
|
||||
let path = crate::windows_shell_path::absolute_and_check_exists(dunce::simplified(path))?;
|
||||
#[cfg(not(windows))]
|
||||
let path = std::fs::canonicalize(path)?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
mod imp {
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::HashMap,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use windows::Win32::UI::Shell::Common::ITEMIDLIST;
|
||||
use windows::{
|
||||
@@ -101,10 +112,9 @@ mod imp {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut grouped_paths: HashMap<&Path, Vec<&Path>> = HashMap::new();
|
||||
let mut grouped_paths: HashMap<Cow<Path>, Vec<&Path>> = HashMap::new();
|
||||
for path in paths {
|
||||
let parent = path
|
||||
.parent()
|
||||
let parent = crate::windows_shell_path::shell_parent_path(path)
|
||||
.ok_or_else(|| crate::Error::NoParent(path.to_path_buf()))?;
|
||||
grouped_paths.entry(parent).or_default().push(path);
|
||||
}
|
||||
@@ -112,7 +122,7 @@ mod imp {
|
||||
let _ = unsafe { CoInitialize(None) };
|
||||
|
||||
for (parent, to_reveals) in grouped_paths {
|
||||
let parent_item_id_list = OwnedItemIdList::new(parent)?;
|
||||
let parent_item_id_list = OwnedItemIdList::new(&parent)?;
|
||||
let to_reveals_item_id_list = to_reveals
|
||||
.iter()
|
||||
.map(|to_reveal| OwnedItemIdList::new(to_reveal))
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
ffi::OsString,
|
||||
io,
|
||||
os::windows::ffi::OsStringExt,
|
||||
path::{Component, Path, PathBuf, Prefix, PrefixComponent},
|
||||
};
|
||||
|
||||
use windows::{core::HSTRING, Win32::Storage::FileSystem::GetFullPathNameW};
|
||||
|
||||
pub fn absolute_and_check_exists(path: &Path) -> io::Result<PathBuf> {
|
||||
let path = absolute(path)?;
|
||||
if path.exists() {
|
||||
Ok(path)
|
||||
} else {
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
"path doesn't exist",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Switch to use `std::path::absolute` once MSRV > 1.79
|
||||
// Modified from https://github.com/rust-lang/rust/blob/b49ecc9eb70a51e89f32a7358e790f7b3808ccb3/library/std/src/sys/path/windows.rs#L185
|
||||
// Note: this doesn't resolve symlinks
|
||||
fn absolute(path: &Path) -> io::Result<PathBuf> {
|
||||
if path.as_os_str().is_empty() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"cannot make an empty path absolute",
|
||||
));
|
||||
}
|
||||
|
||||
let prefix = path.components().next();
|
||||
// Verbatim paths should not be modified.
|
||||
if prefix
|
||||
.map(|component| {
|
||||
let Component::Prefix(prefix) = component else {
|
||||
return false;
|
||||
};
|
||||
matches!(
|
||||
prefix.kind(),
|
||||
Prefix::Verbatim(..) | Prefix::VerbatimDisk(..) | Prefix::VerbatimUNC(..)
|
||||
)
|
||||
})
|
||||
.unwrap_or(false)
|
||||
{
|
||||
// NULs in verbatim paths are rejected for consistency.
|
||||
if path.as_os_str().as_encoded_bytes().contains(&0) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"strings passed to WinAPI cannot contain NULs",
|
||||
));
|
||||
}
|
||||
return Ok(path.to_owned());
|
||||
}
|
||||
|
||||
// This is an additional check to make sure we don't pass in a single driver letter to GetFullPathNameW
|
||||
// which will resolves to the current working directory
|
||||
//
|
||||
// > https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfullpathnamew#:~:text=If%20you%20specify%20%22U%3A%22%20the%20path%20returned%20is%20the%20current%20directory%20on%20the%20%22U%3A%5C%22%20drive
|
||||
#[allow(clippy::collapsible_if)]
|
||||
if let Some(Component::Prefix(last_prefix)) = path.components().next_back() {
|
||||
if matches!(last_prefix.kind(), Prefix::Disk(..)) {
|
||||
return Ok(PathBuf::from(last_prefix.as_os_str()));
|
||||
}
|
||||
}
|
||||
|
||||
let path_hstring = HSTRING::from(path);
|
||||
|
||||
let size = unsafe { GetFullPathNameW(&path_hstring, None, None) };
|
||||
if size == 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
let mut buffer = vec![0; size as usize];
|
||||
let size = unsafe { GetFullPathNameW(&path_hstring, Some(&mut buffer), None) };
|
||||
if size == 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
|
||||
Ok(PathBuf::from(OsString::from_wide(&buffer[..size as usize])))
|
||||
}
|
||||
|
||||
/// Similar to [`Path::parent`] but resolves parent of `C:`/`C:\` to `""` and handles UNC host name (`\\wsl.localhost\Ubuntu\` to `\\wsl.localhost`)
|
||||
pub fn shell_parent_path(path: &Path) -> Option<Cow<'_, Path>> {
|
||||
fn handle_prefix(prefix: PrefixComponent<'_>) -> Option<Cow<'_, Path>> {
|
||||
match prefix.kind() {
|
||||
Prefix::UNC(host_name, _share_name) => {
|
||||
let mut path = OsString::from(r"\\");
|
||||
path.push(host_name);
|
||||
Some(PathBuf::from(path).into())
|
||||
}
|
||||
Prefix::Disk(_) => Some(PathBuf::from("").into()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
let mut components = path.components();
|
||||
let component = components.next_back()?;
|
||||
match component {
|
||||
Component::Normal(_) | Component::CurDir | Component::ParentDir => {
|
||||
Some(components.as_path().into())
|
||||
}
|
||||
Component::Prefix(prefix) => handle_prefix(prefix),
|
||||
// Handle cases like `C:\` and `\\wsl.localhost\Ubuntu\`
|
||||
Component::RootDir => {
|
||||
if let Component::Prefix(prefix) = components.next_back()? {
|
||||
handle_prefix(prefix)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::path::Path;
|
||||
|
||||
// absolute() tests
|
||||
|
||||
#[test]
|
||||
fn absolute_empty_error() {
|
||||
let err = absolute(Path::new("")).unwrap_err();
|
||||
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn absolute_verbatim_passthrough() {
|
||||
let path = Path::new(r"\\?\C:\foo");
|
||||
assert_eq!(absolute(path).unwrap(), path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn absolute_verbatim_unc_passthrough() {
|
||||
let path = Path::new(r"\\?\UNC\server\share");
|
||||
assert_eq!(absolute(path).unwrap(), path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn absolute_bare_drive_letter() {
|
||||
let result = absolute(Path::new("C:")).unwrap();
|
||||
assert_eq!(result, Path::new("C:"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn absolute_already_absolute() {
|
||||
let result = absolute(Path::new(r"C:\Windows")).unwrap();
|
||||
assert_eq!(result, Path::new(r"C:\Windows"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn absolute_unc_path() {
|
||||
let result = absolute(Path::new(r"\\server\share\folder")).unwrap();
|
||||
assert_eq!(result, Path::new(r"\\server\share\folder"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn absolute_converts_forward_slashes() {
|
||||
let result = absolute(Path::new("C:/Windows/System32")).unwrap();
|
||||
assert_eq!(result, Path::new(r"C:\Windows\System32"));
|
||||
}
|
||||
|
||||
// absolute_and_check_exists() tests
|
||||
|
||||
#[test]
|
||||
fn absolute_and_check_exists_existing_path() {
|
||||
assert!(absolute_and_check_exists(Path::new(r"C:\Windows")).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn absolute_and_check_exists_nonexistent_path() {
|
||||
let err = absolute_and_check_exists(Path::new(r"C:\nonexistent_xyz_12345")).unwrap_err();
|
||||
assert_eq!(err.kind(), io::ErrorKind::NotFound);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn absolute_and_check_exists_empty_propagates() {
|
||||
let err = absolute_and_check_exists(Path::new("")).unwrap_err();
|
||||
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
|
||||
}
|
||||
|
||||
// shell_parent_path() tests
|
||||
|
||||
#[test]
|
||||
fn shell_parent_path_local_path() {
|
||||
let result = shell_parent_path(Path::new(r"C:\Users\foo"));
|
||||
assert_eq!(result.as_deref(), Some(Path::new(r"C:\Users")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shell_parent_path_nested_path() {
|
||||
let result = shell_parent_path(Path::new(r"C:\a\b\c\d"));
|
||||
assert_eq!(result.as_deref(), Some(Path::new(r"C:\a\b\c")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shell_parent_path_drive_root_trailing() {
|
||||
let result = shell_parent_path(Path::new(r"C:\"));
|
||||
assert_eq!(result.as_deref(), Some(Path::new("")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shell_parent_path_bare_drive() {
|
||||
let result = shell_parent_path(Path::new("C:"));
|
||||
assert_eq!(result.as_deref(), Some(Path::new("")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shell_parent_path_unc_with_subfolder() {
|
||||
let result = shell_parent_path(Path::new(r"\\server\share\folder"));
|
||||
assert_eq!(result.as_deref(), Some(Path::new(r"\\server\share")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shell_parent_path_unc_share_trailing_slash() {
|
||||
let result = shell_parent_path(Path::new(r"\\server.local\share\"));
|
||||
assert_eq!(result.as_deref(), Some(Path::new(r"\\server.local")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shell_parent_path_unc_share_no_slash() {
|
||||
let result = shell_parent_path(Path::new(r"\\server\share"));
|
||||
assert_eq!(result.as_deref(), Some(Path::new(r"\\server")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shell_parent_path_relative() {
|
||||
let result = shell_parent_path(Path::new(r"foo\bar"));
|
||||
assert_eq!(result.as_deref(), Some(Path::new("foo")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shell_parent_path_single_component() {
|
||||
let result = shell_parent_path(Path::new("foo"));
|
||||
assert_eq!(result.as_deref(), Some(Path::new("")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shell_parent_path_empty() {
|
||||
let result = shell_parent_path(Path::new(""));
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shell_parent_path_verbatim() {
|
||||
let result = shell_parent_path(Path::new(r"\\?\C:\foo"));
|
||||
assert_eq!(result.as_deref(), Some(Path::new(r"\\?\C:\")));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user