From ef2983b244f3f662fe6eb2a3d6905962781b430e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20L=C3=B6nnhager?= Date: Wed, 19 Mar 2025 10:42:27 +0100 Subject: [PATCH 1/2] Rename win-shortcuts to windows-utils --- CHANGELOG.md | 4 ++++ Cargo.lock | 18 +++++++++--------- Cargo.toml | 2 +- desktop/package-lock.json | 14 +++++++------- desktop/packages/mullvad-vpn/gulpfile.js | 2 +- desktop/packages/mullvad-vpn/package.json | 2 +- .../src/main/windows-split-tunneling.ts | 2 +- .../packages/mullvad-vpn/tasks/distribution.js | 6 +++--- desktop/packages/mullvad-vpn/tasks/scripts.js | 8 ++++---- desktop/packages/mullvad-vpn/vite.config.ts | 4 ++-- .../.gitignore | 0 .../Cargo.toml | 4 ++-- .../{win-shortcuts => windows-utils}/README.md | 6 +++--- .../eslint.config.mjs | 0 .../package.json | 8 ++++---- .../src/index.cts | 0 .../src/index.mts | 0 .../src/load.cts | 0 .../tsconfig.json | 0 .../windows-utils-rs}/lib.rs | 0 20 files changed, 42 insertions(+), 38 deletions(-) rename desktop/packages/{win-shortcuts => windows-utils}/.gitignore (100%) rename desktop/packages/{win-shortcuts => windows-utils}/Cargo.toml (89%) rename desktop/packages/{win-shortcuts => windows-utils}/README.md (71%) rename desktop/packages/{win-shortcuts => windows-utils}/eslint.config.mjs (100%) rename desktop/packages/{win-shortcuts => windows-utils}/package.json (85%) rename desktop/packages/{win-shortcuts => windows-utils}/src/index.cts (100%) rename desktop/packages/{win-shortcuts => windows-utils}/src/index.mts (100%) rename desktop/packages/{win-shortcuts => windows-utils}/src/load.cts (100%) rename desktop/packages/{win-shortcuts => windows-utils}/tsconfig.json (100%) rename desktop/packages/{win-shortcuts/win-shortcuts-rs => windows-utils/windows-utils-rs}/lib.rs (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72e2327d5088..12a00f72aa11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,10 @@ Line wrap the file at 100 chars. Th * **Security**: in case of vulnerabilities. ## [Unreleased] +### Changed +#### Windows +- Rename `win-shortcuts` native module to `windows-utils`. + ### Removed - Remove "Any" option for tunnel protocol. The default is now WireGuard. diff --git a/Cargo.lock b/Cargo.lock index f6b3c2c3350d..5d4adf228182 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5812,15 +5812,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" -[[package]] -name = "win-shortcuts" -version = "0.0.0" -dependencies = [ - "neon", - "thiserror 2.0.9", - "windows 0.59.0", -] - [[package]] name = "winapi" version = "0.3.9" @@ -6139,6 +6130,15 @@ dependencies = [ "windows_x86_64_msvc 0.53.0", ] +[[package]] +name = "windows-utils" +version = "0.0.0" +dependencies = [ + "neon", + "thiserror 2.0.9", + "windows 0.59.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" diff --git a/Cargo.toml b/Cargo.toml index 4a10fcdb23c9..a52f66781e7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ resolver = "2" members = [ "android/translations-converter", "desktop/packages/nseventforwarder", - "desktop/packages/win-shortcuts", + "desktop/packages/windows-utils", "mullvad-api", "mullvad-cli", "mullvad-daemon", diff --git a/desktop/package-lock.json b/desktop/package-lock.json index db77855a4020..a0cd55a3205e 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -15246,8 +15246,8 @@ "string-width": "^1.0.2 || 2" } }, - "node_modules/win-shortcuts": { - "resolved": "packages/win-shortcuts", + "node_modules/windows-utils": { + "resolved": "packages/windows-utils", "link": true }, "node_modules/word-wrap": { @@ -15701,7 +15701,7 @@ "simple-plist": "^1.3.1", "sprintf-js": "^1.1.2", "styled-components": "^6.1.13", - "win-shortcuts": "0.0.0" + "windows-utils": "0.0.0" }, "devDependencies": { "@playwright/test": "^1.41.1", @@ -15765,7 +15765,7 @@ "@neon-rs/load": "^0.1.73" } }, - "packages/win-shortcuts": { + "packages/windows-utils": { "version": "0.0.0", "hasInstallScript": true, "license": "GPL-3.0", @@ -23989,7 +23989,7 @@ "vinyl-source-stream": "^2.0.0", "vite": "6.2.0", "vite-plugin-electron": "^0.29.0", - "win-shortcuts": "0.0.0", + "windows-utils": "0.0.0", "xvfb-maybe": "^0.2.1" } }, @@ -27378,8 +27378,8 @@ "string-width": "^1.0.2 || 2" } }, - "win-shortcuts": { - "version": "file:packages/win-shortcuts", + "windows-utils": { + "version": "file:packages/windows-utils", "requires": { "@neon-rs/load": "^0.1.73" } diff --git a/desktop/packages/mullvad-vpn/gulpfile.js b/desktop/packages/mullvad-vpn/gulpfile.js index 917878b9770c..90ebb8df130b 100644 --- a/desktop/packages/mullvad-vpn/gulpfile.js +++ b/desktop/packages/mullvad-vpn/gulpfile.js @@ -41,7 +41,7 @@ task( 'clean', 'set-dev-env', scripts.buildNseventforwarder, - scripts.buildWinShortcuts, + scripts.buildWindowsUtils, watch.start, ), ); diff --git a/desktop/packages/mullvad-vpn/package.json b/desktop/packages/mullvad-vpn/package.json index 57e415c9daed..0cd951ed030b 100644 --- a/desktop/packages/mullvad-vpn/package.json +++ b/desktop/packages/mullvad-vpn/package.json @@ -28,7 +28,7 @@ "simple-plist": "^1.3.1", "sprintf-js": "^1.1.2", "styled-components": "^6.1.13", - "win-shortcuts": "0.0.0" + "windows-utils": "0.0.0" }, "devDependencies": { "@playwright/test": "^1.41.1", diff --git a/desktop/packages/mullvad-vpn/src/main/windows-split-tunneling.ts b/desktop/packages/mullvad-vpn/src/main/windows-split-tunneling.ts index f8052c894784..f2928bd433eb 100644 --- a/desktop/packages/mullvad-vpn/src/main/windows-split-tunneling.ts +++ b/desktop/packages/mullvad-vpn/src/main/windows-split-tunneling.ts @@ -1,7 +1,7 @@ import { app } from 'electron'; import fs from 'fs'; import path from 'path'; -import { readShortcut } from 'win-shortcuts'; +import { readShortcut } from 'windows-utils'; import { ISplitTunnelingApplication, diff --git a/desktop/packages/mullvad-vpn/tasks/distribution.js b/desktop/packages/mullvad-vpn/tasks/distribution.js index 52a053d541ed..88fcfa65d675 100644 --- a/desktop/packages/mullvad-vpn/tasks/distribution.js +++ b/desktop/packages/mullvad-vpn/tasks/distribution.js @@ -57,7 +57,7 @@ function newConfig() { '!node_modules/grpc-tools', '!node_modules/@types', '!node_modules/nseventforwarder/debug', - '!node_modules/win-shortcuts/debug', + '!node_modules/windows-utils/debug', ], // Make sure that all files declared in "extraResources" exists and abort if they don't. @@ -291,7 +291,7 @@ async function packWin() { process.env.TARGET_SUBDIR = 'x86_64-pc-windows-msvc'; process.env.DIST_SUBDIR = ''; - execFileSync('npm', ['-w', 'win-shortcuts', 'run', 'build-x86'], { shell: true }); + execFileSync('npm', ['-w', 'windows-utils', 'run', 'build-x86'], { shell: true }); break; case 'arm64': process.env.TARGET_TRIPLE = 'aarch64-pc-windows-msvc'; @@ -299,7 +299,7 @@ async function packWin() { process.env.TARGET_SUBDIR = 'aarch64-pc-windows-msvc'; process.env.DIST_SUBDIR = 'aarch64-pc-windows-msvc'; - execFileSync('npm', ['-w', 'win-shortcuts', 'run', 'build-arm'], { shell: true }); + execFileSync('npm', ['-w', 'windows-utils', 'run', 'build-arm'], { shell: true }); break; default: throw new Error('Invalid or unknown target (only one may be specified)'); diff --git a/desktop/packages/mullvad-vpn/tasks/scripts.js b/desktop/packages/mullvad-vpn/tasks/scripts.js index fb1372762b60..f3a395a9bef0 100644 --- a/desktop/packages/mullvad-vpn/tasks/scripts.js +++ b/desktop/packages/mullvad-vpn/tasks/scripts.js @@ -117,9 +117,9 @@ function buildNseventforwarder(callback) { } } -function buildWinShortcuts(callback) { +function buildWindowsUtils(callback) { if (process.platform === 'win32') { - exec('npm -w win-shortcuts run build-debug', (err) => callback(err)); + exec('npm -w windows-utils run build-debug', (err) => callback(err)); } else { callback(); } @@ -127,12 +127,12 @@ function buildWinShortcuts(callback) { compileScripts.displayName = 'compile-scripts'; buildNseventforwarder.displayName = 'build-nseventforwarder'; -buildWinShortcuts.displayName = 'build-win-shortcuts'; +buildWindowsUtils.displayName = 'build-windows-utils'; exports.build = series( compileScripts, parallel(makeBrowserifyPreload(false), makeBrowserifyRenderer(false)), ); exports.buildNseventforwarder = buildNseventforwarder; -exports.buildWinShortcuts = buildWinShortcuts; +exports.buildWindowsUtils = buildWindowsUtils; exports.makeWatchCompiler = makeWatchCompiler; diff --git a/desktop/packages/mullvad-vpn/vite.config.ts b/desktop/packages/mullvad-vpn/vite.config.ts index 657111cf9a84..bfcf6cf9625e 100644 --- a/desktop/packages/mullvad-vpn/vite.config.ts +++ b/desktop/packages/mullvad-vpn/vite.config.ts @@ -74,7 +74,7 @@ const viteConfig = defineConfig({ // Packages in workspace which exports common js /management-interface/, /nseventforwarder/, - /win-shortcuts/, + /windows-utils/, // External dependencies which exports common js /node_modules/, ], @@ -87,7 +87,7 @@ const viteConfig = defineConfig({ }, external: [ // Packages in workspace which can not be bundled - 'win-shortcuts', + 'windows-utils', // External dependencies '@grpc/grpc-js', 'google-protobuf', diff --git a/desktop/packages/win-shortcuts/.gitignore b/desktop/packages/windows-utils/.gitignore similarity index 100% rename from desktop/packages/win-shortcuts/.gitignore rename to desktop/packages/windows-utils/.gitignore diff --git a/desktop/packages/win-shortcuts/Cargo.toml b/desktop/packages/windows-utils/Cargo.toml similarity index 89% rename from desktop/packages/win-shortcuts/Cargo.toml rename to desktop/packages/windows-utils/Cargo.toml index ed8f3f43afdf..377e17e208a5 100644 --- a/desktop/packages/win-shortcuts/Cargo.toml +++ b/desktop/packages/windows-utils/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "win-shortcuts" +name = "windows-utils" description = "" authors.workspace = true repository.workspace = true @@ -13,7 +13,7 @@ workspace = true [lib] crate-type = ["cdylib"] -path = "win-shortcuts-rs/lib.rs" +path = "windows-utils-rs/lib.rs" [target.'cfg(target_os = "windows")'.dependencies] neon = "1" diff --git a/desktop/packages/win-shortcuts/README.md b/desktop/packages/windows-utils/README.md similarity index 71% rename from desktop/packages/win-shortcuts/README.md rename to desktop/packages/windows-utils/README.md index b837b8e6a68c..6f15dedae7a6 100644 --- a/desktop/packages/win-shortcuts/README.md +++ b/desktop/packages/windows-utils/README.md @@ -1,8 +1,8 @@ -# win-shortcuts +# windows-utils -## Building win-shortcuts +## Building windows-utils -Building win-shortcuts requires a [supported version of Node and Rust](https://github.com/neon-bindings/neon#platform-support). +Building windows-utils requires a [supported version of Node and Rust](https://github.com/neon-bindings/neon#platform-support). To run the build, run: diff --git a/desktop/packages/win-shortcuts/eslint.config.mjs b/desktop/packages/windows-utils/eslint.config.mjs similarity index 100% rename from desktop/packages/win-shortcuts/eslint.config.mjs rename to desktop/packages/windows-utils/eslint.config.mjs diff --git a/desktop/packages/win-shortcuts/package.json b/desktop/packages/windows-utils/package.json similarity index 85% rename from desktop/packages/win-shortcuts/package.json rename to desktop/packages/windows-utils/package.json index d549adcc8988..e61266b999bf 100644 --- a/desktop/packages/win-shortcuts/package.json +++ b/desktop/packages/windows-utils/package.json @@ -1,5 +1,5 @@ { - "name": "win-shortcuts", + "name": "windows-utils", "version": "0.0.0", "author": "Mullvad VPN", "license": "GPL-3.0", @@ -7,10 +7,10 @@ "main": "./lib/index.cjs", "scripts": { "cargo-build": "npm run build-typescript && cargo build", - "build-debug": "npm run cargo-build && (test -d debug || mkdir debug) && cp ../../../target/debug/win_shortcuts.dll debug/index.node", - "build-arm": "npm run cargo-build -- --release --target aarch64-pc-windows-msvc && (test -d dist || mkdir dist) && (test -d dist/win32-arm64-msvc || mkdir \"dist/win32-arm64-msvc\") && cp ../../../target/aarch64-pc-windows-msvc/release/win_shortcuts.dll dist/win32-arm64-msvc/index.node", + "build-debug": "npm run cargo-build && (test -d debug || mkdir debug) && cp ../../../target/debug/windows_utils.dll debug/index.node", + "build-arm": "npm run cargo-build -- --release --target aarch64-pc-windows-msvc && (test -d dist || mkdir dist) && (test -d dist/win32-arm64-msvc || mkdir \"dist/win32-arm64-msvc\") && cp ../../../target/aarch64-pc-windows-msvc/release/windows_utils.dll dist/win32-arm64-msvc/index.node", "build-typescript": "tsc", - "build-x86": "npm run cargo-build -- --release --target x86_64-pc-windows-msvc && (test -d dist || mkdir dist) && (test -d dist/win32-x64-msvc || mkdir \"dist/win32-x64-msvc\") && cp ../../../target/x86_64-pc-windows-msvc/release/win_shortcuts.dll dist/win32-x64-msvc/index.node", + "build-x86": "npm run cargo-build -- --release --target x86_64-pc-windows-msvc && (test -d dist || mkdir dist) && (test -d dist/win32-x64-msvc || mkdir \"dist/win32-x64-msvc\") && cp ../../../target/x86_64-pc-windows-msvc/release/windows_utils.dll dist/win32-x64-msvc/index.node", "clean": "rm -rf debug; rm -rf dist", "lint": "eslint .", "lint-fix": "eslint --fix .", diff --git a/desktop/packages/win-shortcuts/src/index.cts b/desktop/packages/windows-utils/src/index.cts similarity index 100% rename from desktop/packages/win-shortcuts/src/index.cts rename to desktop/packages/windows-utils/src/index.cts diff --git a/desktop/packages/win-shortcuts/src/index.mts b/desktop/packages/windows-utils/src/index.mts similarity index 100% rename from desktop/packages/win-shortcuts/src/index.mts rename to desktop/packages/windows-utils/src/index.mts diff --git a/desktop/packages/win-shortcuts/src/load.cts b/desktop/packages/windows-utils/src/load.cts similarity index 100% rename from desktop/packages/win-shortcuts/src/load.cts rename to desktop/packages/windows-utils/src/load.cts diff --git a/desktop/packages/win-shortcuts/tsconfig.json b/desktop/packages/windows-utils/tsconfig.json similarity index 100% rename from desktop/packages/win-shortcuts/tsconfig.json rename to desktop/packages/windows-utils/tsconfig.json diff --git a/desktop/packages/win-shortcuts/win-shortcuts-rs/lib.rs b/desktop/packages/windows-utils/windows-utils-rs/lib.rs similarity index 100% rename from desktop/packages/win-shortcuts/win-shortcuts-rs/lib.rs rename to desktop/packages/windows-utils/windows-utils-rs/lib.rs From 53ad0f1b82f0528649f544c243ff017b888f14ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20L=C3=B6nnhager?= Date: Wed, 19 Mar 2025 15:10:43 +0100 Subject: [PATCH 2/2] Move shortcut code to own module in windows-utils --- .../windows-utils/windows-utils-rs/lib.rs | 185 +----------------- .../windows-utils-rs/shortcut.rs | 182 +++++++++++++++++ 2 files changed, 185 insertions(+), 182 deletions(-) create mode 100644 desktop/packages/windows-utils/windows-utils-rs/shortcut.rs diff --git a/desktop/packages/windows-utils/windows-utils-rs/lib.rs b/desktop/packages/windows-utils/windows-utils-rs/lib.rs index 9cc0cea0c670..bdb0c90559c2 100644 --- a/desktop/packages/windows-utils/windows-utils-rs/lib.rs +++ b/desktop/packages/windows-utils/windows-utils-rs/lib.rs @@ -1,191 +1,12 @@ #![cfg(target_os = "windows")] -use std::marker::PhantomData; -use std::string::FromUtf16Error; -use std::sync::{mpsc, OnceLock}; +use neon::{prelude::ModuleContext, result::NeonResult}; -use neon::prelude::*; -use windows::core::{Interface, HSTRING, PCWSTR}; -use windows::Win32::System::Com::{ - CoCreateInstance, CoInitializeEx, CoUninitialize, IPersistFile, CLSCTX_INPROC_SERVER, - COINIT_APARTMENTTHREADED, STGM_READ, -}; -use windows::Win32::UI::Shell::{IShellLinkW, ShellLink, SLGP_UNCPRIORITY}; - -/// Messages that can be sent to the thread -enum Message { - ResolveShortcut { - path: String, - result_tx: mpsc::Sender, Error>>, - }, -} - -#[derive(thiserror::Error, Debug)] -enum Error { - /// The handler thread is down - #[error("The handler thread is down")] - ThreadDown, - - /// CoCreateInstance failed to create an IShellLinkW instance - #[error("CoCreateInstance failed to create an IShellLinkW instance")] - CreateInstance(#[source] windows::core::Error), - - /// Failed to cast shortcut to IPersistFile - #[error("Failed to cast IShellLinkW")] - CastShortcut(#[source] windows::core::Error), - - /// Failed to load shortcut - #[error("Failed to load shortcut .lnk")] - LoadShortcut(#[source] windows::core::Error), - - /// Failed to retrieve IShellLinkW path - #[error("Failed to retrieve IShellLinkW link")] - GetPath(#[source] windows::core::Error), - - /// Path is not valid UTF-16 - #[error("Path is not valid UTF-16")] - Utf16ToString(#[source] FromUtf16Error), -} - -/// Maximum path length of shortcut -/// 32 KiB: https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=registry -const MAX_PATH_LEN: usize = 0x7fff; +mod shortcut; #[neon::main] fn main(mut cx: ModuleContext<'_>) -> NeonResult<()> { - cx.export_function("readShortcut", read_shortcut)?; + cx.export_function("readShortcut", shortcut::read_shortcut)?; Ok(()) } - -fn read_shortcut(mut cx: FunctionContext<'_>) -> JsResult<'_, JsValue> { - let link_path = cx.argument::(0)?.value(&mut cx); - - match read_shortcut_inner(link_path) { - Ok(Some(path)) => Ok(cx.string(path).as_value(&mut cx)), - Ok(None) => Ok(cx.null().as_value(&mut cx)), - Err(err) => cx.throw_error(format!("Failed to read shortcut: {err}")), - } -} - -fn read_shortcut_inner(link_path: String) -> Result, Error> { - let tx = get_com_thread(); - - let (result_tx, result_rx) = mpsc::channel(); - tx.send(Message::ResolveShortcut { - path: link_path, - result_tx, - }) - .map_err(|_err| Error::ThreadDown)?; - - result_rx.recv().map_err(|_err| Error::ThreadDown)? -} - -/// Retrieve shortcut .lnk to its target path -fn get_shortcut_path(path: &str) -> Result, Error> { - let shell_link_result: windows::core::Result = - // SAFETY: We're passing a valid GUID pointer. - unsafe { CoCreateInstance(&ShellLink, None, CLSCTX_INPROC_SERVER) }; - let shell_link = shell_link_result.map_err(Error::CreateInstance)?; - - // Load the .lnk using IPersistFile - let path = HSTRING::from(path); - let persist_file_result: windows::core::Result = shell_link.cast(); - let persist_file = persist_file_result.map_err(Error::CastShortcut)?; - - // SAFETY: HSTRING::from will ensure that path is a valid utf16 null-terminated string. - unsafe { persist_file.Load(PCWSTR(path.as_ptr()), STGM_READ) }.map_err(Error::LoadShortcut)?; - - let mut target_buffer = [0u16; MAX_PATH_LEN]; - - // SAFETY: This function is trivially safe to call. - unsafe { - shell_link.GetPath( - &mut target_buffer, - std::ptr::null_mut(), - SLGP_UNCPRIORITY.0 as u32, - ) - } - .map_err(Error::GetPath)?; - - let utf16_slice = split_at_null(&target_buffer); - let s = String::from_utf16(utf16_slice).map_err(Error::Utf16ToString)?; - Ok(Some(s)) -} - -fn split_at_null(slice: &[u16]) -> &[u16] { - slice.split(|&c| c == 0).next().unwrap_or(slice) -} - -/// Struct for safely handling initialization and deinitialization of the Windows COM library. -/// A successful call to [CoInitializeEx] _needs_ to be accompanied by a call to [CoUninitialize], -/// which is taken care by the drop implementation on [ComContext]. -/// -/// [CoInitializeEx] sets up thread-local state. Thus this type is `!Send` to stop it being moved -/// to another thread. -struct ComContext { - // HACK: until negative impls are stable, this how we stop `Send` from being impld - _do_not_impl_send: PhantomData<*mut ()>, -} - -impl ComContext { - /// Create a new [ComContext]. - /// - /// This will call [CoInitializeEx] now, and [CoUninitialize] when dropped. - /// - /// May return an error if [CoInitializeEx] was previously called with different arguments on - /// the same thread. - fn new() -> Result { - // SAFETY: This is paired with CoUninitialize in impl Drop - unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) }.ok()?; - - Ok(Self { - _do_not_impl_send: PhantomData, - }) - } -} - -impl Drop for ComContext { - fn drop(&mut self) { - // SAFETY: CoInitializeEx was called when this struct was created, - // and it was called on the same thread since ComContext is !Send. - unsafe { - CoUninitialize(); - } - } -} - -/// Retrieve a channel for communicating with the thread responsible for handling -/// COM library safely. -/// We spawn a thread in case the caller may have already initialized COM in an -/// incompatible way. -fn get_com_thread() -> mpsc::Sender { - static THREAD_SENDER: OnceLock> = OnceLock::new(); - THREAD_SENDER - .get_or_init(move || { - let (tx, rx) = mpsc::channel(); - - std::thread::spawn(move || { - let com = match ComContext::new() { - Ok(com) => com, - Err(e) => { - eprintln!("Failed to initialize ComContext: {e}"); - return; - } - }; - - while let Ok(msg) = rx.recv() { - match msg { - Message::ResolveShortcut { path, result_tx } => { - let _ = result_tx.send(get_shortcut_path(&path)); - } - } - } - - drop(com); - }); - - tx - }) - .clone() -} diff --git a/desktop/packages/windows-utils/windows-utils-rs/shortcut.rs b/desktop/packages/windows-utils/windows-utils-rs/shortcut.rs new file mode 100644 index 000000000000..3d912711e199 --- /dev/null +++ b/desktop/packages/windows-utils/windows-utils-rs/shortcut.rs @@ -0,0 +1,182 @@ +use std::marker::PhantomData; +use std::string::FromUtf16Error; +use std::sync::{mpsc, OnceLock}; + +use neon::prelude::*; +use windows::core::{Interface, HSTRING, PCWSTR}; +use windows::Win32::System::Com::{ + CoCreateInstance, CoInitializeEx, CoUninitialize, IPersistFile, CLSCTX_INPROC_SERVER, + COINIT_APARTMENTTHREADED, STGM_READ, +}; +use windows::Win32::UI::Shell::{IShellLinkW, ShellLink, SLGP_UNCPRIORITY}; + +/// Messages that can be sent to the thread +enum Message { + ResolveShortcut { + path: String, + result_tx: mpsc::Sender, Error>>, + }, +} + +#[derive(thiserror::Error, Debug)] +enum Error { + /// The handler thread is down + #[error("The handler thread is down")] + ThreadDown, + + /// CoCreateInstance failed to create an IShellLinkW instance + #[error("CoCreateInstance failed to create an IShellLinkW instance")] + CreateInstance(#[source] windows::core::Error), + + /// Failed to cast shortcut to IPersistFile + #[error("Failed to cast IShellLinkW")] + CastShortcut(#[source] windows::core::Error), + + /// Failed to load shortcut + #[error("Failed to load shortcut .lnk")] + LoadShortcut(#[source] windows::core::Error), + + /// Failed to retrieve IShellLinkW path + #[error("Failed to retrieve IShellLinkW link")] + GetPath(#[source] windows::core::Error), + + /// Path is not valid UTF-16 + #[error("Path is not valid UTF-16")] + Utf16ToString(#[source] FromUtf16Error), +} + +/// Maximum path length of shortcut +/// 32 KiB: https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=registry +const MAX_PATH_LEN: usize = 0x7fff; + +pub fn read_shortcut(mut cx: FunctionContext<'_>) -> JsResult<'_, JsValue> { + let link_path = cx.argument::(0)?.value(&mut cx); + + match read_shortcut_inner(link_path) { + Ok(Some(path)) => Ok(cx.string(path).as_value(&mut cx)), + Ok(None) => Ok(cx.null().as_value(&mut cx)), + Err(err) => cx.throw_error(format!("Failed to read shortcut: {err}")), + } +} + +fn read_shortcut_inner(link_path: String) -> Result, Error> { + let tx = get_com_thread(); + + let (result_tx, result_rx) = mpsc::channel(); + tx.send(Message::ResolveShortcut { + path: link_path, + result_tx, + }) + .map_err(|_err| Error::ThreadDown)?; + + result_rx.recv().map_err(|_err| Error::ThreadDown)? +} + +/// Retrieve shortcut .lnk to its target path +fn get_shortcut_path(path: &str) -> Result, Error> { + let shell_link_result: windows::core::Result = + // SAFETY: We're passing a valid GUID pointer. + unsafe { CoCreateInstance(&ShellLink, None, CLSCTX_INPROC_SERVER) }; + let shell_link = shell_link_result.map_err(Error::CreateInstance)?; + + // Load the .lnk using IPersistFile + let path = HSTRING::from(path); + let persist_file_result: windows::core::Result = shell_link.cast(); + let persist_file = persist_file_result.map_err(Error::CastShortcut)?; + + // SAFETY: HSTRING::from will ensure that path is a valid utf16 null-terminated string. + unsafe { persist_file.Load(PCWSTR(path.as_ptr()), STGM_READ) }.map_err(Error::LoadShortcut)?; + + let mut target_buffer = [0u16; MAX_PATH_LEN]; + + // SAFETY: This function is trivially safe to call. + unsafe { + shell_link.GetPath( + &mut target_buffer, + std::ptr::null_mut(), + SLGP_UNCPRIORITY.0 as u32, + ) + } + .map_err(Error::GetPath)?; + + let utf16_slice = split_at_null(&target_buffer); + let s = String::from_utf16(utf16_slice).map_err(Error::Utf16ToString)?; + Ok(Some(s)) +} + +fn split_at_null(slice: &[u16]) -> &[u16] { + slice.split(|&c| c == 0).next().unwrap_or(slice) +} + +/// Struct for safely handling initialization and deinitialization of the Windows COM library. +/// A successful call to [CoInitializeEx] _needs_ to be accompanied by a call to [CoUninitialize], +/// which is taken care by the drop implementation on [ComContext]. +/// +/// [CoInitializeEx] sets up thread-local state. Thus this type is `!Send` to stop it being moved +/// to another thread. +struct ComContext { + // HACK: until negative impls are stable, this how we stop `Send` from being impld + _do_not_impl_send: PhantomData<*mut ()>, +} + +impl ComContext { + /// Create a new [ComContext]. + /// + /// This will call [CoInitializeEx] now, and [CoUninitialize] when dropped. + /// + /// May return an error if [CoInitializeEx] was previously called with different arguments on + /// the same thread. + fn new() -> Result { + // SAFETY: This is paired with CoUninitialize in impl Drop + unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) }.ok()?; + + Ok(Self { + _do_not_impl_send: PhantomData, + }) + } +} + +impl Drop for ComContext { + fn drop(&mut self) { + // SAFETY: CoInitializeEx was called when this struct was created, + // and it was called on the same thread since ComContext is !Send. + unsafe { + CoUninitialize(); + } + } +} + +/// Retrieve a channel for communicating with the thread responsible for handling +/// COM library safely. +/// We spawn a thread in case the caller may have already initialized COM in an +/// incompatible way. +fn get_com_thread() -> mpsc::Sender { + static THREAD_SENDER: OnceLock> = OnceLock::new(); + THREAD_SENDER + .get_or_init(move || { + let (tx, rx) = mpsc::channel(); + + std::thread::spawn(move || { + let com = match ComContext::new() { + Ok(com) => com, + Err(e) => { + eprintln!("Failed to initialize ComContext: {e}"); + return; + } + }; + + while let Ok(msg) = rx.recv() { + match msg { + Message::ResolveShortcut { path, result_tx } => { + let _ = result_tx.send(get_shortcut_path(&path)); + } + } + } + + drop(com); + }); + + tx + }) + .clone() +}