Бинарный grep Powershell - PullRequest
       47

Бинарный grep Powershell

2 голосов
/ 16 июня 2020

Есть ли способ определить, содержит ли указанный файл указанный массив байтов (в любой позиции) в PowerShell?

Что-то вроде:

fgrep --binary-files=binary "$data" "$filepath"

Конечно, я могу написать наивная реализация:

function posOfArrayWithinArray {
    param ([byte[]] $arrayA, [byte[]]$arrayB)
    if ($arrayB.Length -ge $arrayA.Length) {
        foreach ($pos in 0..($arrayB.Length - $arrayA.Length)) {
            if ([System.Linq.Enumerable]::SequenceEqual(
                $arrayA,
                [System.Linq.Enumerable]::Skip($arrayB, $pos).Take($arrayA.Length)
            )) {return $pos}
        }
    }
    -1
}

function posOfArrayWithinFile {
    param ([byte[]] $array, [string]$filepath)
    posOfArrayWithinArray $array (Get-Content $filepath -Raw -AsByteStream)
}

// They return position or -1, but simple $false/$true are also enough for me.

- но это чрезвычайно медленно.

Ответы [ 4 ]

1 голос
/ 22 июня 2020

Извините, за дополнительный ответ. Это необычно, но меня заинтриговал универсальный вопрос, и подход и информация моего первоначального ответа « с использованием -Like » совершенно разные. Кстати, если вы ищете положительный ответ на вопрос « Я считаю, что он должен существовать в. NET», чтобы принять ответ, вероятно, этого не произойдет, такой же квест существует для поиска StackOverflow в сочетании с C#, .Net или Linq. В любом случае, тот факт, что никто не может найти команду single предположительно .Net для этого до сих пор, вполне понятно, что вместо этого предлагаются несколько решений semi-.Net, но я считаю, что это вызовет некоторые нежелательные накладные расходы для универсальной функции. Предполагая, что вы ByteArray (массив байтов, в котором выполняется поиск), и SearchArray (массив байтов для поиска) полностью случайны. Существует только 1/256 шанс, что каждый байт в ByteArray будет соответствовать первому байту SearchArray . В этом случае вам не нужно искать дальше, и если он совпадает с , вероятность того, что второй байт также совпадает, составляет 1/256 2 , et c. Это означает, что внутренний l oop будет работать только в 1,004 раз больше, чем внешний l oop. Другими словами, производительность всего, что находится за пределами внутреннего l oop (но во внешнем l oop), почти так же важно, как и то, что находится во внутреннем l oop! Обратите внимание, что это также означает, что вероятность того, что случайная последовательность размером 500 Кбайт существует в случайной последовательности 100 Мбайт, практически равна нулю. (Итак, насколько на самом деле случайны заданные вами двоичные последовательности? Если они далеки от случайности, я думаю, вам нужно добавить еще несколько деталей к своему вопросу). Худшим сценарием для моего предположения будет ByteArray , состоящий из тех же байтов (например, 0, 0, 0, ..., 0, 0, 0), и SearchArray из тех же байтов, заканчивающийся другим байтом (например, 0, 0, 0, ..., 0, 0, 1).

На основании этого он снова показывает (я также доказал это в некоторых других ответах), что собственные команды PowerShell не так уж плохи и, возможно, даже могут превзойти . Net / Linq команд в некоторых случаях. В моем тестировании функция Find-Bytes ниже примерно на 20% и вдвое быстрее, чем функция в вашем вопросе:

Find-Bytes

Возвращает индекс, где -Search байт последовательность находится в последовательности байт -Bytes. Если последовательность поиска не найдена, возвращается $Null ([System.Management.Automation.Internal.AutomationNull]::Value).

Параметры

-Bytes Массив байтов для поиска

-Search Массив байтов для поиска

-Start Определяет, где начать поиск в последовательности Bytes (по умолчанию: 0)

-All По умолчанию будет возвращен только первый найденный индекс. Используйте переключатель -All, чтобы получить оставшиеся индексы любых других найденных последовательностей поиска.

Function Find-Bytes([byte[]]$Bytes, [byte[]]$Search, [int]$Start, [Switch]$All) {
    For ($Index = $Start; $Index -le $Bytes.Length - $Search.Length ; $Index++) {
        For ($i = 0; $i -lt $Search.Length -and $Bytes[$Index + $i] -eq $Search[$i]; $i++) {}
        If ($i -ge $Search.Length) { 
            $Index
            If (!$All) { Return }
        } 
    }
}

Пример использования:

$a = [byte[]]("the quick brown fox jumps over the lazy dog".ToCharArray())
$b = [byte[]]("the".ToCharArray())

Find-Bytes -all $a $b
0
31

Benchmark Обратите внимание, что вам следует открыть новый сеанс PowerShell, чтобы правильно протестировать это, поскольку Linq использует большой кеш, который должным образом не подходит для вашего варианта использования.

$a = [byte[]](&{ foreach ($i in (0..500Kb)) { Get-Random -Maximum 256 } })
$b = [byte[]](&{ foreach ($i in (0..500))   { Get-Random -Maximum 256 } })

Measure-Command {
    $y = Find-Bytes $a $b
}

Measure-Command {
    $x = posOfArrayWithinArray $b $a
}
1 голос
/ 18 июня 2020

Просто формализирую свои комментарии и соглашаюсь с вашим комментарием:

Мне вообще не нравится идея преобразования байтовых последовательностей в символьные (мне бы лучше иметь функцию сопоставления байтовых (или других) последовательностей как они есть), среди решений, подразумевающих преобразование в символьные строки, это кажется одним из самых быстрых

Производительность

Манипуляции со строками обычно дороги, но повторная инициализация Вызов LINQ , по-видимому, также довольно дорог. Я предполагаю, что вы можете предположить, что собственные алгоритмы для строкового представления PowerShell и методы (операторы), такие как -Like, тем временем полностью сжаты.

Память

В сторону из-за некоторых обнаруженных недостатков производительности существует недостаток памяти, поскольку преобразование каждого байта в представление десятичной строки. В целевом решении каждый байт займет в среднем 2.57 байт (в зависимости от количества десятичных цифр каждого байта: (1 * 10 / 256) + (2 * 90 /256) + (3 * 156 / 256)). Кроме того, вам понадобится дополнительный байт для разделения представлений numeri c. В общей сложности это увеличит последовательность примерно в 3.57 раз!.
Вы можете рассмотреть возможность экономии байтов, например, преобразовав их в шестнадцатеричные и / или комбинируя разделитель, но это вероятно, снова приведет к дорогостоящему преобразованию.

Легко

В любом случае, простой способ, вероятно, по-прежнему наиболее эффективен.
Это сводится к следующему упрощенному синтаксису:

" $Sequence " -Like "* $SubSequence *" # $True if $Sequence contains $SubSequence

(где $Sequence и $SubSequence - двоичные массивы типа: [Byte[]])

Примечание 1: пробелы вокруг переменных важны. Это предотвратит ложное срабатывание в случае, если представление 1 (или 2) байта di git перекрывается с представлением 2 (или 3) байта di git. Например: 123 59 74 содержит 23 59 7 в строковом представлении, но не в фактических байтах.

Примечание 2: Этот синтаксис сообщит вам только , содержит ли $arrayA $arrayB ($True или $False). Нет никакой подсказки, где $arrayB на самом деле находится в $arrayA. Если вам нужно это знать или, например, вы хотите заменить $arrayB чем-то другим, обратитесь к этому ответу: Методы шестнадцатеричного редактирования двоичных файлов через PowerShell .

1 голос
/ 16 июня 2020

Приведенный ниже код может оказаться быстрее, но вам придется проверить это на своих двоичных файлах:

function Get-BinaryText {
    # converts the bytes of a file to a string that has a
    # 1-to-1 mapping back to the file's original bytes. 
    # Useful for performing binary regular expressions.
    Param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateScript( { Test-Path $_ -PathType Leaf } )]
        [Alias('FullName','FilePath')]
        [string]$Path
    )

    $Stream = New-Object System.IO.FileStream -ArgumentList $Path, 'Open', 'Read'

    # Note: Codepage 28591 returns a 1-to-1 char to byte mapping
    $Encoding     = [Text.Encoding]::GetEncoding(28591)
    $StreamReader = New-Object System.IO.StreamReader -ArgumentList $Stream, $Encoding
    $BinaryText   = $StreamReader.ReadToEnd()

    $Stream.Dispose()
    $StreamReader.Dispose()

    return $BinaryText
}

# enter the byte array to search for here
# for demo, I'll use 'SearchMe' in bytes
[byte[]]$searchArray = 83,101,97,114,99,104,77,101

# create a regex from the $searchArray bytes
# 'SearchMe' --> '\x53\x65\x61\x72\x63\x68\x4D\x65'
$searchString = ($searchArray | ForEach-Object { '\x{0:X2}' -f $_ }) -join ''
$regex = [regex]$searchString

# read the file as binary string
$binString = Get-BinaryText -Path 'D:\test.bin'

# use regex to return the 0-based starting position of the search string
# return -1 if not found
$found = $regex.Match($binString)
if ($found.Success) { $found.Index } else { -1}
0 голосов
/ 16 июня 2020

Я определил, что следующее может работать как обходной путь:

(Get-Content $filepath -Raw -Encoding 28591).IndexOf($fragment)

- т.е. любые байты могут быть успешно сопоставлены PowerShell string s (фактически, NET System.String s) когда мы указываем бинарно-безопасную кодировку . Конечно, нам нужно использовать ту же кодировку как для файла, так и для фрагмента, и кодировка должна быть действительно бинарно-безопасной (например, 1250, 1000 и 28591 подходят, но различные виды Unicode (включая значение по умолчанию UTF-8 без спецификации) нет, потому что они преобразуют любую некорректно сформированную кодовую единицу в тот же символ замены (U + FFFD) ). Спасибо Тео за разъяснения.

На более ранней версии PowerShell вы можете использовать:

[System.Text.Encoding]::GetEncoding(28591).
    GetString([System.IO.File]::ReadAllBytes($filepath)).
    IndexOf($fragment)

К сожалению, я не нашел способа универсального сопоставления последовательностей (т.е. общий метод сопоставления последовательностей с любым типом элемента: целое число, объект и т. д. c). Я считаю, что он должен существовать в NET (особенно существует конкретная реализация для последовательностей символов). Надеюсь, кто-нибудь это предложит.

...