Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 118 additions & 15 deletions src/Microsoft.PowerShell.Commands.Utility/commands/utility/GetHash.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,24 @@
namespace Microsoft.PowerShell.Commands
{
/// <summary>
/// This class implements Get-FileHash
/// This class implements Get-Hash
/// </summary>
[Cmdlet(VerbsCommon.Get, "FileHash", DefaultParameterSetName = PathParameterSet, HelpUri = "https://go.microsoft.com/fwlink/?LinkId=517145")]
[OutputType(typeof(FileHashInfo))]
public class GetFileHashCommand : HashCmdletBase
[Cmdlet(VerbsCommon.Get, "Hash", DefaultParameterSetName = PathParameterSet, HelpUri = "https://go.microsoft.com/fwlink/?LinkId=517145")]
[OutputType(typeof(FileHashInfo), ParameterSetName = new[] { PathParameterSet,
LiteralPathParameterSet,
StreamParameterSet
})]
[OutputType(typeof(StringHashInfo), ParameterSetName = new[] { StringHashParameterSet })]
public class GetHashCommand : HashCmdletBase
{
/// <summary>
/// Path parameter
/// The paths of the files to calculate a hashs
/// Resolved wildcards
/// </summary>
/// <value></value>
[Parameter(Mandatory = true, ParameterSetName = PathParameterSet, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
public String[] Path
[Parameter(Mandatory = true, ParameterSetName = PathParameterSet, Position = 0, ValueFromPipelineByPropertyName = true)]
public string[] Path
{
get
{
Expand All @@ -41,7 +45,7 @@ public String[] Path
/// <value></value>
[Parameter(Mandatory = true, ParameterSetName = LiteralPathParameterSet, Position = 0, ValueFromPipelineByPropertyName = true)]
[Alias("PSPath")]
public String[] LiteralPath
public string[] LiteralPath
{
get
{
Expand All @@ -53,7 +57,7 @@ public String[] LiteralPath
}
}

private String[] _paths;
private string[] _paths;

/// <summary>
/// InputStream parameter
Expand All @@ -63,6 +67,38 @@ public String[] LiteralPath
[Parameter(Mandatory = true, ParameterSetName = StreamParameterSet, Position = 0)]
public Stream InputStream { get; set; }

/// <summary>
/// InputString parameter
/// The strings to calculate a hash
/// We allow `null` and `empty` strings because we can get it from pipeline.
/// </summary>
/// <value></value>
[Parameter(Mandatory = true, ParameterSetName = StringHashParameterSet, Position = 0, ValueFromPipeline = true)]
[AllowNull()]
[AllowEmptyString()]
public string[] InputString { get; set; }

/// <summary>
/// Encoding parameter
/// The Encoding of the 'InputString'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is confusing because a string is always encoded in Unicode.
A better comment might be something like:

the 'InputString' converted to the encoding if necessary

I do wonder if this parameter makes sense. In light of the approach in #4119 - the parameter becomes FileEncoding, which then becomes very confusing for this cmdlet - it applies to the string and not the file.

Given how uncommon it is to hash strings in other encodings, maybe it's fine to require the caller to convert to another encoding and this cmdlet also support hashing a byte array.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll fix the comment.

About #4119. We have a conflict. We can have (1) split into two cmdlets - Get-FileHash and Get-StringHash, two parameters - FileEncoding and StringEncoding, or universal cmdlet Get-Hash with a special solution. If we want one cmdlet it would be good that it accept strings "as is".

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should remove this parameter, but use UTF8 encoding. Anyone wanting something else would write the string to a temp file and hash the file.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, the original idea was just to eliminate such workarounds.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could add an alias for FileEncoding - Encoding.

/// </summary>
/// <value></value>
[Parameter(Mandatory = false, ParameterSetName = StringHashParameterSet, Position = 2)]
[ValidateSetAttribute(new string[] {
EncodingConversion.Unknown,
EncodingConversion.String,
EncodingConversion.Unicode,
EncodingConversion.BigEndianUnicode,
EncodingConversion.Utf8,
EncodingConversion.Utf7,
EncodingConversion.Utf32,
EncodingConversion.Ascii,
EncodingConversion.Default,
EncodingConversion.OEM })]
public string Encoding { get; set; } = EncodingConversion.Default;



/// <summary>
/// BeginProcessing() override
/// This is for hash function init
Expand All @@ -78,6 +114,34 @@ protected override void BeginProcessing()
/// </summary>
protected override void ProcessRecord()
{
if ( ParameterSetName == StringHashParameterSet)
{
if (InputString == null)
{
WriteStringHashInfo(Algorithm, null, null, Encoding);
}
else
{
foreach (string str in InputString)
{
if (str == null)
{
WriteStringHashInfo(Algorithm, string.Empty, string.Empty, Encoding);
Exception exception = new Exception("Hash for 'null' string is 'null'");
WriteError(new ErrorRecord(exception, "GetHashInvalidData", ErrorCategory.InvalidData, null));
}
else
{
byte[] bytehash = hasher.ComputeHash(EncodingConversion.Convert(this, Encoding).GetBytes(str));
string hash = BitConverter.ToString(bytehash).Replace("-","");
WriteStringHashInfo(Algorithm, hash, str, Encoding);
}
}
}

return;
}

List<string> pathsToProcess = new List<string>();
ProviderInfo provider = null;

Expand Down Expand Up @@ -119,17 +183,15 @@ protected override void ProcessRecord()

foreach (string path in pathsToProcess)
{
byte[] bytehash = null;
String hash = null;
Stream openfilestream = null;

try
{
openfilestream = File.OpenRead(path);
bytehash = hasher.ComputeHash(openfilestream);
byte[] bytehash = hasher.ComputeHash(openfilestream);

hash = BitConverter.ToString(bytehash).Replace("-","");
WriteHashResult(Algorithm, hash, path);
String hash = BitConverter.ToString(bytehash).Replace("-","");
WriteFileHashInfo(Algorithm, hash, path);
}
catch (FileNotFoundException ex)
{
Expand Down Expand Up @@ -160,14 +222,14 @@ protected override void EndProcessing()
bytehash = hasher.ComputeHash(InputStream);

hash = BitConverter.ToString(bytehash).Replace("-","");
WriteHashResult(Algorithm, hash, "");
WriteFileHashInfo(Algorithm, hash, "");
}
}

/// <summary>
/// Create FileHashInfo object and output it
/// </summary>
private void WriteHashResult(string Algorithm, string hash, string path)
private void WriteFileHashInfo(string Algorithm, string hash, string path)
{
FileHashInfo result = new FileHashInfo();
result.Algorithm = Algorithm;
Expand All @@ -176,12 +238,26 @@ private void WriteHashResult(string Algorithm, string hash, string path)
WriteObject(result);
}

/// <summary>
/// Create StringHashInfo object and output it
/// </summary>
private void WriteStringHashInfo(string Algorithm, string hash, string HashedString, string Encoding)
{
StringHashInfo result = new StringHashInfo();
result.Algorithm = Algorithm;
result.Hash = hash;
result.HashedString = HashedString;
result.Encoding = Encoding;
WriteObject(result);
}

/// <summary>
/// Parameter set names
/// </summary>
private const string PathParameterSet = "Path";
private const string LiteralPathParameterSet = "LiteralPath";
private const string StreamParameterSet = "StreamParameterSet";
private const string StringHashParameterSet = "StringHashParameterSet";

}

Expand Down Expand Up @@ -289,4 +365,31 @@ public class FileHashInfo
/// </summary>
public string Path { get; set;}
}

/// <summary>
/// StringHashInfo class contains information about a String hash
/// </summary>
public class StringHashInfo
{
/// <summary>
/// Hash algorithm name
/// </summary>
public string Algorithm { get; set;}

/// <summary>
/// Hash value of the 'HashedString' string
/// </summary>
public string Hash { get; set;}

/// <summary>
/// Encoding of the 'HashedString' string
/// </summary>
public string Encoding { get; set;}

/// <summary>
/// HashedString value which 'Hash' calculated for
/// </summary>
public string HashedString { get; set;}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ CmdletsToExport= "Format-List", "Format-Custom", "Format-Table", "Format-Wide",
"Clear-Variable", "Export-Clixml", "Import-Clixml", "Import-PowerShellDataFile", "ConvertTo-Xml", "Select-Xml", "Write-Debug",
"Write-Verbose", "Write-Warning", "Write-Error", "Write-Information", "Write-Output", "Set-PSBreakpoint",
"Get-PSBreakpoint", "Remove-PSBreakpoint", "Enable-PSBreakpoint", "Disable-PSBreakpoint", "Get-PSCallStack",
"Send-MailMessage", "Get-TraceSource", "Set-TraceSource", "Trace-Command", "Get-FileHash",
"Send-MailMessage", "Get-TraceSource", "Set-TraceSource", "Trace-Command", "Get-Hash",
"Get-Runspace", "Debug-Runspace", "Enable-RunspaceDebug", "Disable-RunspaceDebug",
"Get-RunspaceDebug", "Wait-Debugger" , "Get-Uptime", "New-TemporaryFile", "Get-Verb", "Format-Hex", "Remove-Alias"
FunctionsToExport= "Import-PowerShellDataFile"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ CmdletsToExport= "Format-List", "Format-Custom", "Format-Table", "Format-Wide",
"Clear-Variable", "Export-Clixml", "Import-Clixml", "Import-PowerShellDataFile","ConvertTo-Xml", "Select-Xml", "Write-Debug",
"Write-Verbose", "Write-Warning", "Write-Error", "Write-Information", "Write-Output", "Set-PSBreakpoint",
"Get-PSBreakpoint", "Remove-PSBreakpoint", "New-TemporaryFile", "Enable-PSBreakpoint", "Disable-PSBreakpoint", "Get-PSCallStack",
"Send-MailMessage", "Get-TraceSource", "Set-TraceSource", "Trace-Command", "Get-FileHash",
"Send-MailMessage", "Get-TraceSource", "Set-TraceSource", "Trace-Command", "Get-Hash",
"Unblock-File", "Get-Runspace", "Debug-Runspace", "Enable-RunspaceDebug", "Disable-RunspaceDebug",
"Get-RunspaceDebug", "Wait-Debugger" , "Get-Uptime", "Get-Verb", "Format-Hex", "Remove-Alias"
FunctionsToExport= "ConvertFrom-SddlString"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ CmdletsToExport= "Format-List", "Format-Custom", "Format-Table", "Format-Wide",
"Clear-Variable", "Export-Clixml", "Import-Clixml", "Import-PowerShellDataFile", "ConvertTo-Xml", "Select-Xml", "Write-Debug",
"Write-Verbose", "Write-Warning", "Write-Error", "Write-Information", "Write-Output", "Set-PSBreakpoint", "Get-PSBreakpoint",
"Remove-PSBreakpoint", "Enable-PSBreakpoint", "Disable-PSBreakpoint", "Get-PSCallStack",
"Send-MailMessage", "Get-TraceSource", "Set-TraceSource", "Trace-Command", "Show-Command", "Unblock-File", "Get-FileHash",
"Send-MailMessage", "Get-TraceSource", "Set-TraceSource", "Trace-Command", "Show-Command", "Unblock-File", "Get-Hash",
"Get-Runspace", "Debug-Runspace", "Enable-RunspaceDebug", "Disable-RunspaceDebug", "Get-RunspaceDebug", "Wait-Debugger",
"ConvertFrom-String", "Convert-String" , "Get-Uptime", "New-TemporaryFile", "Get-Verb", "Format-Hex"
FunctionsToExport= "ConvertFrom-SddlString"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4605,6 +4605,7 @@ internal static SessionStateAliasEntry[] BuiltInAliases
new SessionStateAliasEntry("ghy", "Get-History", "", ReadOnly),
new SessionStateAliasEntry("gi", "Get-Item", "", ReadOnly),
new SessionStateAliasEntry("gl", "Get-Location", "", ReadOnly),
new SessionStateAliasEntry("Get-FileHash", "Get-Hash", "", ReadOnly),
new SessionStateAliasEntry("gm", "Get-Member", "", ReadOnly),
new SessionStateAliasEntry("gmo", "Get-Module", "", ReadOnly),
new SessionStateAliasEntry("gp", "Get-ItemProperty", "", ReadOnly),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
Describe "Get-Hash tests for files" -Tags "CI" {

BeforeAll {
$testDocument = Join-Path -Path $PSScriptRoot -ChildPath assets testablescript.ps1
}

Context "Default result tests" {
It "Should default to correct algorithm, hash and path" {
$result = Get-Hash $testDocument

$result | Should BeOfType 'Microsoft.PowerShell.Commands.FileHashInfo'
$result.Algorithm | Should Be "SHA256"
$result.Hash | Should Be "4A6DA9F1C0827143BB19FC4B0F2A8057BC1DF55F6D1F62FA3B917BA458E8F570"
$result.Path | Should Be $testDocument
}
}

Context "Algorithm tests" {
BeforeAll {
# Keep "sHA1" below! It is for testing that the cmdlet accept a hash algorithm name in any case!
$testcases =
@{ algorithm = "sHA1"; hash = "01B865D143E07ECC875AB0EFC0A4429387FD0CF7" },
@{ algorithm = "SHA256"; hash = "4A6DA9F1C0827143BB19FC4B0F2A8057BC1DF55F6D1F62FA3B917BA458E8F570" },
@{ algorithm = "SHA384"; hash = "656215B6A07011E625206F43E57873F49AD7B36DFCABB70F6CDCE2303D7A603E55D052774D26F339A6D80A264340CB8C" },
@{ algorithm = "SHA512"; hash = "C688C33027D89ACAC920545471C8053D8F64A54E21D0415F1E03766DDCDA215420E74FAFD1DC399864C6B6B5723A3358BD337339906797A39090B02229BF31FE" },
@{ algorithm = "MD5"; hash = "7B09811D1631C9FD46B39D1D35522F0A" }
}

It "Should be able to get the correct hash by Path from <algorithm> algorithm" -TestCases $testCases {
param($algorithm, $hash)
$algorithmResult = Get-Hash -Path $testDocument -Algorithm $algorithm

$algorithmResult | Should BeOfType 'Microsoft.PowerShell.Commands.FileHashInfo'
$algorithmResult.Algorithm | Should Be $algorithm
$algorithmResult.Hash | Should Be $hash
$algorithmResult.Path | Should Be $testDocument
}

It "Should be able to get the correct hash by InputStream from <algorithm> algorithm" -TestCases $testCases {
param($algorithm, $hash)
$testFileStream = [System.IO.File]::OpenRead($testDocument)
$algorithmResult = Get-Hash -InputStream $testFileStream -Algorithm $algorithm

$algorithmResult | Should BeOfType 'Microsoft.PowerShell.Commands.FileHashInfo'
$algorithmResult.Algorithm | Should Be $algorithm
$algorithmResult.Hash | Should Be $hash
}

It "Should be able to get the correct hash by String from <algorithm> algorithm" -TestCases $testCases {
param($algorithm, $hash)
# Simple trick needed to get a test string from byte sequence because the test file contains BOM.
# It allows to reuse the file hashes.
$testBytes = Get-Content $testDocument -Raw -Encoding Byte
$testString = [System.Text.Encoding]::UTF8.GetString($testBytes)
$algorithmResult = Get-Hash -InputString $testString -Algorithm $algorithm -Encoding UTF8
$algorithmResultFromPipe = $testString | Get-Hash -Algorithm $algorithm -Encoding UTF8

$algorithmResult | Should BeOfType 'Microsoft.PowerShell.Commands.StringHashInfo'
$algorithmResult.Algorithm | Should Be $algorithm
$algorithmResult.Hash | Should Be $hash
$algorithmResult.Encoding | Should Be 'UTF8'
$algorithmResult.HashedString | Should Be $testString

$algorithmResultFromPipe | Should BeOfType 'Microsoft.PowerShell.Commands.StringHashInfo'
$algorithmResultFromPipe.Algorithm | Should Be $algorithm
$algorithmResultFromPipe.Hash | Should Be $hash
$algorithmResultFromPipe.Encoding | Should Be 'UTF8'
$algorithmResultFromPipe.HashedString | Should Be $testString
}

It "Should be able to get the correct hash for 'null' String" {
$result = Get-Hash -InputString $null

$result | Should BeOfType 'Microsoft.PowerShell.Commands.StringHashInfo'
$result.Algorithm | Should Be 'SHA256'
$result.Hash | Should Be $null
$result.Encoding | Should Be 'Default'
$result.HashedString | Should Be $null
}

It "Should be throw for wrong algorithm name" {
{ Get-Hash $testDocument -Algorithm wrongAlgorithm } | ShouldBeErrorId "ParameterArgumentValidationError,Microsoft.PowerShell.Commands.GetHashCommand"
}
}

Context "Paths tests" {
It "With '-Path': no file exist" {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a test for piping a file. The equivalent of dir * -File | Get-FileHash

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To ensure compatibility

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the test .

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#resolved

{ Get-Hash -Path nofileexist.ttt -ErrorAction Stop } | ShouldBeErrorId "FileNotFound,Microsoft.PowerShell.Commands.GetHashCommand"
}

It "With '-LiteralPath': no file exist" {
{ Get-Hash -LiteralPath nofileexist.ttt -ErrorAction Stop } | ShouldBeErrorId "FileNotFound,Microsoft.PowerShell.Commands.GetHashCommand"
}

It "With '-Path': file exist" {
$result = Get-Hash -Path $testDocument

$result | Should BeOfType 'Microsoft.PowerShell.Commands.FileHashInfo'
$result.Algorithm | Should Be "SHA256"
$result.Hash | Should Be "4A6DA9F1C0827143BB19FC4B0F2A8057BC1DF55F6D1F62FA3B917BA458E8F570"
$result.Path | Should Be $testDocument
}

It "With '-LiteralPath': file exist" {
$result = Get-Hash -LiteralPath $testDocument

$result | Should BeOfType 'Microsoft.PowerShell.Commands.FileHashInfo'
$result.Algorithm | Should Be "SHA256"
$result.Hash | Should Be "4A6DA9F1C0827143BB19FC4B0F2A8057BC1DF55F6D1F62FA3B917BA458E8F570"
$result.Path | Should Be $testDocument
}

It "With '-Path': using a pipe" {
$result = Get-ChildItem $testDocument | Get-Hash

$result | Should BeOfType 'Microsoft.PowerShell.Commands.FileHashInfo'
$result.Algorithm | Should Be "SHA256"
$result.Hash | Should Be "4A6DA9F1C0827143BB19FC4B0F2A8057BC1DF55F6D1F62FA3B917BA458E8F570"
$result.Path | Should Be $testDocument
}
}
}
Loading