Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changes/opener-windows-network-path.md
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions plugins/opener/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
1 change: 1 addition & 0 deletions plugins/opener/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
2 changes: 2 additions & 0 deletions plugins/opener/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
Expand Down
32 changes: 21 additions & 11 deletions plugins/opener/src/reveal_item_in_dir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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:
///
Expand All @@ -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);
}

Expand All @@ -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::{
Expand All @@ -101,18 +112,17 @@ 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);
}

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))
Expand Down
255 changes: 255 additions & 0 deletions plugins/opener/src/windows_shell_path.rs
Original file line number Diff line number Diff line change
@@ -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:\")));
}
}
Loading