Contents of this directory is archived and no longer updated.

Продолжаем повествование о процессе разработки функционального аналога FCIV.

Подсчёт хешей и кодирование в Base64 String

Как я уже упоминал, самая первая и основная функциональная часть – функция подсчёта хешей. Я уже делал скрипт, который считает хеши: Полезная безделушка 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 файла

Если посмотреть в статью по 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 составные части:

  1. заголовок XML и открывающийся тег <FCIV>. Это присуствует только в начале XML файла и всё. Этот тег и заголовок больше нигде в XML не встречается. Поэтому header и открывающийся тег нужно поместить в скриптоблок Begin функции.
  2. А вот теги <FILE_ENTRY> они повторяются для каждой записи. Следовательно этот тег будем заполнять в конструкции Process функции. Т.е. в каждой итерации Process будет создаваться новый тег <FILE_ENTRY>. Но внутри этого тега есть ещё вложенные теги, которые описывают наш файл. Чтобы решить эту задачу мы должны внутри конструкции Process (которая по сути будет являться циклом) написать ещё один цикл, который будет разбирать входящий объект и данные из его свойств записывать во вложенные теги.
  3. В конце XML файла будет закрывающийся тег </FCIV>, его будем помещать в скриптоблок End нашей функции.

Вот такая функция у меня получилась:

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 нашего объекта.

Преобразование FCIV-совместимого XML в массив объектов

Вот мы уже научились правильно создавать 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 и в цикле разбираем вложенные теги и собираем объект. Получилось достаточно просто. На сегодня, я думаю, что хватит, продолжение следует.


Share this article:

Comments:

Comments are closed.