summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDavid Lönnhager <david.l@mullvad.net>2025-02-11 15:38:03 +0100
committerDavid Lönnhager <david.l@mullvad.net>2025-02-11 15:38:03 +0100
commit0cf84de8141d6791c0f826b3cf0aa9ac6fea4d55 (patch)
tree7c3d4320ff3709719068c0910b92620f99cd1a58
parente93aba7e9d8c7210aac9b6d09c0067823bffc7e9 (diff)
parentda5ff2744931b244352bf6de5953f2e1e62382e4 (diff)
downloadmullvadvpn-0cf84de8141d6791c0f826b3cf0aa9ac6fea4d55.tar.xz
mullvadvpn-0cf84de8141d6791c0f826b3cf0aa9ac6fea4d55.zip
Merge branch 'port-link-parser'
-rw-r--r--CHANGELOG.md9
-rw-r--r--Cargo.lock176
-rw-r--r--Cargo.toml1
-rw-r--r--desktop/package-lock.json21
-rw-r--r--desktop/packages/mullvad-vpn/gulpfile.js12
-rw-r--r--desktop/packages/mullvad-vpn/package.json1
-rw-r--r--desktop/packages/mullvad-vpn/src/main/index.ts7
-rw-r--r--desktop/packages/mullvad-vpn/src/main/windows-split-tunneling.ts47
-rw-r--r--desktop/packages/mullvad-vpn/tasks/distribution.js5
-rw-r--r--desktop/packages/mullvad-vpn/tasks/scripts.js6
-rw-r--r--desktop/packages/win-shortcuts/.gitignore9
-rw-r--r--desktop/packages/win-shortcuts/Cargo.toml21
-rw-r--r--desktop/packages/win-shortcuts/README.md19
-rw-r--r--desktop/packages/win-shortcuts/eslint.config.mjs3
-rw-r--r--desktop/packages/win-shortcuts/package.json36
-rw-r--r--desktop/packages/win-shortcuts/src/index.cts18
-rw-r--r--desktop/packages/win-shortcuts/src/index.mts3
-rw-r--r--desktop/packages/win-shortcuts/src/load.cts11
-rw-r--r--desktop/packages/win-shortcuts/tsconfig.json9
-rw-r--r--desktop/packages/win-shortcuts/win-shortcuts-rs/lib.rs191
20 files changed, 570 insertions, 35 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 868d7a072e..0a4b48b1a0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -27,6 +27,15 @@ Line wrap the file at 100 chars. Th
- Add support for DAITA V2.
- Add back wireguard-go (userspace WireGuard) support.
+### Changed
+#### Windows
+- Replace the Electron API `shell.readShortcutLink` with a custom, native rust module
+ `win-shortcuts`.
+
+### Fixed
+#### Windows
+- Fix GUI crashing at launch on some systems by replacing Electron's shortcut parser.
+
## [2025.3] - 2025-02-07
### Changed
diff --git a/Cargo.lock b/Cargo.lock
index a43fb10201..a237ae236a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -560,9 +560,9 @@ dependencies = [
[[package]]
name = "chrono"
-version = "0.4.38"
+version = "0.4.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
+checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825"
dependencies = [
"android-tzdata",
"iana-time-zone",
@@ -1398,9 +1398,9 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
[[package]]
name = "glob"
-version = "0.3.1"
+version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
+checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
[[package]]
name = "group"
@@ -4220,18 +4220,18 @@ dependencies = [
[[package]]
name = "serde"
-version = "1.0.204"
+version = "1.0.217"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12"
+checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
-version = "1.0.204"
+version = "1.0.217"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222"
+checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
dependencies = [
"proc-macro2",
"quote",
@@ -4240,9 +4240,9 @@ dependencies = [
[[package]]
name = "serde_json"
-version = "1.0.122"
+version = "1.0.138"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da"
+checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949"
dependencies = [
"itoa",
"memchr",
@@ -4676,7 +4676,7 @@ dependencies = [
"tun 0.5.5",
"which",
"widestring",
- "windows",
+ "windows 0.58.0",
"windows-core 0.58.0",
"windows-service",
"windows-sys 0.52.0",
@@ -5641,6 +5641,15 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5682,6 +5691,16 @@ dependencies = [
]
[[package]]
+name = "windows"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f919aee0a93304be7f62e8e5027811bbba96bcb1de84d6618be56e43f8a32a1"
+dependencies = [
+ "windows-core 0.59.0",
+ "windows-targets 0.53.0",
+]
+
+[[package]]
name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5696,14 +5715,27 @@ version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
dependencies = [
- "windows-implement",
- "windows-interface",
- "windows-result",
- "windows-strings",
+ "windows-implement 0.58.0",
+ "windows-interface 0.58.0",
+ "windows-result 0.2.0",
+ "windows-strings 0.1.0",
"windows-targets 0.52.6",
]
[[package]]
+name = "windows-core"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "810ce18ed2112484b0d4e15d022e5f598113e220c53e373fb31e67e21670c1ce"
+dependencies = [
+ "windows-implement 0.59.0",
+ "windows-interface 0.59.0",
+ "windows-result 0.3.0",
+ "windows-strings 0.3.0",
+ "windows-targets 0.53.0",
+]
+
+[[package]]
name = "windows-implement"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5715,6 +5747,17 @@ dependencies = [
]
[[package]]
+name = "windows-implement"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83577b051e2f49a058c308f17f273b570a6a758386fc291b5f6a934dd84e48c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.89",
+]
+
+[[package]]
name = "windows-installer"
version = "0.0.0"
dependencies = [
@@ -5737,13 +5780,24 @@ dependencies = [
]
[[package]]
+name = "windows-interface"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb26fd936d991781ea39e87c3a27285081e3c0da5ca0fcbc02d368cc6f52ff01"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.89",
+]
+
+[[package]]
name = "windows-registry"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0"
dependencies = [
- "windows-result",
- "windows-strings",
+ "windows-result 0.2.0",
+ "windows-strings 0.1.0",
"windows-targets 0.52.6",
]
@@ -5757,6 +5811,15 @@ dependencies = [
]
[[package]]
+name = "windows-result"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d08106ce80268c4067c0571ca55a9b4e9516518eaa1a1fe9b37ca403ae1d1a34"
+dependencies = [
+ "windows-targets 0.53.0",
+]
+
+[[package]]
name = "windows-service"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5773,11 +5836,20 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
dependencies = [
- "windows-result",
+ "windows-result 0.2.0",
"windows-targets 0.52.6",
]
[[package]]
+name = "windows-strings"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b888f919960b42ea4e11c2f408fadb55f78a9f236d5eef084103c8ce52893491"
+dependencies = [
+ "windows-targets 0.53.0",
+]
+
+[[package]]
name = "windows-sys"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5865,7 +5937,7 @@ dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
- "windows_i686_gnullvm",
+ "windows_i686_gnullvm 0.52.6",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
@@ -5873,6 +5945,22 @@ dependencies = [
]
[[package]]
+name = "windows-targets"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b"
+dependencies = [
+ "windows_aarch64_gnullvm 0.53.0",
+ "windows_aarch64_msvc 0.53.0",
+ "windows_i686_gnu 0.53.0",
+ "windows_i686_gnullvm 0.53.0",
+ "windows_i686_msvc 0.53.0",
+ "windows_x86_64_gnu 0.53.0",
+ "windows_x86_64_gnullvm 0.53.0",
+ "windows_x86_64_msvc 0.53.0",
+]
+
+[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5891,6 +5979,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
+
+[[package]]
name = "windows_aarch64_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5915,6 +6009,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
+
+[[package]]
name = "windows_i686_gnu"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5939,12 +6039,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
+name = "windows_i686_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
+
+[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
+
+[[package]]
name = "windows_i686_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5969,6 +6081,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
+name = "windows_i686_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
+
+[[package]]
name = "windows_x86_64_gnu"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5993,6 +6111,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
+
+[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -6011,6 +6135,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
+
+[[package]]
name = "windows_x86_64_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -6035,6 +6165,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
+
+[[package]]
name = "winnow"
version = "0.6.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -6121,7 +6257,7 @@ dependencies = [
"log",
"serde",
"thiserror 1.0.59",
- "windows",
+ "windows 0.58.0",
"windows-core 0.58.0",
]
diff --git a/Cargo.toml b/Cargo.toml
index 6b64342fc3..b1c5ff8aa1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,6 +11,7 @@ resolver = "2"
members = [
"android/translations-converter",
"desktop/packages/nseventforwarder",
+ "desktop/packages/win-shortcuts",
"mullvad-api",
"mullvad-cli",
"mullvad-daemon",
diff --git a/desktop/package-lock.json b/desktop/package-lock.json
index fc1c088646..de1e261588 100644
--- a/desktop/package-lock.json
+++ b/desktop/package-lock.json
@@ -14219,6 +14219,10 @@
"string-width": "^1.0.2 || 2"
}
},
+ "node_modules/win-shortcuts": {
+ "resolved": "packages/win-shortcuts",
+ "link": true
+ },
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
@@ -14669,7 +14673,8 @@
"redux": "^4.2.0",
"simple-plist": "^1.3.1",
"sprintf-js": "^1.1.2",
- "styled-components": "^6.1.13"
+ "styled-components": "^6.1.13",
+ "win-shortcuts": "0.0.0"
},
"devDependencies": {
"@playwright/test": "^1.41.1",
@@ -14720,6 +14725,13 @@
"dependencies": {
"@neon-rs/load": "^0.1.73"
}
+ },
+ "packages/win-shortcuts": {
+ "version": "0.0.0",
+ "license": "GPL-3.0",
+ "dependencies": {
+ "@neon-rs/load": "^0.1.73"
+ }
}
},
"dependencies": {
@@ -22607,6 +22619,7 @@
"tsc-watch": "^5.0.3",
"vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0",
+ "win-shortcuts": "0.0.0",
"xvfb-maybe": "^0.2.1"
}
},
@@ -25870,6 +25883,12 @@
"string-width": "^1.0.2 || 2"
}
},
+ "win-shortcuts": {
+ "version": "file:packages/win-shortcuts",
+ "requires": {
+ "@neon-rs/load": "^0.1.73"
+ }
+ },
"word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
diff --git a/desktop/packages/mullvad-vpn/gulpfile.js b/desktop/packages/mullvad-vpn/gulpfile.js
index 02375032f4..529b05f9c4 100644
--- a/desktop/packages/mullvad-vpn/gulpfile.js
+++ b/desktop/packages/mullvad-vpn/gulpfile.js
@@ -18,8 +18,18 @@ task('set-prod-env', function (done) {
task('clean', function (done) {
fs.rm('./build', { recursive: true, force: true }, done);
});
+task('build-proto', scripts.buildProto);
+task(
+ 'develop',
+ series(
+ 'clean',
+ 'set-dev-env',
+ scripts.buildNseventforwarder,
+ scripts.buildWinShortcuts,
+ watch.start,
+ ),
+);
task('build', series('clean', 'set-prod-env', assets.copyAll, scripts.build));
-task('develop', series('clean', 'set-dev-env', scripts.buildNseventforwarder, watch.start));
task('pack-win', series('build', dist.packWin));
task('pack-linux', series('build', dist.packLinux));
task('pack-mac', series('build', dist.packMac));
diff --git a/desktop/packages/mullvad-vpn/package.json b/desktop/packages/mullvad-vpn/package.json
index 056b0be93f..0963f79914 100644
--- a/desktop/packages/mullvad-vpn/package.json
+++ b/desktop/packages/mullvad-vpn/package.json
@@ -25,6 +25,7 @@
"react-redux": "^7.2.9",
"react-router": "^5.3.4",
"redux": "^4.2.0",
+ "win-shortcuts": "0.0.0",
"simple-plist": "^1.3.1",
"sprintf-js": "^1.1.2",
"styled-components": "^6.1.13"
diff --git a/desktop/packages/mullvad-vpn/src/main/index.ts b/desktop/packages/mullvad-vpn/src/main/index.ts
index 2186168379..a80241f527 100644
--- a/desktop/packages/mullvad-vpn/src/main/index.ts
+++ b/desktop/packages/mullvad-vpn/src/main/index.ts
@@ -829,7 +829,12 @@ class ApplicationMain
// If the applications is a string (path) it's an application picked with the file picker
// that we want to add to the list of additional applications.
if (typeof application === 'string') {
- const executablePath = await splitTunneling!.resolveExecutablePath(application);
+ let executablePath;
+ try {
+ executablePath = await splitTunneling!.resolveExecutablePath(application);
+ } catch {
+ return;
+ }
this.settings.gui.addBrowsedForSplitTunnelingApplications(executablePath);
await splitTunneling!.addApplicationPathToCache(application);
await this.daemonRpc.addSplitTunnelingApplication(executablePath);
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 7ae73827d0..4f1df5660a 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,10 @@
-import { app, shell } from 'electron';
+import { app } from 'electron';
import fs from 'fs';
import path from 'path';
+// eslint-disable-next-line @typescript-eslint/no-require-imports
+const { readShortcut } = require('win-shortcuts');
+
import {
ISplitTunnelingApplication,
ISplitTunnelingAppListRetriever,
@@ -114,7 +117,12 @@ export class WindowsSplitTunnelingAppListRetriever implements ISplitTunnelingApp
public resolveExecutablePath(providedPath: string): Promise<string> {
if (path.extname(providedPath) === '.lnk') {
- return Promise.resolve(shell.readShortcutLink(path.resolve(providedPath)).target);
+ const target = this.tryReadShortcut(path.resolve(providedPath));
+ if (target) {
+ return Promise.resolve(target);
+ } else {
+ return Promise.reject('Failed to resolve shortcut');
+ }
}
return Promise.resolve(providedPath);
@@ -124,12 +132,14 @@ export class WindowsSplitTunnelingAppListRetriever implements ISplitTunnelingApp
public async addApplicationPathToCache(applicationPath: string): Promise<void> {
const parsedPath = path.parse(applicationPath);
if (parsedPath.ext === '.lnk') {
- const shortcutDetiails = shell.readShortcutLink(path.resolve(applicationPath));
- this.additionalShortcuts.push({
- ...shortcutDetiails,
- name: path.parse(applicationPath).name,
- deletable: true,
- });
+ const target = this.tryReadShortcut(path.resolve(applicationPath));
+ if (target) {
+ this.additionalShortcuts.push({
+ target,
+ name: path.parse(applicationPath).name,
+ deletable: true,
+ });
+ }
} else {
await this.addApplicationToAdditionalShortcuts(applicationPath);
}
@@ -216,14 +226,14 @@ export class WindowsSplitTunnelingAppListRetriever implements ISplitTunnelingApp
private resolveLinks(linkPaths: string[]): ShortcutDetails[] {
return linkPaths
.map((link) => {
- try {
+ const target = this.tryReadShortcut(path.resolve(link));
+ if (target) {
return {
- ...shell.readShortcutLink(path.resolve(link)),
+ target,
name: path.parse(link).name,
};
- } catch {
- return null;
}
+ return null;
})
.filter(
(shortcut): shortcut is ShortcutDetails =>
@@ -677,4 +687,17 @@ export class WindowsSplitTunnelingAppListRetriever implements ISplitTunnelingApp
private alignDword(offset: number): number {
return Math.ceil(offset / 4) * 4;
}
+
+ private tryReadShortcut(appPath: string): string | null {
+ try {
+ return readShortcut(path.resolve(appPath));
+ } catch (e) {
+ if (e && typeof e === 'object' && 'message' in e) {
+ log.error(`Failed to read .lnk shortcut for ${appPath}.`, e.message);
+ } else {
+ log.error(`Failed to read .lnk shortcut for ${appPath}.`);
+ }
+ return null;
+ }
+ }
}
diff --git a/desktop/packages/mullvad-vpn/tasks/distribution.js b/desktop/packages/mullvad-vpn/tasks/distribution.js
index 8ab8a35882..9965db3ccb 100644
--- a/desktop/packages/mullvad-vpn/tasks/distribution.js
+++ b/desktop/packages/mullvad-vpn/tasks/distribution.js
@@ -62,6 +62,7 @@ function newConfig() {
'!node_modules/grpc-tools',
'!node_modules/@types',
'!node_modules/nseventforwarder/debug',
+ '!node_modules/win-shortcuts/debug',
],
// Make sure that all files declared in "extraResources" exists and abort if they don't.
@@ -300,12 +301,16 @@ async function packWin() {
process.env.SETUP_SUBDIR = '.';
process.env.TARGET_SUBDIR = 'x86_64-pc-windows-msvc';
process.env.DIST_SUBDIR = '';
+
+ execFileSync('npm', ['-w', 'win-shortcuts', 'run', 'build-x86']);
break;
case 'arm64':
process.env.TARGET_TRIPLE = 'aarch64-pc-windows-msvc';
process.env.SETUP_SUBDIR = 'aarch64-pc-windows-msvc';
process.env.TARGET_SUBDIR = 'aarch64-pc-windows-msvc';
process.env.DIST_SUBDIR = 'aarch64-pc-windows-msvc';
+
+ execFileSync('npm', ['-w', 'win-shortcuts', 'run', 'build-arm']);
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 066ad60c6b..baa89736d2 100644
--- a/desktop/packages/mullvad-vpn/tasks/scripts.js
+++ b/desktop/packages/mullvad-vpn/tasks/scripts.js
@@ -117,12 +117,18 @@ function buildNseventforwarder(callback) {
}
}
+function buildWinShortcuts(callback) {
+ exec('npm -w win-shortcuts run build-debug', (err) => callback(err));
+}
+
compileScripts.displayName = 'compile-scripts';
buildNseventforwarder.displayName = 'build-nseventforwarder';
+buildWinShortcuts.displayName = 'build-win-shortcuts';
exports.build = series(
compileScripts,
parallel(makeBrowserifyPreload(false), makeBrowserifyRenderer(false)),
);
exports.buildNseventforwarder = buildNseventforwarder;
+exports.buildWinShortcuts = buildWinShortcuts;
exports.makeWatchCompiler = makeWatchCompiler;
diff --git a/desktop/packages/win-shortcuts/.gitignore b/desktop/packages/win-shortcuts/.gitignore
new file mode 100644
index 0000000000..ec4fda9fef
--- /dev/null
+++ b/desktop/packages/win-shortcuts/.gitignore
@@ -0,0 +1,9 @@
+target
+index.node
+**/node_modules
+**/.DS_Store
+npm-debug.log*
+lib
+*.log
+dist/
+debug/
diff --git a/desktop/packages/win-shortcuts/Cargo.toml b/desktop/packages/win-shortcuts/Cargo.toml
new file mode 100644
index 0000000000..ed8f3f43af
--- /dev/null
+++ b/desktop/packages/win-shortcuts/Cargo.toml
@@ -0,0 +1,21 @@
+[package]
+name = "win-shortcuts"
+description = ""
+authors.workspace = true
+repository.workspace = true
+license.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+exclude = ["index.node"]
+
+[lints]
+workspace = true
+
+[lib]
+crate-type = ["cdylib"]
+path = "win-shortcuts-rs/lib.rs"
+
+[target.'cfg(target_os = "windows")'.dependencies]
+neon = "1"
+windows = { version = "0.59.0", features = ["Win32", "Win32_UI", "Win32_UI_Shell", "Win32_System", "Win32_System_Com", "Win32_Storage_FileSystem"] }
+thiserror = { workspace = true }
diff --git a/desktop/packages/win-shortcuts/README.md b/desktop/packages/win-shortcuts/README.md
new file mode 100644
index 0000000000..b837b8e6a6
--- /dev/null
+++ b/desktop/packages/win-shortcuts/README.md
@@ -0,0 +1,19 @@
+# win-shortcuts
+
+## Building win-shortcuts
+
+Building win-shortcuts requires a [supported version of Node and Rust](https://github.com/neon-bindings/neon#platform-support).
+
+To run the build, run:
+
+```sh
+$ npm run build-debug
+```
+
+## Learn More
+
+Learn more about:
+
+- [Neon](https://neon-bindings.com).
+- [Rust](https://www.rust-lang.org).
+- [Node](https://nodejs.org).
diff --git a/desktop/packages/win-shortcuts/eslint.config.mjs b/desktop/packages/win-shortcuts/eslint.config.mjs
new file mode 100644
index 0000000000..9e16ad57ea
--- /dev/null
+++ b/desktop/packages/win-shortcuts/eslint.config.mjs
@@ -0,0 +1,3 @@
+import workspaceConfig from '../../eslint.config.mjs';
+
+export default [...workspaceConfig, { ignores: ['lib/'] }];
diff --git a/desktop/packages/win-shortcuts/package.json b/desktop/packages/win-shortcuts/package.json
new file mode 100644
index 0000000000..245c9ef179
--- /dev/null
+++ b/desktop/packages/win-shortcuts/package.json
@@ -0,0 +1,36 @@
+{
+ "name": "win-shortcuts",
+ "version": "0.0.0",
+ "author": "Mullvad VPN",
+ "license": "GPL-3.0",
+ "description": "",
+ "main": "./lib/index.cjs",
+ "scripts": {
+ "cargo-build": "tsc && 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-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",
+ "clean": "rm -rf debug; rm -rf dist",
+ "lint": "eslint .",
+ "lint-fix": "eslint --fix ."
+ },
+ "exports": {
+ ".": {
+ "import": {
+ "types": "./lib/index.d.mts",
+ "default": "./lib/index.mjs"
+ },
+ "require": {
+ "types": "./lib/index.d.cts",
+ "default": "./lib/index.cjs"
+ }
+ }
+ },
+ "types": "./lib/index.d.cts",
+ "files": [
+ "lib/**/*.?({c,m}){t,j}s"
+ ],
+ "dependencies": {
+ "@neon-rs/load": "^0.1.73"
+ }
+}
diff --git a/desktop/packages/win-shortcuts/src/index.cts b/desktop/packages/win-shortcuts/src/index.cts
new file mode 100644
index 0000000000..717968cda4
--- /dev/null
+++ b/desktop/packages/win-shortcuts/src/index.cts
@@ -0,0 +1,18 @@
+// This module is the CJS entry point for the library.
+
+// The Rust addon.
+import * as addon from './load.cjs';
+
+// Use this declaration to assign types to the addon's exports,
+// which otherwise by default are `any`.
+declare module './load.cjs' {
+ function readShortcut(linkPath: string): string | null;
+}
+
+/**
+ * Return path for a shortcut.
+ * @param linkPath absolute path to a `.lnk`.
+ */
+export function readShortcut(linkPath: string): string | null {
+ return addon.readShortcut(linkPath);
+}
diff --git a/desktop/packages/win-shortcuts/src/index.mts b/desktop/packages/win-shortcuts/src/index.mts
new file mode 100644
index 0000000000..5e1ab260f6
--- /dev/null
+++ b/desktop/packages/win-shortcuts/src/index.mts
@@ -0,0 +1,3 @@
+// This module is the ESM entry point for the library.
+
+export * from './index.cjs';
diff --git a/desktop/packages/win-shortcuts/src/load.cts b/desktop/packages/win-shortcuts/src/load.cts
new file mode 100644
index 0000000000..e58b265901
--- /dev/null
+++ b/desktop/packages/win-shortcuts/src/load.cts
@@ -0,0 +1,11 @@
+// This module loads the platform-specific build of the addon on
+// the current system.
+
+/* eslint-disable @typescript-eslint/no-require-imports */
+module.exports = require('@neon-rs/load').proxy({
+ platforms: {
+ 'win32-x64-msvc': () => require('../dist/win32-x64-msvc'),
+ 'win32-arm64-msvc': () => require('../dist/win32-arm64-msvc'),
+ },
+ debug: () => require('../debug/index.node'),
+});
diff --git a/desktop/packages/win-shortcuts/tsconfig.json b/desktop/packages/win-shortcuts/tsconfig.json
new file mode 100644
index 0000000000..31e6603771
--- /dev/null
+++ b/desktop/packages/win-shortcuts/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "module": "node16",
+ "declaration": true,
+ "outDir": "lib"
+ },
+ "exclude": ["lib"]
+}
diff --git a/desktop/packages/win-shortcuts/win-shortcuts-rs/lib.rs b/desktop/packages/win-shortcuts/win-shortcuts-rs/lib.rs
new file mode 100644
index 0000000000..9cc0cea0c6
--- /dev/null
+++ b/desktop/packages/win-shortcuts/win-shortcuts-rs/lib.rs
@@ -0,0 +1,191 @@
+#![cfg(target_os = "windows")]
+
+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<Result<Option<String>, 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;
+
+#[neon::main]
+fn main(mut cx: ModuleContext<'_>) -> NeonResult<()> {
+ cx.export_function("readShortcut", read_shortcut)?;
+
+ Ok(())
+}
+
+fn read_shortcut(mut cx: FunctionContext<'_>) -> JsResult<'_, JsValue> {
+ let link_path = cx.argument::<JsString>(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<Option<String>, 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<Option<String>, Error> {
+ let shell_link_result: windows::core::Result<IShellLinkW> =
+ // 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<IPersistFile> = 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<Self, windows::core::Error> {
+ // 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<Message> {
+ static THREAD_SENDER: OnceLock<mpsc::Sender<Message>> = 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()
+}