1

Тема: PowerShell: пример вычисления хешей файла различными способами

С появлением в PowerShell 4.0 командлета Get-FileHash получение хеша файла не является проблемой, но когда я начал изучать PowerShell, на Висте, вроде, был ещё только .Net 2.0 со своим весьма скудным пространством имён и вычисление хеша разными способами казалось интересной задачей. Тогда и написал этот скрипт, стараясь следовать тогдашним правилам написания и оформления кода. В то время у меня все мои скрипты (не только PowerShell, но и WSH) подписывались (что, как потом оказалось, ломало нормальную работу сторонних скриптов) и я выбрал тот вариант оформления кода, который мог обеспечить получение информации по Get-Help для подписанного скрипта, то есть "описание в начале". Скрипт, возможно, интересен с точки зрения решения задачи различными способами и влияния выбранного способа на скорость работы, но, конечно, сейчас это имеет, разве что, исторический интерес. Тем не менее привожу скрипт здесь, предварительно поправив синтаксические неточности в описании, возможно он кого-то заинтересует. Check-Hash.ps1:


<#.SYNOPSIS
Выводит либо проверяет MD5, SHA1, SHA256, SHA384, SHA512 хеши указанного файла.
.DESCRIPTION
Выводит либо проверяет MD5, SHA1, SHA256, SHA384, SHA512 хеши указанного файла.
.PARAMETER Path
Задает путь к файлу.
.PARAMETER MD5
Вычисляет и выводит MD5 хеш.
.PARAMETER SHA1
Вычисляет и выводит SHA1 хеш.
.PARAMETER SHA256
Вычисляет и выводит SHA256 хеш.
.PARAMETER SHA384
Вычисляет и выводит SHA384 хеш.
.PARAMETER SHA512
Вычисляет и выводит SHA512 хеш.
.PARAMETER Common
Вычисляет и выводит MD5 и SHA1 хеш. Если ни один параметр MD5, SHA1, SHA256, SHA384 или SHA512 не задан, считается, что задан параметр Common.
.PARAMETER All
Вычисляет и выводит MD5, SHA1, SHA256, SHA384 и SHA512 хеш.
.PARAMETER CheckMD5
Задает хеш MD5, который следует проверить на совпадение с вычисленным.
.PARAMETER CheckSHA1
Задает хеш SHA1, который следует проверить на совпадение с вычисленным.
.PARAMETER CheckSHA256
Задает хеш SHA256, который следует проверить на совпадение с вычисленным.
.PARAMETER CheckSHA384
Задает хеш SHA384, который следует проверить на совпадение с вычисленным.
.PARAMETER CheckSHA512
Задает хеш SHA512, который следует проверить на совпадение с вычисленным.
.PARAMETER Action
Задает действие, которое следует выполнить. Допустимые значения: "ComputeMD5", "ComputeSHA1", "ComputeSHA256", "ComputeSHA384", "ComputeSHA512", "MD5", "SHA1", "SHA256", "SHA384", "SHA512" - для вычисления соответствующего хеша; "CheckMD5", "CheckSHA1", "CheckSHA256", "CheckSHA384", "CheckSHA512" - для проверки хеша, заданного параметром GivenHash. Регистр символов параметра Action значения не имеет.
.PARAMETER GivenHash
Задает хеш, который следует проверить на совпадение с вычисленным.
.PARAMETER Mode
Задает режим вычисления нескольких хешей. Допустимые значения: 0 (значение по умолчанию) - файл читается однократно, все суммы расчитываются по мере чтения файла; 1 - каждая из хеш сумм вычисляется последовательно, файл читается столько раз, сколько требуется вычислить хешей и обрабатывается как поток, а не читается частями (применяется по умолчанию, если тербуется вычислить только одну хеш сумму); 2 - расчет выполняется параллельно в фоновых заданиях PowerShell, каждое задание вычисляет один хеш и работает в режиме 1 (режим 2 не действует, если необходимо вычислить только одну хеш сумму).
.PARAMETER ScriptArguments
Задает хеш-таблицу, которая используется для передачи дополнительной информации (общие и некоторые необязательные параметры) из основного скрипта в фоновые задания. Ключи хеш-таблицы задают имена переменных скрипта, значения которых должны быть установлены равными значениям хеш-таблицы. Не все переменные могут быть изменены таким способом.
.PARAMETER ToLower
Выводит хеш суммы строчными буквами.
.INPUTS
System.String
Строку, содержащую путь к файлу, можно передать скрипту Check-Hash по конвейеру.
.OUTPUTS
System.Collections.Hashtable
Выводит хеш-таблицу, содержащую вычисленные хеш суммы для файла или логические значения, отражающие успешность проверки заданных хеш сумм.
.NOTES
1. Быстрое прерывание вычислений по Control-Break возможно только в режиме 0. Этот режим можно задать и при вычислении одного хеша.
2. Когда проверяются несколько хеш сумм, выходная хеш-таблица будет содержать значение Status равное $true, если все хеш суммы совпали и $false в противном случае.
3. В режиме 2 из заданных обших параметров в фоновые задания передаются только параметры Verbose, WarningActon, ErrorAction, а также соответствующие им привилегированные переменные, если их значения не равны значениям по умолчанию.
#>

[CmdletBinding(DefaultParameterSetName='Compute')]
Param(	[parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
		[String][Alias("FullName")][ValidateNotNullOrEmpty()]$Path,
	[parameter(ParameterSetName='Compute')][Switch]$MD5,
	[parameter(ParameterSetName='Compute')][Switch]$SHA1,
	[parameter(ParameterSetName='Compute')][Switch]$SHA256,
	[parameter(ParameterSetName='Compute')][Switch]$SHA384,
	[parameter(ParameterSetName='Compute')][Switch]$SHA512,
	[parameter(ParameterSetName='Compute')][Switch]$Common,
	[parameter(ParameterSetName='Compute')][Switch]$All,
	[parameter(ParameterSetName='Check')][String][ValidateLength(32,32)][ValidatePattern('[0-9a-fA-F]{32}')]$CheckMD5,
	[parameter(ParameterSetName='Check')][String][ValidateLength(40,40)][ValidatePattern('[0-9a-fA-F]{40}')]$CheckSHA1,
	[parameter(ParameterSetName='Check')][String][ValidateLength(64,64)][ValidatePattern('[0-9a-fA-F]{64}')]$CheckSHA256,
	[parameter(ParameterSetName='Check')][String][ValidateLength(96,96)][ValidatePattern('[0-9a-fA-F]{96}')]$CheckSHA384,
	[parameter(ParameterSetName='Check')][String][ValidateLength(128,128)][ValidatePattern('[0-9a-fA-F]{128}')]$CheckSHA512,
	[parameter(ParameterSetName='Compute')][parameter(ParameterSetName='Check')]
		[Int][ValidateRange(0,2)][ValidateScript({$script:ModeSet = $true; $true})]$Mode=0,
	[parameter(ParameterSetName='SingleSum', Mandatory=$true, Position=1)]
		[String][ValidatePattern('^(Check|Compute)?(MD5|SHA1|SHA256|SHA384|SHA512){1}$')]$Action,
	[parameter(ParameterSetName='SingleSum', Position=2)][String]$GivenHash,
	[parameter(ParameterSetName='SingleSum')][Hashtable]$ScriptArguments,
	[Switch]$ToLower)

function OutHashTable($out){
	if($out){$HashTable = @{}
		foreach($i in $out){$HashTable += @{([Array]$i.Keys)[0]=([Array]$i.Values)[0]}}
		if(($v = [Array]$HashTable.Values)[0] -is [Boolean] -and $v.length -gt 1){
			$Status = $true
			foreach($i in $v){if(!$i){$Status = $false; break}}
			$HashTable += @{Status=$Status}
		}
	}
	$HashTable
}

function GetTypes($a){$t=@(); foreach($i in $a){$t += @($i.type)}; $t -join ', '}

$Path = Resolve-Path $Path
if(!(Test-Path $Path)){Throw "Не найден:""$Path"""}
$a = @()
switch($PsCmdlet.ParameterSetName){
	'Check'{
		if($CheckMD5){$a += @{type='MD5'; value=$CheckMD5}}
		if($CheckSHA1){$a += @{type='SHA1'; value=$CheckSHA1}}
		if($CheckSHA256){$a += @{type='SHA256'; value=$CheckSHA256}}
		if($CheckSHA384){$a += @{type='SHA384'; value=$CheckSHA384}}
		if($CheckSHA512){$a += @{type='SHA512'; value=$CheckSHA512}}
	}
	'Compute'{
		if($MD5 -or $Common -or $All){$a += @{type='MD5'}}
		if($SHA1 -or $Common -or $All){$a += @{type='SHA1'}}
		if($SHA256 -or $All){$a += @{type='SHA256'}}
		if($SHA384 -or $All){$a += @{type='SHA384'}}
		if($SHA512 -or $All){$a += @{type='SHA512'}}
	}
	'SingleSum'{
		if(($Action -notmatch 'Check' -and !$GivenHash) -or ($Action -match 'Check' -and`
		(($Action -match 'MD5' -and $GivenHash -match '[0-9a-fA-F]{32}') -or`
		($Action -match 'SHA1' -and $GivenHash -match '[0-9a-fA-F]{40}') -or`
		($Action -match 'SHA256' -and $GivenHash -match '[0-9a-fA-F]{64}') -or`
		($Action -match 'SHA384' -and $GivenHash -match '[0-9a-fA-F]{96}') -or`
		($Action -match 'SHA512' -and $GivenHash -match '[0-9a-fA-F]{128}')))){
			$h = @{type=$Action -replace 'Check|Compute', ''}
			if($GivenHash){$h += @{value=$GivenHash}}
			$a += $h
		}else{Throw "Значение параметра GivenHash задано неверно!"}
		if($ScriptArguments){
			$ScriptArguments.GetEnumerator() | Foreach-Object {
				if('a', 'Path', 'value', 'out' -notcontains $_.Key){
					if('Verbose', 'ErrorAction' -contains $_.Key){
							$Key = "$($_.Key)Preference"
							$NewValue = if($_.Value -isnot [Boolean]){$_.Value}
								else{"$(if(!$_.Value){'Silently'})Continue"}
						}
						else{	$Key = "$($_.Key)"
							if($Key -eq 'WarningAction'){$Key = 'WarningPreference'}
							$NewValue = $_.Value
						}
					Set-Variable "$Key" -Value $NewValue -Scope Script
				}
			}
		}
	}
}
if(!$a.Count){$a = @(@{type='MD5'}, @{type='SHA1'})}
$Script = $MyInvocation.MyCommand.Path
$bufsize = 16kb
$buffer = New-Object Byte[] -ArgumentList $bufsize
if($a.Count -eq 1 -and (!$ModeSet -or ($ModeSet -and $Mode -gt 0))){
	$type = $a[0].type
	if($a[0].value){$value = $a[0].value}
	Write-Verbose "Расчет $type выполняется в режиме 1"
	$file = New-Object System.IO.FileStream($Path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read,
		[System.IO.FileShare]::Read, $bufsize)
	$CSP = New-Object "System.Security.Cryptography.${type}CryptoServiceProvider"
	if(!$ToLower){$type = $type.ToUpper(); $x = 'X2'}else{$type = $type.ToLower(); $x = 'x2'}
	$hash = ($CSP.ComputeHash($file) | ForEach-Object {$_.ToString($x)}) -join ''
	$file.Close()
	if($value){@{$type=$hash -eq $value}}else{@{$type=$hash}}
}elseif($Mode -eq 0){
	Write-Verbose "Расчет $(GetTypes $a) выполняется в режиме 0"
	$file = New-Object System.IO.FileStream($Path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read,
		[System.IO.FileShare]::Read, $bufsize)
	for($i = 0; $i -lt $a.length; $i++){
		$a[$i] += @{CSP=New-Object "System.Security.Cryptography.$($a[$i].type)CryptoServiceProvider"}
	}
	while($bytes = $file.Read($buffer, 0, $bufsize)){
		for($i = 0; $i -lt $a.length; $i++){[Void]$a[$i].CSP.TransformBlock($buffer, 0, $bytes, $buffer, 0)}
	}
	for($i = 0; $i -lt $a.length; $i++){[Void]$a[$i].CSP.TransformFinalBlock($buffer, 0, 0)}
	$file.Close()
	$out = @()
	if(!$ToLower){$x = 'X2'}else{$x = 'x2'}
	for($i = 0; $i -lt $a.length; $i++){
		$hash = ($a[$i].CSP.Hash | ForEach-Object {$_.ToString($x)}) -join ''
		$type = if(!$ToLower){$a[$i].type.ToUpper()}else{$a[$i].type.ToLower()}
		$out += if($a[$i].value){@{$type=$hash -eq $a[$i].value}}else{@{$type=$hash}}
	}	
	OutHashTable $out
}elseif($Mode -eq 1){
	Write-Verbose "Расчет $(GetTypes $a) выполняется в режиме 1"
	$out = @()
	foreach($i in $a){
		$out += & "$Script" $Path "$(if($i.value){"Check"})$($i.type)" "$(if($i.value){$i.value})" -ToLower:$ToLower
	}
	OutHashTable $out
}elseif($Mode -eq 2){
	Write-Verbose "Расчет $(GetTypes $a) выполняется в режиме 2"
	$jobs = @()
	$ScriptArguments_ = ""
	$PSBoundParameters.GetEnumerator() |
		Foreach-Object {if($_.Key -notmatch 'Path|MD5|SHA1|SHA256|SHA384|SHA512|Common|All|Mode|^Action$|GivenHash|ScriptArguments'){
			$v = "$($_.Value.ToString())" -replace "'", "''"
			$ScriptArguments_ += "`n$($_.Key)=$(if($_.Value -eq $null){'$null'}else{"[$($_.Value.GetType().Name)]'$v'"})"
		}}
	if($VerbosePreference -ne 'SilentlyContinue' -and $ScriptArguments_ -notmatch '\nVerbose='){
		$ScriptArguments_ += "`nVerbosePreference=[String]'$VerbosePreference'"
	}
	if($WarningPreference -ne 'Continue' -and $ScriptArguments_ -notmatch '\nWarningAction='){
		$ScriptArguments_ += "`nWarningPreference=[String]'$WarningPreference'"
	}
	if($ErrorActionPreference -ne 'Continue' -and $ScriptArguments_ -notmatch '\nErrorAction='){
		$ScriptArguments_ += "`nErrorActionPreference=[String]'$ErrorActionPreference'"
	}
	foreach($i in $a){$jobs += Start-Job -ScriptBlock {param($p1, $p2, $p3, $p4, $p5) $p=@{};
		(ConvertFrom-StringData $p5).GetEnumerator() | Foreach-Object {
			$v = $_.Value -replace '\[SwitchParameter\]', '[Boolean]' -replace '\[ActionPreference\]', '[String]'
			if($v -match "^\[Boolean\]'([^']+)'$"){$v = '$' + $Matches[1]}
			$p += Invoke-Expression "@{$($_.Key)=$v}"
		}
		& "$p1" $p2 $p3 $p4 -ScriptArguments $p}`
		-ArgumentList $Script, $Path, "$(if($i.value){"Check"})$($i.type)", "$(if($i.value){$i.value})", $ScriptArguments_}
	if($jobs){try{$out = Wait-Job $jobs | Receive-Job}finally{Remove-Job $jobs -Force}}
	OutHashTable $out
}else{Throw "Неизвестный режим:$Mode!"}