Примечание: данный пост перепечатан в связи с закрытием бложиков на spaces.live.com, как имеющий какую-то ценность для автора и/или читателей.
В сети есть уйма маленьких консольных и графических утилит, которые позволяют считать хэши MD5/SHA1/SHA256/etc для файлов. Это иногда очень полезно. Расслабляясь после ряда статей по управлению Share Permissions решил от нечего делать написать свою утилитку полностью на PowerShell, которая интегрируется с контекстным меню Windows Explorer. Ну и самописный инсталлятор на PowerShell, который всю эту интеграцию и выполнит. Конечно же, это будет выглядеть продуктом, который выполнен на коленке в поезде, но тем не менее, это неплохой пример для начала работы с GUI в PowerShell. Ну что-ж, давайте разбираться.
У нас в Windows Explorer при правом клике на файле (только файле) будет контекстное меню вот такого вида:
Делается это очень просто, мы просто создадим ветку в реестре и зададим ей значение следующего вида:
Ключ реестра:
HKEY_CLASSES_ROOT\*\Shell\Hash SHA1\command
Значение элемента Default:
C:\WINDOWS\system32\WindowsPowerShell\v1.0\powershell.exe\" -nologo -noninteractive -noprofile -command get-location | hash.ps1 '%1'
Данная команда запускает PowerShell без логотипа и без профиля. Ключ -command показывает какие команды следует исполнить. В нашем случае это будет вызов скрипта hash.ps1 с аргументом в виде переменной %1 . Данная переменная будет содержать путь к файлу и использоваться как аргумент для скрипта. Как я уже рассказывал раньше про передачу аргументов из командной строки в скрипт, то при использовании пробелов в аргументах (а путь к файлу очень даже может содержать пробелы), то его нужно заключить в одинарные кавычки. Теперь давайте вспомним, как создавать путь в реестре и присваивать им занчения из PowerShell:
New-Item -Path "Registry::HKLM\Software\Classes\*\Shell\Hash SHA1\command" -Force
В конце строки я использовал ключ -Force, чтобы путь создался полностью, даже если реальный путь обрывается посередине. Без этого ключа PowerShell вернёт ошибку, что ключ реестра не существует и не сможет создать последующие ключи. Теперь пора внести запись в реестр. Выше я писал ключ реестра, который находится в ветке HKEY_CLASSES_ROOT, а создаю ключ в ветке HKLM (HKEY_LOCAL_MACHINE). Всё очень просто. Тут нужно понимать структуру реестра в Windows NT системах. На самом деле на диске физически хранятся лишь HKLM и множество кустов HKCU (по одному кусту на каждого пользователя), которые в итоге составляют один общий раздел HKU. Остальные же ветки реестра, которые мы видим в классическом реестре являются лишь ссылками на соответствующие ключи этих двух основных файлов кустов реестра. Для удобочитаемости кода я длинные значения присвоил переменным и в основном коде использую уже переменные:
$RegPath = "Registry::HKLM\Software\Classes\*\Shell\Hash SHA1\command" $RegValue = "C:\WINDOWS\system32\WindowsPowerShell\v1.0\powershell.exe -nologo -noninteractive -noprofile -command C:\hash.ps1 '%1'" New-Item -Path $RegPath -Force New-ItemProperty -Path $RegPath -Name "(Default)" -Value $RegValue
После выполнения этих команд мы получим в контекстном меню элемент Hash SHA1. А теперь уже можно начинать писать основной код, который будет считать хэш файла. Мне из хэш-методов больше нравится SHA1 (кому-то может и MD5 или ещё что), поэтому писать буду применительно под него (что характерно от используемого хэш-метода код изменится только в названии класса и всё). Для SHA1 в .NET есть класс System.Security.Cryptography.SHA1. Давайте создадим новый инстанс с этим классом. Если посмотреть список членов класса SHA1, то найдём там Create. В принципе, этого хватает вполне:
$hasher = [System.Security.Cryptography.SHA1]::Create()
Хорошо, инстанс создали, что дальше? Дальше нужно подобрать метод для данного класса. Из списка методов нам для решения поставленной задачи пригодится метод ComputeHash(Stream). Stream в данном случае означает, что будет создаваться поток байтов и пропускаться через метод ComputeHash для вычисления хэша. Значит, нам нужно создать поток байтов. В описании метода ComputeHash в описании параметров указано как создавать поток байтов, а именно с использованием System.IO.Stream. Давайте создадим объект с этим классом и в качестве параметра ему передадим путь к файлу. Путь файла сожержится в переменной $file:
$inputStream = New-Object System.IO.StreamReader($file)
теперь файл будет преобразован в виде потока байтов. Ну что, теперь можно этот поток байтов можно передать в метод ComputeHash класса System.Security.Cryptography.SHA1. Сказано - сделано:
$hashBytes = $hasher.ComputeHash($inputStream.BaseStream)
Теперь завершаем поток байтов:
$inputStream.Close()
В принципе, если вместо переменной $file подставить путь к файлу (пока без вызова из контекстного меню, а просто из GUI, то мы увидим результат работы скрипта:
[C:\] $hasher = [System.Security.Cryptography.SHA1]::Create() [C:\] $inputStream = New-Object System.IO.StreamReader ("C:\windows\Notepad.exe") [C:\] $hashBytes = $hasher.ComputeHash($inputStream.BaseStream) [C:\] $inputStream.Close() [C:\] $hashbytes 125 108 181 162 72 110 38 219 208 171 69 138 233 234 9 49 57 116 174 96
На выходе мы получили массив посчитанных байтов. Чтобы собрать этот массив в строку мы воспользуемся классом System.Text.StringBuilder. Создадим объект с этим классом:
$builder = New-Object System.Text.StringBuilder
Теперь посмотрим, какие методы у него есть - StringBuilder Members. Здесь нас заинтересует метод Append Method (Byte). Т.к. у нас массив байтов, то нужно данный метод использовать в цикле foreach-object и необходимо его привести в HEX формат:
$hashBytes | Foreach-Object {[void]$builder.Append($_.ToString("X2"))} $output = New-Object PsObject $output | Add-Member NoteProperty HashValue ([string]$builder.ToString())
В первой строке мы только сложили массив байтов в строку. Теперь нужно весь этот результат куда-то разместить. Для этого второй строкой мы создаём собственный абстрактный объект и добавляем ему свойство HashValue (можно и на своё усмотрение название придумать) и присваиваем ему значение. Давайте посмотрим, что после исполнения кода мы будем иметь в переменной $output:
[C:\] $builder = New-Object System.Text.StringBuilder [C:\] $hashBytes | Foreach-Object {[void]$builder.Append($_.ToString("X2"))} [C:\] $output = New-Object PsObject [C:\] $output | Add-Member NoteProperty HashValue ([string]$builder.ToString()) [C:\] $output.hashvalue 7D6CB5A2486E26DBD0AB458AE9EA09313974AE60
Жирным я выделил содержимое переменной, а именно - итоговый хэш-код файла notepad.exe по системе SHA1! Отлично, сам конструктив уже готов (код, который для файлов считает хэш). Теперь немного GUI. Объявляем часть GUI:
[void][reflection.assembly]::LoadWithPartialName("System.Windows.Forms")
В принципе, эта команда используется всегда перед использованием GUI элементов. Давайте создадим простенькую небольшую форму и зададим ей какие-нибудь свойства:
$form = new-object Windows.Forms.Form $form.Text = "$file SHA1 Hash" $form.width = 314 $form.height = 110 $form.startposition = "CenterScreen" $form.MaximizeBox = 0 $form.MinimizeBox = 0 $form.FormBorderStyle = "FixedSingle" $form.autosize = 0
Первой строкой мы создали объект Windows.Forms.Form а в последующих строках заполнил свойства этого объекта. Касательно размеров я не действовал наугад, а создал примерное окно в Visual Studio с нужными размерами. И уже из Visual Studio выбирал нужные значения для свойств объекта. Напрямую из PowerShell не очень удобно рассчитывать размеры и компоновать элементы внутри формы. Я считаю, что тут комментарии излишни по коду, т.к. значения свойств понятны из самих названий. Например, свойство Text для формы определяет заголовок формы. У меня в заголовке динамически подставляется путь из переменной $file и добавил текст SHA1 Hash. На форме у меня будет ещё два элемента - Метка (Label) и кнопка (Button). Начнём с метки:
$label = new-object Windows.Forms.label $label.Location = New-Object System.Drawing.Size(12,18) $label.autosize = 1 $label.text = $output.hashvalue
местоположение метки относительно верхнего левого угла формы я взял из Visual Studio. В свойство Text я вписал содержимое переменной $output.hashvalue, которая динамически для каждого файла будет содержать SHA1 хэш. От созерцания хэша легче мне не станет, поэтому его нужно как-то изъять из метки простым способом (метка не поддерживает выделение и копирование текста. Даже при использовании элемента EditBox пришлось бы выделять текст и вручную копировать, что не сильно удобно). Для этого я на форме разместил кнопку, которая при нажатии будет копировать содержимое метки в буфер обмена и закрывать форму:
$button = new-object Windows.Forms.button $button.Location = New-Object System.Drawing.Size(12,39) $button.text = "Скопировать в буфер обмена" $button.width = 274 $button.height = 23 $button.add_click({ $label.text | clip; $form.close() })
Примечение: вот тут мне пришлось отступиться от чистого кода на .NET, т.к. PowerShell не умеет нативно использовать буфер обмена и для этого нужно писать собственную функцию. В принципе, она уже есть в PowerShell Community Extensions и позволяет как записывать в буфер обмена, так и извлекать оттуда данные. У меня же нету задачи по извлечению данных из буфера обмена, поэтому я схитрил и использовал системную утилиту clip.exe, которая без расходования лишних строк позволяет скопировать данные в буфер обмена.
Вот и всё, больше ничего на моей форме не будет, поэтому можно активировать форму и объявлять в ней мои элементы:
$form.controls.add($label) $form.controls.add($button) $form.Add_Shown({$form.Activate()}) $form.ShowDialog()
Если создать нужные ключи реестра и сохранить этот код в файл, то при клике на контекстное меню Hash SHA1 появится вот такое окошко:
Конечно же тут есть один недостаток - на заднем плане нашей формы будет маячить чёрная консоль PowerShell. К сожалению я пока не придумал, как сделать по-тихому, чтобы на экране была бы только форма без окна консоли. Если придумаю, то обязательно напишу или, если у вас есть уже готовые наработки или идеи, оставьте их в комментариях. Теперь настало время подвести итог и написать конечный PS1 файл:
######################################################## # Hash SHA1_install.ps1 # Version 1.0 # # SHA1 hash calculator for a file from context menu # # Vadims Podans (c) 2008 # http://vpodans.spaces.live.com/ ######################################################## $FilePath = Read-Host "Укажите путь для размещения Hash.PS1" if (Test-Path $FilePath) { $FilePath = $FilePath + "\" + "hash.ps1" $RegPath = "Registry::HKLM\Software\Classes\*\Shell\Hash SHA1\command" $RegValue = "C:\WINDOWS\system32\WindowsPowerShell\v1.0\powershell.exe -nologo -noninteractive -noprofile -command $FilePath '%1'" New-Item -Path $RegPath -Force -ErrorAction SilentlyContinue if (Test-Path -LiteralPath $RegPath) { New-ItemProperty -LiteralPath $RegPath -Name "(Default)" -Value $RegValue New-Item -ItemType file -Path $FilePath -Force -ErrorAction SilentlyContinue if (Test-Path $FilePath) { $exefile = 'param ($file) $hasher = [System.Security.Cryptography.SHA1]::Create() $inputStream = New-Object System.IO.StreamReader ($file) $hashBytes = $hasher.ComputeHash($inputStream.BaseStream) $inputStream.Close() $builder = New-Object System.Text.StringBuilder $hashBytes | Foreach-Object { [void] $builder.Append($_.ToString("X2")) } $output = New-Object PsObject $output | Add-Member NoteProperty HashValue ([string]$builder.ToString()) # creating a form [void][reflection.assembly]::LoadWithPartialName("System.Windows.Forms") $form = new-object Windows.Forms.Form $form.Text = "$file SHA1 Hash" $form.width = 314 $form.height = 110 $form.startposition = "CenterScreen" $form.MaximizeBox = 0 $form.MinimizeBox = 0 $form.FormBorderStyle = "FixedSingle" $form.autosize = 0 # add a label $label = new-object Windows.Forms.label $label.Location = New-Object System.Drawing.Size(12,18) $label.autosize = 1 $label.text = $output.hashvalue # add a button $button = new-object Windows.Forms.button $button.Location = New-Object System.Drawing.Size(12,39) $button.text = "Скопировать в буфер обмена" $button.width = 274 $button.height = 23 $button.add_click({$label.text | clip; $form.close()}) # showing the form $form.controls.add($label) $form.controls.add($button) $form.Add_Shown({$form.Activate()}) $form.ShowDialog()' Set-Content -Path $FilePath -Value $exefile } else {Write-Warning "Не удалось создать файл по пути: $FilePath. Операция прервана."} } else {Write-Warning "Не удалось создать ключ реестра. Возможно у вас нету прав на запись в раздел HKLM. Операция прервана."} }
В конечном итоге я обвязал код некоторыми проверками, которые проверяют возможность записи в реестр (для работы скрипта нужно иметь право записи в HKLM) и в указанную пользователем папку. Если какое-либо действие не удастся, то код выдаст соответствующее сообщение об ошибке и прервёт дальнейшую операцию. Если обе операции записи прошли успешно, то происходит копирование рабочего кода (содержимое переменной $exefile), который уже не содержит инсталляционных и проверочных строк кода. Вот как бы и всё на сегодня :-) .
Comments: