Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions apps/tauri/crates/file-opening-windows/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ windows = { version = "0.58", features = [
"Win32_UI_Shell",
"Win32_UI_Shell_Common",
"Win32_UI_WindowsAndMessaging",
"Win32_Storage_FileSystem",
] }
53 changes: 49 additions & 4 deletions apps/tauri/crates/file-opening-windows/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use file_opening::{FileOpener, OpenResult, OpenWithApp};
use std::path::Path;
use std::path::{Path, PathBuf};
use std::os::windows::ffi::OsStrExt;
use windows::core::*;
use windows::Win32::Foundation::*;
use windows::Win32::System::Com::*;
use windows::Win32::UI::Shell::*;
use windows::Win32::UI::WindowsAndMessaging::*;
Expand All @@ -28,7 +28,14 @@ impl FileOpener for WindowsFileOpener {
fn get_apps_for_file(&self, path: &Path) -> std::result::Result<Vec<OpenWithApp>, String> {
ensure_com_initialized();

let ext = path
let mut actual_path = path.to_path_buf();
if path.extension().and_then(|e| e.to_str()) == Some("lnk") {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Windows shortcuts can come through as .LNK too. Using a case-insensitive check avoids silently skipping resolution.

Suggested change
if path.extension().and_then(|e| e.to_str()) == Some("lnk") {
if matches!(path.extension().and_then(|e| e.to_str()), Some(ext) if ext.eq_ignore_ascii_case("lnk")) {

if let Some(target) = resolve_shortcut(path) {
actual_path = target;
}
}

let ext = actual_path
.extension()
.and_then(|e| e.to_str())
.map(|e| format!(".{}", e))
Expand All @@ -44,7 +51,14 @@ impl FileOpener for WindowsFileOpener {
fn open_with_default(&self, path: &Path) -> std::result::Result<OpenResult, String> {
ensure_com_initialized();

let path_str = path.to_string_lossy();
let mut actual_path = path.to_path_buf();
if path.extension().and_then(|e| e.to_str()) == Some("lnk") {
if let Some(target) = resolve_shortcut(path) {
actual_path = target;
}
}

let path_str = actual_path.to_string_lossy();
let h_path = HSTRING::from(&*path_str);

unsafe {
Expand Down Expand Up @@ -162,3 +176,34 @@ fn list_apps_for_extension(ext: &str) -> std::result::Result<Vec<OpenWithApp>, S
Ok(apps)
}
}

fn resolve_shortcut(path: &Path) -> Option<PathBuf> {
unsafe {
let shell_link: IShellLinkW =
CoCreateInstance(&ShellLink, None, CLSCTX_INPROC_SERVER).ok()?;
let persist_file: IPersistFile = shell_link.cast().ok()?;

let path_vec: Vec<u16> = path
.as_os_str()
.encode_wide()
.chain(std::iter::once(0))
.collect();

persist_file
.Load(PCWSTR(path_vec.as_ptr()), STGM_READ)
.ok()?;

let mut buffer = [0u16; 260];
shell_link
.GetPath(&mut buffer, std::ptr::null_mut(), 0)
Comment on lines +197 to +198

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This buffer is NUL-terminated; slicing to the first 0 avoids trim_matches scanning the whole string and is a bit clearer.

Suggested change
shell_link
.GetPath(&mut buffer, std::ptr::null_mut(), 0)
let len = buffer.iter().position(|&c| c == 0).unwrap_or(buffer.len());
let target = String::from_utf16_lossy(&buffer[..len]);

.ok()?;

let target = String::from_utf16_lossy(&buffer);
let target = target.trim_matches('\0');
if target.is_empty() {
None
} else {
Some(PathBuf::from(target))
}
}
}