使用PowerShell和计算哈希仅流式传输文件的一部分



我需要能够识别一些在安全服务器之间复制和重命名的大型二进制文件。要做到这一点,我希望能够散列所有文件的前X个字节和最后X个字节。我只需要用标准Windows 10系统上可用的东西来做这件事,没有安装额外的软件,所以PowerShell似乎是正确的选择。

一些不起作用的东西:

  • 我无法在中读取整个文件,然后提取我想要哈希的文件部分。我试图实现的目标是最大限度地减少我需要读取的文件量,而读取整个文件会破坏这一目的
  • 将文件的较大部分读取到PowerShell变量中似乎相当缓慢,因此$hash.ComputeHash($moderatelyLargeVariable)似乎不是一个可行的解决方案

我很确定我需要执行$hash.ComputeHash($stream),其中$stream只流式传输文件的一部分。

到目前为止,我已经尝试过:

function Get-FileStreamHash {
param (
$FilePath,
$Algorithm
)
$hash = [Security.Cryptography.HashAlgorithm]::Create($Algorithm)
## METHOD 0: See description below
$stream = ([IO.StreamReader]"${FilePath}").BaseStream
$hashValue = $hash.ComputeHash($stream)
## END of part I need help with
# Convert to a hexadecimal string
$hexHashValue = -join ($hashValue | ForEach-Object { "{0:x2}" -f $_ })
$stream.Close()
# return
$hexHashValue
}

方法0:这是有效的,但它正在流式传输整个文件,因此不能解决我的问题。对于一个3GB的文件,这在我的机器上大约需要7秒。

方法1:$hashValue = $hash.ComputeHash((Get-Content -Path $FilePath -Stream ""))。这也会对整个文件进行流式传输,而且还需要很长时间。对于同一个3GB文件,它需要超过5分钟的时间(我当时取消了,不知道总持续时间是多少(。

方法2:$hashValue = $hash.ComputeHash((Get-Content -Path $FilePath -Encoding byte -TotalCount $qtyBytes -Stream ""))。这与方法1相同,只是将内容限制为$qtyBytes。在1000000(1MB(时需要18秒。我认为这意味着方法1需要大约15个小时,比方法0慢7700倍。

有没有一种方法可以像方法2一样(限制阅读内容(,但不需要放慢速度?如果是这样的话,有没有一个好的方法可以在文件的末尾完成?

谢谢!

您可以尝试以下助手函数中的一个(或两者的组合(来读取文件开头或结尾的多个字节:

function Read-FirstBytes {
param (
[Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 0)]
[Alias('FullName', 'FilePath')]
[ValidateScript({ Test-Path -Path $_ -PathType Leaf })]
[string]$Path,        

[Parameter(Mandatory=$true, Position = 1)]
[int]$Bytes,
[ValidateSet('ByteArray', 'HexString', 'Base64')]
[string]$As = 'ByteArray'
)
try {
$stream = [System.IO.File]::OpenRead($Path)
$length = [math]::Min([math]::Abs($Bytes), $stream.Length)
$buffer = [byte[]]::new($length)
$null   = $stream.Read($buffer, 0, $length)
switch ($As) {
'HexString' { ($buffer | ForEach-Object { "{0:x2}" -f $_ }) -join '' ; break }
'Base64'    { [Convert]::ToBase64String($buffer) ; break }
default     { ,$buffer }
}
}
catch { throw }
finally { $stream.Dispose() }
}
function Read-LastBytes {
param (
[Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 0)]
[Alias('FullName', 'FilePath')]
[ValidateScript({ Test-Path -Path $_ -PathType Leaf })]
[string]$Path,        

[Parameter(Mandatory=$true, Position = 1)]
[int]$Bytes,
[ValidateSet('ByteArray', 'HexString', 'Base64')]
[string]$As = 'ByteArray'
)
try {
$stream = [System.IO.File]::OpenRead($Path)
$length = [math]::Min([math]::Abs($Bytes), $stream.Length)
$null   = $stream.Seek(-$length, 'End')
$buffer = for ($i = 0; $i -lt $length; $i++) { $stream.ReadByte() }
switch ($As) {
'HexString' { ($buffer | ForEach-Object { "{0:x2}" -f $_ }) -join '' ; break }
'Base64'    { [Convert]::ToBase64String($buffer) ; break }
default     { ,[Byte[]]$buffer }
}
}
catch { throw }
finally { $stream.Dispose() }
}

然后,您可以根据它计算哈希值,并根据自己的喜好进行格式化。

组合是可能的,如

$begin = Read-FirstBytes -Path 'D:Testsomefile.dat' -Bytes 50    # take the first 50 bytes
$end   = Read-LastBytes -Path 'D:Testsomefile.dat' -Bytes 1000   # and the last 1000 bytes
$Algorithm = 'MD5'
$hash  = [Security.Cryptography.HashAlgorithm]::Create($Algorithm)
$hashValue = $hash.ComputeHash($begin + $end)
($hashValue  | ForEach-Object { "{0:x2}" -f $_ }) -join ''

我相信这将是使用System.IO.BinaryReader读取文件最后字节的一种更有效的方式。您可以将此函数与现有函数组合,它可以读取所有字节,最后一个n字节(-Last(或第一个n字节(-First(。

function Read-Bytes {
[cmdletbinding(DefaultParameterSetName = 'Path')]
param(
[parameter(
Mandatory,
ValueFromPipelineByPropertyName,
ParameterSetName = 'Path',
Position = 0
)][alias('FullName')]
[ValidateScript({ 
if(Test-Path $_ -PathType Leaf)
{
return $true
}
throw 'Invalid File Path'
})]
[System.IO.FileInfo]$Path,
[parameter(
HelpMessage = 'Specifies the number of Bytes from the beginning of a file.',
ParameterSetName = 'FirstBytes',
Position = 1
)]
[int64]$First,
[parameter(
HelpMessage = 'Specifies the number of Bytes from the end of a file.',
ParameterSetName = 'LastBytes',
Position = 1
)]
[int64]$Last
)
process
{
try
{
$reader = [System.IO.BinaryReader]::new(   
[System.IO.File]::Open(
$Path.FullName,
[system.IO.FileMode]::Open,
[System.IO.FileAccess]::Read
)
)
$stream = $reader.BaseStream

$length = (
$stream.Length, $First
)[[int]($First -lt $stream.Length -and $First)]
$stream.Position = (
0, ($length - $Last)
)[[int]($length -gt $Last -and $Last)]

$bytes = while($stream.Position -ne $length)
{
$stream.ReadByte()
}
[pscustomobject]@{
FilePath = $Path.FullName
Length = $length
Bytes = $bytes
}
}
catch
{
Write-Warning $_.Exception.Message
}
finally
{
$reader.Close()
$reader.Dispose()
}
}
}

用法

  • Get-ChildItem . -File | Read-Bytes -Last 100:读取当前文件夹中所有文件的最后一个100字节。如果-Last参数超过文件长度,它将读取整个文件
  • Get-ChildItem . -File | Read-Bytes -First 100:读取当前文件夹中所有文件的第一个100字节。如果-First参数超过文件长度,它将读取整个文件
  • Read-Bytes -Path path/to/file.ext:读取file.ext的所有字节

输出

返回属性为FilePathLengthBytes的对象。

FilePath                            Length Bytes
--------                            ------ -----
/home/user/Documents/test/......        14 {73, 32, 119, 111…}
/home/user/Documents/test/......         0 
/home/user/Documents/test/......         0 
/home/user/Documents/test/......         0 
/home/user/Documents/test/......       116 {111, 109, 101, 95…}
/home/user/Documents/test/......     17963 {50, 101, 101, 53…}
/home/user/Documents/test/......      3617 {105, 32, 110, 111…}
/home/user/Documents/test/......       638 {101, 109, 112, 116…}
/home/user/Documents/test/......         0 
/home/user/Documents/test/......        36 {65, 99, 114, 101…}
/home/user/Documents/test/......       735 {117, 112, 46, 79…}
/home/user/Documents/test/......      1857 {108, 111, 115, 101…}
/home/user/Documents/test/......        77 {79, 80, 69, 78…}

最新更新