Продолжаем повествование о процессе разработки функционального аналога FCIV.
Как я уже упоминал, самая первая и основная функциональная часть – функция подсчёта хешей. Я уже делал скрипт, который считает хеши: Полезная безделушка Hash SHA1 на PowerShell, который и стал основой моего скрипта. Однако, по одной причине саму считалку пришлось обрезать. Если посмотреть справку по FCIV (http://support.microsoft.com/kb/841290), то он не записывает в БД реальный хеш файла, а пропускает его ещё через Base64 кодировку и закодированный хеш уже записывается в XML файл. В .NET уже есть стандартный кодировщик Base64, который на входе принимает массив байтов. Если посмотреть на указанный пост из моего блога, то там я приводил вывод объекта $hasher, который как раз и является байтовым массивом, который мы потом преобразовывали в строку. Здесь же преобразовывать в строку ничего не нужно, а сразу подаём этот массив в конвертер. Вот как выглядит функция подсчёта хеша:
function _hashbytes_ ($type, $file) { switch ($type) { "sha1" {$hasher = [System.Security.Cryptography.SHA1]::Create()} "md5" {$hasher = [System.Security.Cryptography.MD5]::Create()} } $inputStream = New-Object System.IO.StreamReader ($file) $hashBytes = $hasher.ComputeHash($inputStream.BaseStream) $inputStream.Close() $hashBytes }
в качестве аргументов мы принимаем тип хеша – MD5 или SHA1 и сам файл:
[↓] [vPodans] _hashbytes_ sha1 wsr.txt 7 11 122 247 126 249 232 24 193 38 194 124 237 202 26 183 178 64 173 45 [↓] [vPodans] [System.Convert]::ToBase64String($(_hashbytes_ sha1 wsr.txt)) Bwt693756BjBJsJ87coat7JArS0= [↓] [vPodans] [System.Convert]::ToBase64String($(_hashbytes_ md5 wsr.txt)) kshzMfHPdtuXdL6l/vWwPA== [↓] [vPodans]
я подсчитал SHA1 хеш для текстового файла и на выходе получил такой же по структуре массив байтов, как и в предыдущей статье. И далее я этот массив подал в качестве аргумента для конвертера Base64 строки (статический метод ToBase64String класса System.Convert), а на выходе уже получил правильную Base64 строку. Именно эту строку FCIV записывает в файл. То же самое будем делать и мы. Кстати говоря. обратное преобразование делается через тот же класс System.Convert, но с использованием метода FromBase64String:
[↓] [vPodans] [System.Convert]::FromBase64String("Bwt693756BjBJsJ87coat7JArS0=") 7 11 122 247 126 249 232 24 193 38 194 124 237 202 26 183 178 64 173 45 [↓] [vPodans] -join ([System.Convert]::FromBase64String("Bwt693756BjBJsJ87coat7JArS0=") | %{"{0:X2}" -f $_}) 070B7AF77EF9E818C126C27CEDCA1AB7B240AD2D [↓] [vPodans]
как видите, массив байтов получился точно такой же. Второй строкой я показал, как можно собрать строку самого хеша, которую показывают различные программы подсчёта хешей. Как можно заметить все числа в массиве меньше, чем 256, т.е. не превышают значения 28, что является 1 байтом в двоичной форме. В цикле Foreach-Object я преобразовываю каждый байт в HEX и собираю в единую строку оператором –Join, который доступен только в PowerShell V2. Но можно собрать и через [System.String]::Join() для PowerShell 1.0.
Если посмотреть в статью по FCIV, то увидим, что он использует свою нестандартную XML схему:
<?xml version="1.0" encoding="utf-8" ?> <FCIV> <FILE_ENTRY> <name>wsr.txt</name> <SHA1>Bwt693756BjBJsJ87coat7JArS0=</SHA1> </FILE_ENTRY> <FILE_ENTRY> <name>0</name> <SHA1>xd/Rh0JyuW/I6sxPzY4sULyjS38=</SHA1> </FILE_ENTRY> </FCIV>
По этой причине мы лишаемся возможности использования Export-CliXML и подобных команд, а нужно собирать вручную.
Внимание: хоть FCIV и непривередлив к дополнительным полям в XML, но он начинает сильно ругаться, если поле Name написано с большой буквы. Его нужно заполнять так, как он есть в экземпляре, т.е. все маленькие буквы.
Объект, который будет хранить все данные о файле будет вот такой:
$object = "" | Select Name, Size, TimeStamp, SHA1, MD5
Т.е. мы в XML будем писать имя файла, его размер в байтах, время и дату последней модификации файла и хеши SHA1 и MD5. Если при создании БД мы будем использовать только один хеш, то второй будет просто пустым. Для сборки XML нам потребуется потребуется функция. Эту схему можно разложить на 3 составные части:
Вот такая функция у меня получилась:
function _toxml_ { # в Begin создаём заголовок XML и открывающийся тег <FCIV> Begin { $xmlstring += "<?xml version=`"1.0`" ?>`n <FCIV>`n" } # в Process будут по одному поступать объекты, которые описывают файл. Process { # для каждого файла у нас будет один тег <FILE_ENTRY> $xmlstring += " <FILE_ENTRY>`n" # чтобы вручную не создавать и не заполнять вложенные теги мы простым foreach # перечисляем теги, какие у нас будут в XML и за счёт переменных автоматом # создаём их в XML и заполняем их данными. Для этого свойства объектов должны # называться так же, как и теги. foreach ($child in ("name", "Size", "TimeStamp", "SHA1", "MD5")) { $xmlstring += " <$child>$($_.$child)</$child>`n" } # когда текущий объект обработан, закрываем тег и ждём следующий объект файла $xmlstring += " </FILE_ENTRY>`n" } # когда объекты закончились, закрываем первый тег <FCIV> и подаём полученный XML дальше End { $xmlstring += "</FCIV>`n" $xmlstring } }
в ходе испытаний нарвался на одну засаду. В XML существуют некоторые символы как ‘&’, ‘<’, ‘>’ и другие (полный список перечислен тут: http://www.hdfgroup.org/HDF5/XML/xml_escape_chars.htm), которые нужно эксейпить, поскольку являются метасимволами в XML. Эти символы (в частности одинарная кавычка и амперсанд) могут присутствовать в имени файла. Погуляв по сайту MSDN нашёл выход. Выход заключается в использовании метода Escape класса System.SecurityElement. Поэтому после того как массив объектов (которые описывают каждый файл) будет собран его нужно пропустить через фильтр и только после этого собирать XML вышеприведённой функцией. Вот как выглядит фильтр:
filter _escxml_ { $_.name = [Security.SecurityElement]::Escape($_.name) $_ }
Напоминаю, что мы должны эскейпить только свойство Name нашего объекта.
Вот мы уже научились правильно создавать XML файл, который будет полностью совместим со схемой, которая используется в самом FCIV. Теперь у нас возникает задача чтения этого XML и преобразования в массив объектов. Сначала я долго горевал, плакал и бился головой об стену, что нельзя использовать Import-CliXML, но преобразование типов решило задачу ничуть не сложнее:
function _fromxml_ ($xml) { $sum = ([xml]$(gc -LiteralPath $xml)).FCIV.FILE_ENTRY | %{$_ | Select Name, Size, TimeStamp, SHA1, MD5} $sum }
сначала читаем файл и преобразовываем его в типа данных XML, опускаем теги FCIV и FILE_ENTRY и в цикле разбираем вложенные теги и собираем объект. Получилось достаточно просто. На сегодня, я думаю, что хватит, продолжение следует.
Comments: