nushell/wix/main.wxs
Rong "Mantle" Bao c7e10c3c57
Correctly quote nu.exe and nu.ico path containing spaces in WiX (#15881)
# Description

This PR improves the installation process of Nushell's Windows Terminal
Profile by adding proper quoting when refilling the path to `nu.exe` and
`nu.ico`.

**Crossref:**
https://github.com/microsoft/terminal/issues/6082#issuecomment-1001226003

**Affected lines:**


222c307648/wix/main.wxs (L278-L282)

Currently, when any part of the installation path of `nu.exe` contains
spaces, the auto-generated profile would contain a truncated path due to
improper quoting. At best, this would cause failures when launching the
profile. At worst, this could lead to executable hijacks.

Assume this default-generated profile with the username "Mantle Bao":

```json
{
  "profiles": [
    {
      "guid": "{47302f9c-1ac4-566c-aa3e-8cf29889d6ab}",
      "name": "Nushell",
      "commandline": "C:\\Users\\Mantle Bao\\AppData\\Local\\Programs\\nu\\bin\\nu.exe",
      "icon": "C:\\Users\\Mantle Bao\\AppData\\Local\\Programs\\nu\\nu.ico",
      "startingDirectory": "%USERPROFILE%"
    }
  ]
}
```

And a file named "Mantle" exists under `C:\Users\`:

```nushell
> sudo nu -c `touch C:\Users\Mantle`
> ls `C:\Users\` | find "Mantle" | select name type
╭───┬─────────────────────────────────────────────────┬──────╮
│ # │                      name                       │ type │
├───┼─────────────────────────────────────────────────┼──────┤
│ 0 │ C:\Users\Mantle                                 │ file │
│ 1 │ C:\Users\Mantle Bao                             │ dir  │
╰───┴─────────────────────────────────────────────────┴──────╯
>
```

Launching this profile produces this error in Windows Terminal
1.22.11141.0:

```plain-text
[error 2147942593 (0x800700c1) when launching `C:\Users\Mantle Bao\AppData\Local\Programs\nu\bin\nu.exe']
```

![Error 0x800700c1 pops up when launching the
profile](https://github.com/user-attachments/assets/7cb0d175-299c-4fb0-aa43-2185675e12ae)

[Looking
up](https://learn.microsoft.com/en-us/windows/win32/debug/system-error-code-lookup-tool)
this error code would yield its name as `ERROR_BAD_EXE_FORMAT`, since
the Windows shell will try to execute `C:\\Users\\Mantle` but not the
actual `nu.exe`.

## Hijacking PoC

![Running
Calc](https://github.com/user-attachments/assets/a7ab9ea4-680b-441f-8a7f-26eaad1b7942)

# User-Facing Changes

None. It should only affect the installation phase without any
user-facing changes.

# Tests + Formatting

This PR does not modify Rust or Nu code, and all its improvements belong
to the packaging system. Thus, no conventional tests or formatting
apply. But in case there exists preferred ways to test the packaging
process, please inform me of those, and I would make appropriate
changes.

# After Submitting

None. It should only affect the installation phase without any
post-submission edits.
2025-06-03 21:38:42 +02:00

298 lines
14 KiB
XML

<?xml version="1.0" encoding="UTF-8"?>
<!--
- https://learn.microsoft.com/en-us/windows/win32/msi/single-package-authoring
- https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/msiexec
-->
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs"
xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui"
xmlns:util="http://wixtoolset.org/schemas/v4/wxs/util">
<?define ProductName = "Nushell" ?>
<?define ApplicationFolderName = "nu" ?>
<?define ProductVersion = "$(env.NU_RELEASE_VERSION)" ?>
<?define Manufacturer = "The Nushell Project Developers" ?>
<?define UpgradeCode = "82D756D2-19FA-4F09-B10F-64942E89F364" ?>
<!-- https://docs.firegiant.com/wix/schema/wxs/package/ -->
<Package
Compressed="yes"
Id="Nushell.Nushell"
InstallerVersion="500"
Scope="perUserOrMachine"
Name="$(var.ProductName)"
Version="$(var.ProductVersion)"
UpgradeCode="$(var.UpgradeCode)"
Manufacturer="$(var.Manufacturer)" >
<MajorUpgrade
MigrateFeatures="yes"
Schedule="afterInstallInitialize"
DowngradeErrorMessage="A newer version of [ProductName] is already installed. Setup will now exit." />
<!-- Embed cab media to MSI file -->
<Media Id="1" Cabinet="cab1.cab" EmbedCab="yes" />
<!-- Allow install for Current User Or Machine -->
<Property Id="WixUISupportPerUser" Value="1" />
<Property Id="WixUISupportPerMachine" Value="1" />
<!-- Install for PerUser by default -->
<!-- If set to WixPerMachineFolder will install for PerMachine by default -->
<Property Id="WixAppFolder" Value="WixLocalAppDataFolder" />
<!--
Support for winget install `——scope machine`:
Winget doesn't directly pass ALLUSERS=1 to the MSI installer when using
——scope machine. Instead, it passes a property called INSTALLSCOPEMACHINE=1
This converts the winget parameter to the standard MSI parameter.
FIXME: However, this doesn't seem to work...
-->
<SetProperty Id="ALLUSERS" Value="1" After="LaunchConditions" Condition="INSTALLSCOPEMACHINE=1" />
<!-- This ensures the per-user flag is turned off when installing for all users. -->
<SetProperty Id="MSIINSTALLPERUSER" Value="0" After="LaunchConditions" Condition="INSTALLSCOPEMACHINE=1" />
<!-- Workaround Wix Bug: https://github.com/wixtoolset/issues/issues/2165 -->
<!-- The suggested folder in the dest folder dialog should be C:\Program Files\nu -->
<CustomAction Id="Overwrite_WixSetDefaultPerMachineFolder" Property="WixPerMachineFolder"
Value="[ProgramFiles64Folder][ApplicationFolderName]" Execute="immediate" />
<CustomAction Id="Overwrite_ARPINSTALLLOCATION" Property="ARPINSTALLLOCATION"
Value="[ProgramFiles64Folder][ApplicationFolderName]" Execute="immediate" />
<InstallUISequence>
<Custom Action="Overwrite_WixSetDefaultPerMachineFolder" After="WixSetDefaultPerMachineFolder" />
</InstallUISequence>
<InstallExecuteSequence>
<Custom Action="Overwrite_WixSetDefaultPerMachineFolder" After="WixSetDefaultPerMachineFolder" />
<Custom Action="Overwrite_ARPINSTALLLOCATION" After="InstallValidate"/>
</InstallExecuteSequence>
<!-- Enable UAC prompt when installing for all users -->
<!-- <Property Id="MSIUSEREALADMINDETECTION" Value="1" /> -->
<Icon Id="ProductIconId" SourceFile="$(var.ProjectDir)/nu.ico"/>
<Property Id="ARPPRODUCTICON" Value="ProductIconId" />
<Property Id='ARPHELPLINK' Value='https://www.nushell.sh/book/' />
<Property Id="ApplicationFolderName" Value="$(var.ApplicationFolderName)" />
<!-- INSTALLDIR is the logical target directory whose path will be set by the INSTALLDIR property. -->
<Directory Id="INSTALLDIR" Name="$(var.ApplicationFolderName)">
<!-- LOGICAL_BINDIR's path will be set by the LOGICAL_BINDIR property -->
<Directory Id="LOGICAL_BINDIR" Name="bin" />
</Directory>
<!-- Per Machine Install - these are the definitions of the physical locations -->
<StandardDirectory Id="ProgramFiles64Folder">
<Directory Id="APPLICATIONFOLDER" Name="$(var.ApplicationFolderName)">
<Directory Id="BINDIR" Name="bin">
<!-- Per-machine PATH component -->
<Component Id="EnvironmentPathMachine" Guid="*" Condition="ALLUSERS=1 AND NOT MSIINSTALLPERUSER=1">
<!-- The value MUST BE [LOGICAL_BINDIR] to make sure the env been removed for a custom dir installation -->
<Environment Id="PATHMachine"
Name="PATH"
Value="[LOGICAL_BINDIR]"
Permanent="no"
Part="last"
Action="set"
System="yes" />
<RegistryValue Root="HKLM"
Key="Software\nu"
Name="installed"
Type="integer"
Value="1"
KeyPath="yes" />
</Component>
</Directory>
</Directory>
</StandardDirectory>
<!-- Install for Current User - these are the definitions of the physical locations -->
<StandardDirectory Id="LocalAppDataFolder">
<Directory Id="LocalAppProgramsFolder" Name="Programs">
<Directory Id="INSTALLDIR_USER" Name="$(var.ApplicationFolderName)">
<Directory Id="BINDIR_USER" Name="bin">
<!-- Per-user PATH component -->
<Component Id="EnvironmentPathUser" Guid="*" Condition="MSIINSTALLPERUSER=1">
<Environment Id="PATHUser"
Name="PATH"
Value="[BINDIR_USER]"
Permanent="no"
Part="last"
Action="set"
System="no" />
<RegistryValue Root="HKCU"
Key="Software\nu"
Name="installed"
Type="integer"
Value="1"
KeyPath="yes" />
</Component>
</Directory>
</Directory>
</Directory>
<!-- Windows Terminal Profile Directories -->
<Directory Id="AppDataMicrosoftFolder" Name="Microsoft">
<Directory Id="AppDataWindowsTerminalFolder" Name="Windows Terminal">
<Directory Id="WindowsTerminalProfileFolder" Name="Fragments">
<Directory Id="WindowsTerminalProfileAppFolder" Name="$(var.ApplicationFolderName)">
<Component Id="WindowsTerminalProfile" Guid="*">
<File Id="WindowsTerminalProfileFile"
Name="nu.json"
KeyPath="yes"
Source="$(var.ProjectDir)\windows-terminal-profile.json" />
<RegistryValue Root="HKCU"
Key="Software\nu"
Name="WindowsTerminalProfile"
Value="1"
Type="integer" />
<RemoveFolder Id="RemoveWindowsTerminalProfileFolderA" Directory="WindowsTerminalProfileAppFolder" On="uninstall" />
<RemoveFolder Id="RemoveWindowsTerminalProfileFolderB" Directory="WindowsTerminalProfileFolder" On="uninstall" />
<RemoveFolder Id="RemoveWindowsTerminalProfileFolderC" Directory="AppDataWindowsTerminalFolder" On="uninstall" />
<RemoveFolder Id="RemoveWindowsTerminalProfileFolderD" Directory="AppDataMicrosoftFolder" On="uninstall" />
</Component>
</Directory>
</Directory>
</Directory>
</Directory>
</StandardDirectory>
<ComponentGroup Id="NushellBinaries" Directory="LOGICAL_BINDIR">
<Component Id="Nu_Main" Guid="*">
<File Id="nu.exe" Source="$(var.SourceDir)\nu.exe" KeyPath="yes" />
</Component>
<Component Id="Less" Guid="*">
<File Id="less.exe" Source="$(var.SourceDir)\less.exe" KeyPath="yes" />
</Component>
<Component Id="Nu_Plugin_Inc" Guid="*">
<File Id="nu_plugin_inc.exe" Source="$(var.SourceDir)\nu_plugin_inc.exe" KeyPath="yes" />
</Component>
<Component Id="Nu_Plugin_Gstat" Guid="*">
<File Id="nu_plugin_gstat.exe" Source="$(var.SourceDir)\nu_plugin_gstat.exe" KeyPath="yes" />
</Component>
<Component Id="Nu_Plugin_Query" Guid="*">
<File Id="nu_plugin_query.exe" Source="$(var.SourceDir)\nu_plugin_query.exe" KeyPath="yes" />
</Component>
<Component Id="Nu_Plugin_Polars" Guid="*">
<File Id="nu_plugin_polars.exe" Source="$(var.SourceDir)\nu_plugin_polars.exe" KeyPath="yes" />
</Component>
<Component Id="Nu_Plugin_Formats" Guid="*">
<File Id="nu_plugin_formats.exe" Source="$(var.SourceDir)\nu_plugin_formats.exe" KeyPath="yes" />
</Component>
<Component Id="Less_License" Guid="*">
<File Id="LICENSE_for_less.txt" Source="$(var.SourceDir)\LICENSE-for-less.txt" KeyPath="yes" />
</Component>
</ComponentGroup>
<!-- License and Icon in main installation directory -->
<ComponentGroup Id="NushellResources" Directory="INSTALLDIR">
<Component Id="Nu_Icon" Guid="*">
<File Id="nu.ico" Source="$(var.ProjectDir)\nu.ico" KeyPath="yes" />
</Component>
<Component Id="Nu_Readme" Guid="*">
<File Id="README.txt" Source="$(var.ProjectDir)\README.txt" KeyPath="yes" />
</Component>
<Component Id="Nu_License" Guid="*">
<File Id="License.rtf" Source="$(var.ProjectDir)\License.rtf" KeyPath="yes" />
</Component>
</ComponentGroup>
<!-- Main feature set -->
<Feature Id="ProductFeature"
Level="1"
Title="Nushell"
Description="Install $(var.ProductName) and plugins.">
<ComponentGroupRef Id="NushellBinaries" />
<ComponentGroupRef Id="NushellResources" />
<ComponentRef Id="EnvironmentPathUser" />
<ComponentRef Id="EnvironmentPathMachine" />
</Feature>
<!-- Windows Terminal Profile Feature -->
<Feature Id="WindowsTerminalProfileFeature"
Level="1"
Title="Windows Terminal Profile"
Description="Add $(var.ProductName) profile to Windows Terminal.">
<ComponentRef Id="WindowsTerminalProfile" />
</Feature>
<!-- Load Advanced UI -->
<WixVariable Id="WixUILicenseRtf" Value="$(var.ProjectDir)\License.rtf" />
<ui:WixUI Id="WixUI_Advanced" />
<!-- Windows Version Check -->
<Launch Condition="VersionNT >= 601" Message="This application requires Windows 7 or later." />
<!-- Arch checking -->
<?if $(sys.BUILDARCH) = x64 ?>
<Launch Condition="VersionNT64" Message="This installation package is only supported on 64-bit Windows." />
<?endif?>
<?if $(sys.BUILDARCH) = arm64 ?>
<Launch Condition="ProcessorArchitecture = 'ARM64'" Message="This installation package is only supported on ARM64 Windows." />
<?endif?>
<!-- If installing per-user (MSIINSTALLPERUSER=1), this sets INSTALLDIR and BINDIR to point to user-specific paths -->
<SetProperty Id="INSTALLDIR"
Action="SetINSTALLDIR_User"
Value="[LocalAppDataFolder]Programs\$(var.ApplicationFolderName)"
After="LaunchConditions"
Condition="MSIINSTALLPERUSER=1"
Sequence="both" />
<SetProperty Id="BINDIR"
Action="SetBINDIR_User"
Value="[LocalAppDataFolder]Programs\$(var.ApplicationFolderName)\bin"
After="LaunchConditions"
Condition="MSIINSTALLPERUSER=1"
Sequence="both" />
<!-- If installing per-machine (ALLUSERS=1 AND NOT MSIINSTALLPERUSER=1), this sets INSTALLDIR and BINDIR to point to machine-wide (Program Files) paths -->
<SetProperty Id="INSTALLDIR"
Action="SetINSTALLDIR_Machine"
Value="[APPLICATIONFOLDER]"
After="LaunchConditions"
Condition="ALLUSERS=1 AND NOT MSIINSTALLPERUSER=1"
Sequence="both" />
<!-- Override APPLICATIONFOLDER for 64-bit installations -->
<?if $(sys.BUILDARCH) = x64 OR $(sys.BUILDARCH) = arm64 ?>
<SetProperty Id="APPLICATIONFOLDER"
Value="[ProgramFiles64Folder]$(var.ApplicationFolderName)"
After="LaunchConditions"
Condition="ALLUSERS=1 AND NOT MSIINSTALLPERUSER=1"
Sequence="both" />
<?endif?>
<SetProperty Id="BINDIR"
Action="SetBINDIR_Machine"
Value="[BINDIR]"
After="LaunchConditions"
Condition="ALLUSERS=1 AND NOT MSIINSTALLPERUSER=1"
Sequence="both" />
<!-- Set the LOGICAL_BINDIR property to the resolved BINDIR path -->
<!-- This line MUST NOT be removed in order to set a custom folder for PerMachine installation -->
<SetProperty Id="LOGICAL_BINDIR" Value="[BINDIR]" After="LaunchConditions" Sequence="both" />
<!-- Property that defines the command executed by the Windows Terminal Profile custom action -->
<!-- for Value, see https://learn.microsoft.com/en-ca/windows/win32/msi/formatted -->
<SetProperty Id="ReplacePathsInWindowsTerminalProfile"
Sequence="execute"
After="CostFinalize"
Value="&quot;[#nu.exe]&quot; -c &quot;let doc = (open `[#WindowsTerminalProfileFile]` | update profiles.commandline `\&quot;[#nu.exe]\&quot;` | update profiles.icon `\&quot;[#nu.ico]\&quot;`); $doc | save -f `[#WindowsTerminalProfileFile]`&quot;"
Condition="&amp;WindowsTerminalProfileFeature=3" />
<!-- Defines the custom action that updates paths in the Windows Terminal profile JSON file -->
<CustomAction Id="ReplacePathsInWindowsTerminalProfile"
Return="check"
Impersonate="yes"
Execute="deferred"
DllEntry="WixQuietExec"
BinaryRef="Wix4UtilCA_$(sys.BUILDARCHSHORT)" />
<InstallExecuteSequence>
<Custom Action="ReplacePathsInWindowsTerminalProfile" Before="InstallFinalize"
Condition="&amp;WindowsTerminalProfileFeature=3" />
</InstallExecuteSequence>
</Package>
</Wix>