Fix win execution with whitespace in username #351
This commit addresses the issue where scripts fail to execute on Windows environments with usernames containing spaces. The problem stemmed from PowerShell and cmd shell's handling of spaces in quoted arguments. The solution involves encoding PowerShell commands before execution, which mitigates the quoting issues previously causing script failures. This approach is now integrated into the execution flow, ensuring that commands are correctly handled irrespective of user names or other variables that may include spaces. Changes: - Implement encoding for PowerShell commands to handle spaces in usernames and other similar scenarios. - Update script documentation URLs to reflect changes in directory structure. Fixes #351
This commit is contained in:
@@ -184,7 +184,7 @@ actions:
|
|||||||
> - Logs are valuable for diagnosing issues and understanding past actions [1].
|
> - Logs are valuable for diagnosing issues and understanding past actions [1].
|
||||||
> - Script files can help review changes made to the system and aid in reverting those changes if needed.
|
> - Script files can help review changes made to the system and aid in reverting those changes if needed.
|
||||||
|
|
||||||
[1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com"
|
[1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com"
|
||||||
[2]: https://github.com/undergroundwires/privacy.sexy/blob/master/SECURITY.md "SECURITY.md | privacy.sexy | github.com"
|
[2]: https://github.com/undergroundwires/privacy.sexy/blob/master/SECURITY.md "SECURITY.md | privacy.sexy | github.com"
|
||||||
children:
|
children:
|
||||||
-
|
-
|
||||||
@@ -202,7 +202,7 @@ actions:
|
|||||||
> - This action is irreversible. Deleted script files cannot be retrieved.
|
> - This action is irreversible. Deleted script files cannot be retrieved.
|
||||||
> - These files might be necessary for troubleshooting if you experience issues after using privacy.sexy scripts.
|
> - These files might be necessary for troubleshooting if you experience issues after using privacy.sexy scripts.
|
||||||
|
|
||||||
[1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com"
|
[1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com"
|
||||||
[2]: https://github.com/undergroundwires/privacy.sexy/blob/master/SECURITY.md "SECURITY.md | privacy.sexy | github.com"
|
[2]: https://github.com/undergroundwires/privacy.sexy/blob/master/SECURITY.md "SECURITY.md | privacy.sexy | github.com"
|
||||||
call:
|
call:
|
||||||
function: ClearDirectoryContents
|
function: ClearDirectoryContents
|
||||||
@@ -223,7 +223,7 @@ actions:
|
|||||||
> - Removing logs will prevent you from reviewing the application's activities, which could be helpful in diagnosing issues.
|
> - Removing logs will prevent you from reviewing the application's activities, which could be helpful in diagnosing issues.
|
||||||
> - Logs can contain valuable information for technical support should you need assistance.
|
> - Logs can contain valuable information for technical support should you need assistance.
|
||||||
|
|
||||||
[1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com"
|
[1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com"
|
||||||
[2]: https://github.com/undergroundwires/privacy.sexy/blob/master/SECURITY.md "SECURITY.md | privacy.sexy | github.com"
|
[2]: https://github.com/undergroundwires/privacy.sexy/blob/master/SECURITY.md "SECURITY.md | privacy.sexy | github.com"
|
||||||
call:
|
call:
|
||||||
function: ClearDirectoryContents
|
function: ClearDirectoryContents
|
||||||
@@ -2761,7 +2761,7 @@ actions:
|
|||||||
docs: |-
|
docs: |-
|
||||||
Firefox provides an option for Enhanced Tracking Protection [1], which blocks trackers that
|
Firefox provides an option for Enhanced Tracking Protection [1], which blocks trackers that
|
||||||
gather information about your browsing behavior without disrupting site functionality [1].
|
gather information about your browsing behavior without disrupting site functionality [1].
|
||||||
This feature also includes protections against harmful scripts such as malware that drains
|
This feature also includes protections against harmful scripts such as malware that drain
|
||||||
your battery [1].
|
your battery [1].
|
||||||
|
|
||||||
This script enables the `privacy.resistFingerprinting` preference,
|
This script enables the `privacy.resistFingerprinting` preference,
|
||||||
@@ -2791,7 +2791,7 @@ actions:
|
|||||||
This script enables the `privacy.resistFingerprinting` preference, activating
|
This script enables the `privacy.resistFingerprinting` preference, activating
|
||||||
anti-fingerprinting [1][2].
|
anti-fingerprinting [1][2].
|
||||||
|
|
||||||
As an experimental feature, it might cause some website breakage [2], such as impacting web
|
As an experimental feature, it might cause some website breakages [2], such as impacting web
|
||||||
speech functionality [3] and favicons [4].
|
speech functionality [3] and favicons [4].
|
||||||
|
|
||||||
[1]: https://web.archive.org/web/20221025201025/https://support.mozilla.org/en-US/kb/firefox-protection-against-fingerprinting "Firefox's protection against fingerprinting | Firefox Help | support.mozilla.org"
|
[1]: https://web.archive.org/web/20221025201025/https://support.mozilla.org/en-US/kb/firefox-protection-against-fingerprinting "Firefox's protection against fingerprinting | Firefox Help | support.mozilla.org"
|
||||||
@@ -2876,7 +2876,7 @@ actions:
|
|||||||
|
|
||||||
It's configured to be enabled in nightly, aurora, beta, or default (developer) builds.
|
It's configured to be enabled in nightly, aurora, beta, or default (developer) builds.
|
||||||
In release builds, however, it's set to false [1]. This setting is hard-coded into the C++
|
In release builds, however, it's set to false [1]. This setting is hard-coded into the C++
|
||||||
code to prevent easy disabling [2]. Developers have been approached about this issue but
|
code to prevent easy disabling [2]. Developers have been approached about this issue, but
|
||||||
have rejected proposals to unlock it [3].
|
have rejected proposals to unlock it [3].
|
||||||
|
|
||||||
Mozilla's plan is to deprecate this setting eventually, followed by removal [1].
|
Mozilla's plan is to deprecate this setting eventually, followed by removal [1].
|
||||||
@@ -3012,7 +3012,7 @@ actions:
|
|||||||
recommend: standard
|
recommend: standard
|
||||||
docs: |-
|
docs: |-
|
||||||
This script sets `toolkit.telemetry.server` to be empty.
|
This script sets `toolkit.telemetry.server` to be empty.
|
||||||
This preference defines the server to which Telemetry pings are sent [1].
|
This preference defines the server to which telemetry pings are sent [1].
|
||||||
|
|
||||||
[1]: https://web.archive.org/web/20221015102124/https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/internals/preferences.html "Preferences and Defines — Firefox Source Docs documentation | firefox-source-docs.mozilla.org"
|
[1]: https://web.archive.org/web/20221015102124/https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/internals/preferences.html "Preferences and Defines — Firefox Source Docs documentation | firefox-source-docs.mozilla.org"
|
||||||
call:
|
call:
|
||||||
@@ -3133,7 +3133,7 @@ actions:
|
|||||||
name: Disable Firefox Pioneer study monitoring
|
name: Disable Firefox Pioneer study monitoring
|
||||||
recommend: standard
|
recommend: standard
|
||||||
docs: |-
|
docs: |-
|
||||||
This script configures `toolkit.telemetry.pioneer-new-studies-available` to be disabled to opt out.
|
This script configures `toolkit.telemetry.pioneer-new-studies-available` to be disabled to opt out
|
||||||
Firefox Pioneer program.
|
Firefox Pioneer program.
|
||||||
|
|
||||||
This setting disables availability check for Firefox Pioneer studies [1].
|
This setting disables availability check for Firefox Pioneer studies [1].
|
||||||
|
|||||||
@@ -300,7 +300,7 @@ actions:
|
|||||||
> - Logs are valuable for diagnosing issues and understanding past actions [1].
|
> - Logs are valuable for diagnosing issues and understanding past actions [1].
|
||||||
> - Script files can help review changes made to the system and aid in reverting those changes if needed.
|
> - Script files can help review changes made to the system and aid in reverting those changes if needed.
|
||||||
|
|
||||||
[1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com"
|
[1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com"
|
||||||
[2]: https://github.com/undergroundwires/privacy.sexy/blob/master/SECURITY.md "SECURITY.md | privacy.sexy | github.com"
|
[2]: https://github.com/undergroundwires/privacy.sexy/blob/master/SECURITY.md "SECURITY.md | privacy.sexy | github.com"
|
||||||
children:
|
children:
|
||||||
-
|
-
|
||||||
@@ -318,7 +318,7 @@ actions:
|
|||||||
> - This action is irreversible. Deleted script files cannot be retrieved.
|
> - This action is irreversible. Deleted script files cannot be retrieved.
|
||||||
> - These files might be necessary for troubleshooting if you experience issues after using privacy.sexy scripts.
|
> - These files might be necessary for troubleshooting if you experience issues after using privacy.sexy scripts.
|
||||||
|
|
||||||
[1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com"
|
[1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com"
|
||||||
[2]: https://github.com/undergroundwires/privacy.sexy/blob/master/SECURITY.md "SECURITY.md | privacy.sexy | github.com"
|
[2]: https://github.com/undergroundwires/privacy.sexy/blob/master/SECURITY.md "SECURITY.md | privacy.sexy | github.com"
|
||||||
call:
|
call:
|
||||||
function: ClearDirectoryContents
|
function: ClearDirectoryContents
|
||||||
@@ -339,7 +339,7 @@ actions:
|
|||||||
> - Removing logs will prevent you from reviewing the application's activities, which could be helpful in diagnosing issues.
|
> - Removing logs will prevent you from reviewing the application's activities, which could be helpful in diagnosing issues.
|
||||||
> - Logs can contain valuable information for technical support should you need assistance.
|
> - Logs can contain valuable information for technical support should you need assistance.
|
||||||
|
|
||||||
[1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com"
|
[1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com"
|
||||||
[2]: https://github.com/undergroundwires/privacy.sexy/blob/master/SECURITY.md "SECURITY.md | privacy.sexy | github.com"
|
[2]: https://github.com/undergroundwires/privacy.sexy/blob/master/SECURITY.md "SECURITY.md | privacy.sexy | github.com"
|
||||||
call:
|
call:
|
||||||
function: ClearDirectoryContents
|
function: ClearDirectoryContents
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ actions:
|
|||||||
> - Logs are valuable for diagnosing issues and understanding past actions [1].
|
> - Logs are valuable for diagnosing issues and understanding past actions [1].
|
||||||
> - Script files can help review changes made to the system and aid in reverting those changes if needed.
|
> - Script files can help review changes made to the system and aid in reverting those changes if needed.
|
||||||
|
|
||||||
[1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com"
|
[1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com"
|
||||||
[2]: https://github.com/undergroundwires/privacy.sexy/blob/master/SECURITY.md "SECURITY.md | privacy.sexy | github.com"
|
[2]: https://github.com/undergroundwires/privacy.sexy/blob/master/SECURITY.md "SECURITY.md | privacy.sexy | github.com"
|
||||||
children:
|
children:
|
||||||
-
|
-
|
||||||
@@ -230,7 +230,7 @@ actions:
|
|||||||
> - This action is irreversible. Deleted script files cannot be retrieved.
|
> - This action is irreversible. Deleted script files cannot be retrieved.
|
||||||
> - These files might be necessary for troubleshooting if you experience issues after using privacy.sexy scripts.
|
> - These files might be necessary for troubleshooting if you experience issues after using privacy.sexy scripts.
|
||||||
|
|
||||||
[1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com"
|
[1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com"
|
||||||
[2]: https://github.com/undergroundwires/privacy.sexy/blob/master/SECURITY.md "SECURITY.md | privacy.sexy | github.com"
|
[2]: https://github.com/undergroundwires/privacy.sexy/blob/master/SECURITY.md "SECURITY.md | privacy.sexy | github.com"
|
||||||
call:
|
call:
|
||||||
function: ClearDirectoryContents
|
function: ClearDirectoryContents
|
||||||
@@ -251,7 +251,7 @@ actions:
|
|||||||
> - Removing logs will prevent you from reviewing the application's activities, which could be helpful in diagnosing issues.
|
> - Removing logs will prevent you from reviewing the application's activities, which could be helpful in diagnosing issues.
|
||||||
> - Logs can contain valuable information for technical support should you need assistance.
|
> - Logs can contain valuable information for technical support should you need assistance.
|
||||||
|
|
||||||
[1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com"
|
[1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com"
|
||||||
[2]: https://github.com/undergroundwires/privacy.sexy/blob/master/SECURITY.md "SECURITY.md | privacy.sexy | github.com"
|
[2]: https://github.com/undergroundwires/privacy.sexy/blob/master/SECURITY.md "SECURITY.md | privacy.sexy | github.com"
|
||||||
call:
|
call:
|
||||||
function: ClearDirectoryContents
|
function: ClearDirectoryContents
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import type { PowerShellInvokeShellCommandCreator } from './PowerShellInvokeShellCommandCreator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
Encoding PowerShell commands resolve issues with quote handling.
|
||||||
|
|
||||||
|
There are known problems with PowerShell's handling of double quotes in command line arguments:
|
||||||
|
- Quote stripping in PowerShell command line arguments: https://web.archive.org/web/20240507102706/https://stackoverflow.com/questions/6714165/powershell-stripping-double-quotes-from-command-line-arguments
|
||||||
|
- privacy.sexy double quotes issue when calling PowerShell from command line: https://web.archive.org/web/20240507102841/https://github.com/undergroundwires/privacy.sexy/issues/351
|
||||||
|
- Challenges with single quotes in PowerShell command line: https://web.archive.org/web/20240507102047/https://stackoverflow.com/questions/20958388/command-line-escaping-single-quote-for-powershell
|
||||||
|
|
||||||
|
Using the `EncodedCommand` parameter is recommended by Microsoft for handling
|
||||||
|
complex quoting scenarios. This approach helps avoid issues by encoding the entire
|
||||||
|
command as a Base64 string:
|
||||||
|
- Microsoft's documentation on using the `EncodedCommand` parameter: https://web.archive.org/web/20240507102733/https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_powershell_exe?view=powershell-5.1#-encodedcommand-base64encodedcommand
|
||||||
|
*/
|
||||||
|
export class EncodedPowerShellInvokeCmdCommandCreator
|
||||||
|
implements PowerShellInvokeShellCommandCreator {
|
||||||
|
public createCommandToInvokePowerShell(powerShellScript: string): string {
|
||||||
|
return generateEncodedPowershellCommand(powerShellScript);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateEncodedPowershellCommand(powerShellScript: string): string {
|
||||||
|
const encodedCommand = encodeForPowershellExecution(powerShellScript);
|
||||||
|
return `PowerShell -EncodedCommand ${encodedCommand}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeForPowershellExecution(script: string): string {
|
||||||
|
// The string must be formatted using UTF-16LE character encoding, see: https://web.archive.org/web/20240507102733/https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_powershell_exe?view=powershell-5.1#-encodedcommand-base64encodedcommand
|
||||||
|
const buffer = Buffer.from(script, 'utf16le');
|
||||||
|
return buffer.toString('base64');
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export interface PowerShellInvokeShellCommandCreator {
|
||||||
|
createCommandToInvokePowerShell(powerShellCommand: string): string;
|
||||||
|
}
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import type { ShellArgumentEscaper } from './ShellArgumentEscaper';
|
|
||||||
|
|
||||||
export class CmdShellArgumentEscaper implements ShellArgumentEscaper {
|
|
||||||
public escapePathArgument(pathArgument: string): string {
|
|
||||||
return cmdShellPathArgumentEscape(pathArgument);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function cmdShellPathArgumentEscape(pathArgument: string): string {
|
|
||||||
// - Encloses the path in double quotes, which is necessary for Windows command line (cmd.exe)
|
|
||||||
// to correctly handle paths containing spaces.
|
|
||||||
// - Paths in Windows cannot include double quotes `"` themselves, so these are not escaped.
|
|
||||||
return `"${pathArgument}"`;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import type { ShellArgumentEscaper } from './ShellArgumentEscaper';
|
||||||
|
|
||||||
|
export class PowerShellArgumentEscaper implements ShellArgumentEscaper {
|
||||||
|
public escapePathArgument(pathArgument: string): string {
|
||||||
|
return powerShellPathArgumentEscape(pathArgument);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function powerShellPathArgumentEscape(pathArgument: string): string {
|
||||||
|
// - Encloses the path in single quotes to handle spaces and most special characters.
|
||||||
|
// - Single quotes are used in PowerShell to ensure the string is treated as a literal string.
|
||||||
|
// - Paths in Windows can include single quotes ('), so any internal single quotes are escaped
|
||||||
|
// using double quotes.
|
||||||
|
return `'${pathArgument.replace(/'/g, "''")}'`;
|
||||||
|
}
|
||||||
@@ -1,22 +1,29 @@
|
|||||||
import { CmdShellArgumentEscaper } from './ShellArgument/CmdShellArgumentEscaper';
|
import type { Logger } from '@/application/Common/Log/Logger';
|
||||||
|
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
|
||||||
|
import { PowerShellArgumentEscaper } from './ShellArgument/PowerShellArgumentEscaper';
|
||||||
|
import { EncodedPowerShellInvokeCmdCommandCreator } from './PowerShellInvoke/EncodedPowerShellInvokeCmdCommandCreator';
|
||||||
import type { ShellArgumentEscaper } from './ShellArgument/ShellArgumentEscaper';
|
import type { ShellArgumentEscaper } from './ShellArgument/ShellArgumentEscaper';
|
||||||
import type { CommandDefinition } from '../CommandDefinition';
|
import type { CommandDefinition } from '../CommandDefinition';
|
||||||
|
import type { PowerShellInvokeShellCommandCreator } from './PowerShellInvoke/PowerShellInvokeShellCommandCreator';
|
||||||
|
|
||||||
export class WindowsVisibleTerminalCommand implements CommandDefinition {
|
export class WindowsVisibleTerminalCommand implements CommandDefinition {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly escaper: ShellArgumentEscaper = new CmdShellArgumentEscaper(),
|
private readonly escaper: ShellArgumentEscaper = new PowerShellArgumentEscaper(),
|
||||||
|
private readonly powershellCommandCreator: PowerShellInvokeShellCommandCreator
|
||||||
|
= new EncodedPowerShellInvokeCmdCommandCreator(),
|
||||||
|
private readonly logger: Logger = ElectronLogger,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
public buildShellCommand(filePath: string): string {
|
public buildShellCommand(filePath: string): string {
|
||||||
const command = [
|
const powershellCommand = [
|
||||||
'PowerShell',
|
|
||||||
'Start-Process',
|
'Start-Process',
|
||||||
'-Verb RunAs', // Run as administrator with GUI sudo prompt
|
'-Verb RunAs', // Run as administrator with GUI sudo prompt
|
||||||
`-FilePath ${this.escaper.escapePathArgument(filePath)}`,
|
`-FilePath ${this.escaper.escapePathArgument(filePath)}`,
|
||||||
].join(' ');
|
].join(' ');
|
||||||
return command;
|
|
||||||
/*
|
/*
|
||||||
📝 Options:
|
Running PowerShell command is preferred due to its flexibility and the way it provides
|
||||||
|
GUI sudo prompt through `RunAs` argument.
|
||||||
|
Other options considered:
|
||||||
`child_process.execFile()`
|
`child_process.execFile()`
|
||||||
"path", `cmd.exe /c "path"`
|
"path", `cmd.exe /c "path"`
|
||||||
❌ Script execution in the background without a visible terminal.
|
❌ Script execution in the background without a visible terminal.
|
||||||
@@ -36,6 +43,8 @@ export class WindowsVisibleTerminalCommand implements CommandDefinition {
|
|||||||
`%COMSPEC%` environment variable should be checked before defaulting to `cmd.exe.
|
`%COMSPEC%` environment variable should be checked before defaulting to `cmd.exe.
|
||||||
Related docs: https://web.archive.org/web/20240106002357/https://nodejs.org/api/child_process.html#spawning-bat-and-cmd-files-on-windows
|
Related docs: https://web.archive.org/web/20240106002357/https://nodejs.org/api/child_process.html#spawning-bat-and-cmd-files-on-windows
|
||||||
*/
|
*/
|
||||||
|
this.logger.info(`Building command for PowerShell execution:\n\tCommand: ${powershellCommand}`);
|
||||||
|
return this.powershellCommandCreator.createCommandToInvokePowerShell(powershellCommand);
|
||||||
}
|
}
|
||||||
|
|
||||||
public isExecutionTerminatedExternally(): boolean {
|
public isExecutionTerminatedExternally(): boolean {
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { EncodedPowerShellInvokeCmdCommandCreator } from '@/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/PowerShellInvoke/EncodedPowerShellInvokeCmdCommandCreator';
|
||||||
|
|
||||||
|
describe('EncodedPowerShellInvokeCmdCommandCreator', () => {
|
||||||
|
describe('createCommandToInvokePowerShell', () => {
|
||||||
|
it('starts with PowerShell base command', () => {
|
||||||
|
// arrange
|
||||||
|
const sut = new EncodedPowerShellInvokeCmdCommandCreator();
|
||||||
|
// act
|
||||||
|
const command = sut.createCommandToInvokePowerShell('non-important-command');
|
||||||
|
// assert
|
||||||
|
expect(command.startsWith('PowerShell ')).to.equal(true);
|
||||||
|
});
|
||||||
|
it('includes encoded command as parameter', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedParameterName = '-EncodedCommand';
|
||||||
|
const sut = new EncodedPowerShellInvokeCmdCommandCreator();
|
||||||
|
// act
|
||||||
|
const command = sut.createCommandToInvokePowerShell('non-important-command');
|
||||||
|
// assert
|
||||||
|
const args = parsePowerShellArgs(command);
|
||||||
|
const parameterNames = [...args.keys()];
|
||||||
|
expect(parameterNames).to.include(expectedParameterName);
|
||||||
|
});
|
||||||
|
it('correctly encode the command as utf16le base64', () => {
|
||||||
|
// arrange
|
||||||
|
const givenCode = 'Write-Output "Today is $(Get-Date -Format \'dddd, MMMM dd\')."';
|
||||||
|
const expectedEncodedCommand = 'VwByAGkAdABlAC0ATwB1AHQAcAB1AHQAIAAiAFQAbwBkAGEAeQAgAGkAcwAgACQAKABHAGUAdAAtAEQAYQB0AGUAIAAtAEYAbwByAG0AYQB0ACAAJwBkAGQAZABkACwAIABNAE0ATQBNACAAZABkACcAKQAuACIA';
|
||||||
|
const sut = new EncodedPowerShellInvokeCmdCommandCreator();
|
||||||
|
// act
|
||||||
|
const command = sut.createCommandToInvokePowerShell(givenCode);
|
||||||
|
// assert
|
||||||
|
const args = parsePowerShellArgs(command);
|
||||||
|
const actualEncodedCommand = args.get('-EncodedCommand');
|
||||||
|
expect(actualEncodedCommand).to.equal(expectedEncodedCommand);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function parsePowerShellArgs(command: string): Map<string, string | undefined> {
|
||||||
|
const argsMap = new Map<string, string | undefined>();
|
||||||
|
const argRegex = /(-\w+)(\s+([^ ]+))?/g;
|
||||||
|
let match = argRegex.exec(command);
|
||||||
|
while (match !== null) {
|
||||||
|
const arg = match[1];
|
||||||
|
const value = match[3];
|
||||||
|
argsMap.set(arg, value);
|
||||||
|
match = argRegex.exec(command);
|
||||||
|
}
|
||||||
|
return argsMap;
|
||||||
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { describe } from 'vitest';
|
|
||||||
import { CmdShellArgumentEscaper } from '@/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/ShellArgument/CmdShellArgumentEscaper';
|
|
||||||
import { runEscapeTests } from './ShellArgumentEscaperTestRunner';
|
|
||||||
|
|
||||||
describe('CmdShellArgumentEscaper', () => {
|
|
||||||
runEscapeTests(() => new CmdShellArgumentEscaper(), [
|
|
||||||
{
|
|
||||||
description: 'encloses the path in double quotes',
|
|
||||||
givenPath: 'C:\\Program Files\\app.exe',
|
|
||||||
expectedPath: '"C:\\Program Files\\app.exe"',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { describe } from 'vitest';
|
||||||
|
import { PowerShellArgumentEscaper } from '@/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/ShellArgument/PowerShellArgumentEscaper';
|
||||||
|
import { runEscapeTests } from './ShellArgumentEscaperTestRunner';
|
||||||
|
|
||||||
|
describe('PowerShellArgumentEscaper', () => {
|
||||||
|
runEscapeTests(() => new PowerShellArgumentEscaper(), [
|
||||||
|
{
|
||||||
|
description: 'encloses the path in single quotes',
|
||||||
|
givenPath: 'C:\\Program Files\\app.exe',
|
||||||
|
expectedPath: '\'C:\\Program Files\\app.exe\'',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'escapes single internal single quotes',
|
||||||
|
givenPath: 'C:\\Users\\O\'Reilly\\Documents',
|
||||||
|
expectedPath: '\'C:\\Users\\O\'\'Reilly\\Documents\'',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: 'escapes multiple internal single quotes',
|
||||||
|
givenPath: 'C:\\Program Files\\User\'s Files\\Today\'s Files',
|
||||||
|
expectedPath: '\'C:\\Program Files\\User\'\'s Files\\Today\'\'s Files\'',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
@@ -2,24 +2,66 @@ import { describe, it, expect } from 'vitest';
|
|||||||
import { WindowsVisibleTerminalCommand } from '@/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/WindowsVisibleTerminalCommand';
|
import { WindowsVisibleTerminalCommand } from '@/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/WindowsVisibleTerminalCommand';
|
||||||
import type { ShellArgumentEscaper } from '@/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/ShellArgument/ShellArgumentEscaper';
|
import type { ShellArgumentEscaper } from '@/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/ShellArgument/ShellArgumentEscaper';
|
||||||
import { ShellArgumentEscaperStub } from '@tests/unit/shared/Stubs/ShellArgumentEscaperStub';
|
import { ShellArgumentEscaperStub } from '@tests/unit/shared/Stubs/ShellArgumentEscaperStub';
|
||||||
|
import type { Logger } from '@/application/Common/Log/Logger';
|
||||||
|
import { LoggerStub } from '@tests/unit/shared/Stubs/LoggerStub';
|
||||||
|
import type { PowerShellInvokeShellCommandCreator } from '@/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/PowerShellInvoke/PowerShellInvokeShellCommandCreator';
|
||||||
|
import { PowerShellInvokeShellCommandCreatorStub } from '@tests/unit/shared/Stubs/PowerShellInvokeShellCommandCreatorStub';
|
||||||
|
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||||
|
|
||||||
describe('WindowsVisibleTerminalCommand', () => {
|
describe('WindowsVisibleTerminalCommand', () => {
|
||||||
describe('buildShellCommand', () => {
|
describe('buildShellCommand', () => {
|
||||||
it('returns expected command for given escaped file path', () => {
|
it('creates a PowerShell command with the escaped path', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const escapedFilePath = '/escaped/file/path';
|
const escapedFilePath = '/escaped/file/path';
|
||||||
const expectedCommand = `PowerShell Start-Process -Verb RunAs -FilePath ${escapedFilePath}`;
|
const expectedCommand = `Start-Process -Verb RunAs -FilePath ${escapedFilePath}`;
|
||||||
const escaper = new ShellArgumentEscaperStub();
|
const escaper = new ShellArgumentEscaperStub();
|
||||||
escaper.escapePathArgument = () => escapedFilePath;
|
escaper.escapePathArgument = () => escapedFilePath;
|
||||||
|
const powerShellCommandCreator = new PowerShellInvokeShellCommandCreatorStub();
|
||||||
const sut = new CommandBuilder()
|
const sut = new CommandBuilder()
|
||||||
.withEscaper(escaper)
|
.withEscaper(escaper)
|
||||||
|
.withPowerShellCommandCreator(powerShellCommandCreator)
|
||||||
|
.build();
|
||||||
|
// act
|
||||||
|
sut.buildShellCommand('unimportant');
|
||||||
|
// assert
|
||||||
|
const calls = powerShellCommandCreator.callHistory.filter((c) => c.methodName === 'createCommandToInvokePowerShell');
|
||||||
|
expect(calls).to.have.lengthOf(1);
|
||||||
|
const [actualCommand] = calls[0].args;
|
||||||
|
expect(actualCommand).to.equal(expectedCommand);
|
||||||
|
});
|
||||||
|
it('returns a command to invoke PowerShell', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedCommand = 'expected command from creator';
|
||||||
|
const powerShellCommandCreator = new PowerShellInvokeShellCommandCreatorStub();
|
||||||
|
powerShellCommandCreator.createCommandToInvokePowerShell = () => expectedCommand;
|
||||||
|
const sut = new CommandBuilder()
|
||||||
|
.withPowerShellCommandCreator(powerShellCommandCreator)
|
||||||
.build();
|
.build();
|
||||||
// act
|
// act
|
||||||
const actualCommand = sut.buildShellCommand('unimportant');
|
const actualCommand = sut.buildShellCommand('unimportant');
|
||||||
// assert
|
// assert
|
||||||
expect(actualCommand).to.equal(expectedCommand);
|
expect(actualCommand).to.equal(expectedCommand);
|
||||||
});
|
});
|
||||||
it('escapes provided file path correctly', () => {
|
it('logs the powershell command', () => {
|
||||||
|
// arrange
|
||||||
|
let expectedCommand: string | undefined;
|
||||||
|
const powerShellCommandCreator = new PowerShellInvokeShellCommandCreatorStub();
|
||||||
|
powerShellCommandCreator.createCommandToInvokePowerShell = (command) => {
|
||||||
|
expectedCommand = command;
|
||||||
|
return 'unimportant command';
|
||||||
|
};
|
||||||
|
const logger = new LoggerStub();
|
||||||
|
const sut = new CommandBuilder()
|
||||||
|
.withLogger(logger)
|
||||||
|
.withPowerShellCommandCreator(powerShellCommandCreator)
|
||||||
|
.build();
|
||||||
|
// act
|
||||||
|
sut.buildShellCommand('unimportant');
|
||||||
|
// assert
|
||||||
|
expectExists(expectedCommand);
|
||||||
|
logger.assertLogsContainMessagePart('info', expectedCommand);
|
||||||
|
});
|
||||||
|
it('escapes the provided file path', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedFilePath = '/input';
|
const expectedFilePath = '/input';
|
||||||
const escaper = new ShellArgumentEscaperStub();
|
const escaper = new ShellArgumentEscaperStub();
|
||||||
@@ -61,14 +103,33 @@ describe('WindowsVisibleTerminalCommand', () => {
|
|||||||
class CommandBuilder {
|
class CommandBuilder {
|
||||||
private escaper: ShellArgumentEscaper = new ShellArgumentEscaperStub();
|
private escaper: ShellArgumentEscaper = new ShellArgumentEscaperStub();
|
||||||
|
|
||||||
|
private logger: Logger = new LoggerStub();
|
||||||
|
|
||||||
|
private powerShellCommandCreator
|
||||||
|
: PowerShellInvokeShellCommandCreator = new PowerShellInvokeShellCommandCreatorStub();
|
||||||
|
|
||||||
public withEscaper(escaper: ShellArgumentEscaper): this {
|
public withEscaper(escaper: ShellArgumentEscaper): this {
|
||||||
this.escaper = escaper;
|
this.escaper = escaper;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public withPowerShellCommandCreator(
|
||||||
|
powerShellCommandCreator: PowerShellInvokeShellCommandCreator,
|
||||||
|
): this {
|
||||||
|
this.powerShellCommandCreator = powerShellCommandCreator;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public withLogger(logger: Logger): this {
|
||||||
|
this.logger = logger;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public build(): WindowsVisibleTerminalCommand {
|
public build(): WindowsVisibleTerminalCommand {
|
||||||
return new WindowsVisibleTerminalCommand(
|
return new WindowsVisibleTerminalCommand(
|
||||||
this.escaper,
|
this.escaper,
|
||||||
|
this.powerShellCommandCreator,
|
||||||
|
this.logger,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import type { PowerShellInvokeShellCommandCreator } from '@/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/PowerShellInvoke/PowerShellInvokeShellCommandCreator';
|
||||||
|
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
|
||||||
|
|
||||||
|
export class PowerShellInvokeShellCommandCreatorStub
|
||||||
|
extends StubWithObservableMethodCalls<PowerShellInvokeShellCommandCreator>
|
||||||
|
implements PowerShellInvokeShellCommandCreator {
|
||||||
|
private command: string | undefined;
|
||||||
|
|
||||||
|
public withCreatedCommand(command: string): this {
|
||||||
|
this.command = command;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public createCommandToInvokePowerShell(powerShellCommand: string): string {
|
||||||
|
this.registerMethodCall({
|
||||||
|
methodName: 'createCommandToInvokePowerShell',
|
||||||
|
args: [powerShellCommand],
|
||||||
|
});
|
||||||
|
if (this.command === undefined) {
|
||||||
|
return `[${PowerShellInvokeShellCommandCreatorStub.name}] ${powerShellCommand}`;
|
||||||
|
}
|
||||||
|
return this.command;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user