diff options
| author | Oskar <oskar@mullvad.net> | 2025-02-10 09:28:35 +0100 |
|---|---|---|
| committer | David Lönnhager <david.l@mullvad.net> | 2025-02-11 15:37:22 +0100 |
| commit | e7e7252fc5e765bf971ff502a4f81b1cb20c92ad (patch) | |
| tree | b7381005a33b7a43214fce4572262629141a6521 | |
| parent | e93aba7e9d8c7210aac9b6d09c0067823bffc7e9 (diff) | |
| download | mullvadvpn-e7e7252fc5e765bf971ff502a4f81b1cb20c92ad.tar.xz mullvadvpn-e7e7252fc5e765bf971ff502a4f81b1cb20c92ad.zip | |
Implement shortcut parsing
Co-authored-by: David Lönnhager <david.l@mullvad.net>
Co-authored-by: Markus Pettersson <markus.pettersson@mullvad.net>
Co-authored-by: Joakim Hulthe <joakim.hulthe@mullvad.net>
19 files changed, 561 insertions, 35 deletions
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() +} |
