1 (изменено: lifeflame, 2020-10-19 01:25:52)

Тема: JScript: "WindowsInstaller" и "SAPI" - чтение и запись бинарных файлов

Предлагаю на примерах рассмотреть особенности работы ActiveX-объектов "WindowsInstaller.Installer" и "SAPI.SpFileStream" при чтении и записи бинарных файлов, которые я использовал в HTML приложении "Resource Extractor".

Данный материал рассчитан в том числе и на тех, кто сталкивается с этим впервые, поэтому он достаточно линеен и оставлено множество комментариев с пояснениями.

При исследовании "WindowsInstaller.Installer" я сделал вывод, что он подходит для чтения бинарных файлов и возвращения истинных строковых байтов под любой локалью и на чистом JScript, что является его определенным преимуществом для данного языка, так как JScript не может принимать и изменять данные по ссылке, за исключением изменения свойств объектов, а также отдельных исключительных случаев у некоторых компонентов, относящихся больше не к языку, а непосредственно к реализации таких компонентов.

В случае с "SAPI.SpFileStream" один из параметров метода чтения файлов "Read" (это параметр "Buffer") принимает переменную по ссылке и изменяет ее, что возможно по умолчанию на VBScript, но опять же не на чистом JScript.

Но в качестве исключения, например, метод "transformNodeToObject" вернет измененный по ссылке объект на чистом JScript.

Примечание: в данном обзоре и далее будет встречаться понятие "истинные байты". Истинные байты (true bytes) - это обычные байты, которые не были изменены вмешательством посторонних объектов, таких как, например, "Scripting.FileSystemObject", который возвращает уже не "истинные строковые байты" под китайской локалью и ей подобными. В свою очередь, истинные строковые байты это не то же самое, что истинные байты (тип данных "Byte()"). Истинные строковые байты - это истинные байты, но в строковом представлении (тип данных "String").

Работа с исходным кодом.

  1. Для запуска приложения с примером скопируйте его исходный код и сохраните его в текстовом файле, сменив его расширение на "hta".

  2. Запуск приложения с примером осуществляется двойным нажатием, как и в случае с обычной программой.

Рекомендую использовать AkelPad для просмотра сохраненного исходного кода примеров с правильными отступами.

Рассмотрим подробнее на примере поведение "SAPI.SpFileStream" и "transformNodeToObject" в отношении передачи переменной по ссылке:

<script language = VBScript>

Function alert(message)                                                        'заменим "alert" из JScript на более информативный метод для анализа данных
    Dim messageType
    messageType = TypeName(message)
    If messageType = "String" Or messageType = "Byte()" Then
        InputBox "", "Message from webpage", message
    Else
        InputBox "", "Message from webpage", TypeName(message)
    End If
End Function

Function vbsSourceRead(startPosition, bytesCount)           'функция чтения байтов файла-источника (начальная позиция, количество байтов)
    source.Seek(startPosition)                                                'пропускаем определенное число байтов до достижения начальной позиции чтения
    Dim data
    source.Read data, bytesCount                                          'передаем переменную data по ссылке в метод чтения байтов
    vbsSourceRead = data                                                      'возвращаем прочитанные методом "Read" истинные байты в переменной data по ссылке
End Function

</script>

<script language = JScript>

var jsSourceRead = function(startPosition, bytesCount)      //функция с попыткой чтения байтов файла-источника (начальная позиция, количество байтов)
{
    source.Seek(startPosition);                                               //пропускаем определенное число байтов до достижения начальной позиции чтения
    var data;
    source.Read(data, bytesCount);                                       //пытаемся передать переменную data по ссылке в метод чтения байтов
    return data;                                                        //пытаемся вернуть прочитанные методом "Read" истинные байты по ссылке
},

xml = new ActiveXObject('Microsoft.XMLDOM'),
hex = xml.createElement('hex');
hex.dataType = 'bin.hex';

var bytesToHex = function(bytes)                                          //функция для конвертации байтов в их строковое шестнадцатеричное представление
{
    if(typeof bytes == 'unknown')
    {
        hex.nodeTypedValue = bytes;
        return hex.text;
    }
    return bytes;
}

var source = new ActiveXObject('SAPI.SpFileStream');       //подготовим новый объект для работы с файловой системой
source.Format.Type = 1;                                                       //используем байты-текст как смешанный тип принимаемых данных (SAFTText = 1)
source.Open('C:\\Windows\\System32\\shell32.dll');            //открываем "shell32.dll" только для чтения (SSFMOpenForRead = 0)

var data = vbsSourceRead(0, 7);                                          //читаем первые 7 истинных байтов файла "shell32.dll" на VBScript
alert(data);                                                                              //выведет первые 7 истинных байтов файла "shell32.dll" в исходном виде, но не все байты могут
                                                                                                    //быть отображены в виде текста при таком выводе
alert(bytesToHex(data));                                                        //выведет, например, "4d5a9000030000", то есть это первые 7 истинных байтов "shell32.dll" - 4d 5a 90 00 03 00 00

data = jsSourceRead(0, 7);                                                   //читаем те же первые 7 истинных байтов файла "shell32.dll" на JScript
alert(data);                                                                             //пытаемся вывести первые 7 истинных байтов файла "shell32.dll" в исходном виде, но результат Empty, то есть
                                                                                                    //чтение на JScript не вернуло данные по ссылке

var memory = new ActiveXObject('ADODB.Stream');         //подготовим новый объект для работы в оперативной памяти
memory.Open();                                                                    //откроем поток памяти на чтение и запись
memory.Type = 1;                                                                 //будем работать с байтами (adTypeBinary = 1)
data = memory.Read();                                                         //прочитаем поток памяти в виде байтов
alert(data);                                                                             //выведет Null, то есть новый поток памяти изначально ничем не заполнен

memory.Position = 0;                                                            //вернемся к началу потока памяти после завершения операции чтения
hex.text = '31323334';                                                            //заполним xml-ноду hex байтами "1234" в строковом шестнадцатеричном представлении
hex.transformNodeToObject(hex, memory);                        //попытаемся передать в метод "transformNodeToObject" xml-ноды объект memory в качестве параметра
                                                                                                    //"outputObject" по ссылке (второй параметр "stylesheet" в данном случае не так интересен)
memory.Position = 0;                                                            //вернемся к началу потока памяти после предполагаемого его заполнения методом "transformNodeToObject"
memory.Type = 2;                                                                 //будем работать с текстом (adTypeText = 2)
data = memory.ReadText();                                                  //прочитаем поток памяти в виде текста
alert(data);                                                                             //выведет "<hex dt:dt="bin.hex">31323334</hex>" - текущее представление xml-ноды hex, то есть возникла
                                                                                                    //исключительная ситуация на JScript - был изменен и заполнен данными ноды непосредственно
                                                                                                    //объект memory, переданный по ссылке

</script>

Теперь остановимся подробнее на чтении бинарных файлов при использовании "WindowsInstaller.Installer" на чистом JScript:

<script language = VBScript>

Function alert(message)                                                              'заменим "alert" из JScript на более информативный метод для анализа данных
    Dim messageType
    messageType = TypeName(message)
    If messageType = "String" Or messageType = "Byte()" Then
        InputBox "", "Message from webpage", message
    Else
        InputBox "", "Message from webpage", TypeName(message)
    End If
End Function

</script>

<script language = JScript>

var doc = GetObject('\\', 'htmlfile');
doc.write("<xml><_ xmlns:dt='urn:schemas-microsoft-com:datatypes'><_ dt:dt='bin.hex'/></_></xml>");
var xml = doc.documentElement.firstChild.children[1];
xml.preserveWhiteSpace = 1;

var hex = xml.firstChild.childNodes[0],

bytesToHex = function(bytes)                                                      //функция для конвертации байтов в их строковое шестнадцатеричное представление
{
    if(typeof bytes == 'unknown')
    {
        hex.nodeTypedValue = bytes;
        return hex.text;
    }
    return bytes;
},

sourceRead = function(bytesCount)                                           //функция последовательного чтения байтов файла-источника
                                                                                                            //(количество байтов от предыдущей позиции чтения)
{
    memory.Position = 0;                                                              //сбрасываем поток памяти в начальную позицию
    memory.Type = 2;                                                                   //будем работать с текстом (adTypeText = 2)
    memory.Charset = 'Unicode';                                                  //воспринимаем истинные строковые байты файла-источника как символы в формате Юникод
    var stringBytes = source.ReadStream(1, bytesCount, 3);      //читаем истинные строковые байты файла-источника "напрямую" (msiReadStreamDirect = 3)
    memory.WriteText(stringBytes);                                              //помещаем прочитанные методом "ReadStream" истинные строковые байты в поток памяти для их
                                                                                                            //конвертации в истинные байты
    memory.SetEOS();                                                                  //устанавливаем конец потока памяти, то есть обрезаем остатки потока памяти с предыдущего чтения
    memory.Position = 0;                                                               //вернемся к началу потока памяти после завершения операции записи
    memory.Type = 1;                                                                    //будем работать с байтами (adTypeBinary = 1)
    memory.Position = 2;                                                               //пропускаем два байта заголовка "BOM", который возвращает поток памяти после конвертации
                                                                                                            //в формате Юникод
    var bytes = memory.Read(bytesCount);                                 //обрезаем поток памяти строго по количеству прочитанных байтов, так как в случае чтения нечетного
                                                                                                            //количества байтов будет возвращен лишний нулевой байт как последствие конвертации
                                                                                                            //в формате Юникод
    return bytes;
}

var source = new ActiveXObject('WindowsInstaller.Installer').CreateRecord(1);       //подготовим новый объект для работы с файловой системой
source.SetStream(1, 'C:\\Windows\\System32\\shell32.dll');                                     //открываем "shell32.dll" как текстовый поток файловой системы только для чтения,
                                                                                                                                            //других опций не предусмотрено, последующее закрытие потока не требуется,
                                                                                                                                            //может использоваться вновь для открытия новых файлов

var memory = new ActiveXObject('ADODB.Stream');                                                //подготовим новый объект для работы в оперативной памяти
memory.Open();                                                                                                           //откроем поток памяти на чтение и запись

var data = sourceRead(7);                                                                                           //читаем первые 7 истинных байтов файла "shell32.dll"
alert(data);                                                                                                                    //выведет первые 7 истинных байтов файла "shell32.dll" в исходном виде, но не все байты
                                                                                                                                            //могут быть отображены в виде текста при таком выводе
alert(bytesToHex(data));                                                                                               //выведет, например, "4d5a9000030000", то есть это первые 7 истинных байтов
                                                                                                                                            //"shell32.dll" в строковом шестнадцатеричном представлении - 4d 5a 90 00 03 00 00

data = sourceRead(1);                                                                                                 //читаем следующий один (8-й) истинный байт файла "shell32.dll"
alert(data);                                                                                                                     //выведет один (8-й) истинный байт файла "shell32.dll", если он может быть
                                                                                                                                            //отображен в виде текста
alert(bytesToHex(data));                                                                                               //выведет, например, "00", то есть это один нулевой (8-й) истинный байт "shell32.dll"
                                                                                                                                            //в строковом шестнадцатеричном представлении - 00

</script>

Примечание: удаление нулевого байта после возможной конвертации нечетного количества строковых байтов в формате Юникод в HTML приложении "Resource Extractor" происходит автоматически на последующем уровне обработки полученных данных. Таким образом исключена обработка дополнительного параметра количества байтов в универсальной функции "strToBt" без снижения производительности.

Ну и в заключение рассмотрим особенности записи в бинарные файлы при использовании "SAPI.SpFileStream" на чистом JScript:

<script language = VBScript>

Function alert(message)                                                      'заменим "alert" из JScript на более информативный метод для анализа данных
    Dim messageType
    messageType = TypeName(message)
    If messageType = "String" Or messageType = "Byte()" Then
        InputBox "", "Message from webpage", message
    Else
        InputBox "", "Message from webpage", TypeName(message)
    End If
End Function

</script>

<script language = JScript>

var doc = GetObject('\\', 'htmlfile');
doc.write("<xml><_ xmlns:dt='urn:schemas-microsoft-com:datatypes'><_ dt:dt='bin.hex'/></_></xml>");
var xml = doc.documentElement.firstChild.children[1];
xml.preserveWhiteSpace = 1;

var hex = xml.firstChild.childNodes[0],

bytesToHex = function(bytes)
{
    if(typeof bytes == 'unknown')
    {
        hex.nodeTypedValue = bytes;
        return hex.text;
    }
    return bytes;
},

hexToBytes = function(hexString)                                        //функция для конвертации строкового шестнадцатеричного представления байтов в байты
{
    if(hexString)
    {
        hex.text = hexString;
        return hex.nodeTypedValue;
    }
    return bytes;
},

sourceRead = function(bytesCount)
{
    memory.Position = 0;
    memory.Type = 2;
    memory.Charset = 'Unicode';
    var stringBytes = source.ReadStream(1, bytesCount, 3);
    memory.WriteText(stringBytes);
    memory.SetEOS();
    memory.Position = 0;
    memory.Type = 1;
    memory.Position = 2;
    var bytes = memory.Read(bytesCount);
    return bytes;
}

var source = new ActiveXObject('WindowsInstaller.Installer').CreateRecord(1);
source.SetStream(1, 'C:\\Windows\\System32\\shell32.dll');

var memory = new ActiveXObject('ADODB.Stream');
memory.Open();

var output = new ActiveXObject('SAPI.SpFileStream');       //подготовим новый объект для работы с файловой системой
output.Format.Type = 1;                                                       //используем байты-текст как смешанный тип принимаемых данных (SAFTText = 1)
output.Open('output.bin', 3);                                                 //создаем и открываем файл для выгрузки данных на запись (SSFMCreateForWrite = 3)

var data = sourceRead(7);
var sourceData = bytesToHex(data);                                   //собираем байты источника в строковом шестнадцатеричном представлении для их последующего сравнения
                                                                                                    //с выгруженными данными
output.Write(data);                                                                //пишем первые 7 истинных байтов файла "shell32.dll" в файл для выгрузки данных

sourceData += '313220000000';
output.Write('\u3231\x20');                                                    //пишем строковые байты в файл для выгрузки данных - используем Юникод для последующей нераздельной
                                                                                                    //передачи пары байтов строки "12", а также пишем одиночный байт пробела, который “SAPI.SpFileStream"
                                                                                                    //в силу своих особенностей превращает в 4 байта

data = '68656c6c6f';
sourceData += data;
output.Write(hexToBytes(data));                                           //пишем байты строки "hello" в файл для выгрузки данных

data = sourceRead(10);
sourceData += bytesToHex(data);
output.Write(data);                                                                 //пишем следующие 10 (с 8-го по 17-й) истинных байтов файла "shell32.dll" в файл для выгрузки данных

output.Close();                                                                       //закрываем поток на запись файла для выгрузки данных
source.SetStream(1, 'output.bin');                                         //открываем файл выгрузки данных как текстовый поток файловой системы только для чтения
data = sourceRead(-1);                                                         //считываем все истинные байты файла выгрузки данных

doc.parentWindow.alert
(
    'Source hexString: ' + sourceData +
    '\n\nOutput hexString: ' + bytesToHex(data) +
    '\n\nTotal true bytes count: ' + sourceData.length/2+'.'
);                                                                                             //сравниваем то, что записывали в файл выгрузки данных с тем, что в него записалось

</script>

Подведем итоги:

  • "SAPI.SpFileStream" умеет писать как байты, так и строковые байты под любой локалью, однако необходимость в последних отпадает, если смотреть в сторону уменьшения обращений к жесткому диску и произведения необходимых операций преимущественно в памяти при использовании "ADODB.Stream";

  • путем передачи пары байтов в "SAPI.SpFileStream" в виде символа в формате Юникод можно исключить возникновение лишних нулевых байтов, которые добавляет "SAPI.SpFileStream" в случае записи одиночных строковых байтов;

  • "WindowsInstaller.Installer" прекрасно выполняет свою функцию в комбинации с "ADODB.Stream" для чтения бинарных файлов под любой локалью;

  • "ADODB.Stream" прекрасно выполняет свои функции в качестве обработчика данных в оперативной памяти под любой локалью.

Примечания:

  • если данных очень много, то следует достигать баланса между работой в памяти и выгрузкой данных на жесткий диск.
    Опыт разработки Resource Extractor показал, что JScript довольно слабо справляется с большими объемами данных в памяти, и производительность без промежуточных выгрузок на жесткий диск в этом случае начинает проседать;

  • "SAPI.SpFileStream" не может писать файлы с расширением "wav" в Windows XP при использовании параметра "SAFTText = 1", что, ожидается, было устранено во всех последующих версиях Windows.

Отдельно хочу поблагодарить JSman за примеры работы с "SAPI.SpFileStream", на которые я опирался при разработке, и Xameleon за изначальную подачу материала по "SAPI.SpFileStream" и примеры по hex-конвертации.

Желаю всем удачных экспериментов с бинарными данными! Но, пожалуйста, будьте осторожны.

2

Re: JScript: "WindowsInstaller" и "SAPI" - чтение и запись бинарных файлов

Исключение заголовка "BOM".

Два байта заголовка "BOM" (ff fe) появляются при конвертации в формате Юникод. Их можно пропустить, как было показано выше, но также можно использовать способ конвертации "напрямую", при которой заголовок "BOM" не появится.

Для этого можно воспользоваться возможностями ActiveX объекта "ADODB.Recordset" и функцией конвертации "MultiByteToBinary" от motobit.com.

Стоит отметить, что использовать функцию "MultiByteToBinary" в исходном виде нецелесообразно, так как она не оптимизирована и приведет к снижению производительности.

Оптимизируем "MultiByteToBinary" применительно к чтению бинарного файла с помощью "WindowsInstaller.Installer" так, чтобы исключить повторные инициализации и работать с меньшим количеством входных переменных:

!function sourceRead(bytesCount)                                    //функция последовательного чтения байтов файла-источника
{                                                                                           //(количество байтов от предыдущей позиции чтения)
    var RS = new ActiveXObject('ADODB.Recordset');      //исключение повторных инициализаций при вызове функции
    RS.Fields._Append(0, 205, 1);                                       //имеет меньший размер после инициализации, также у "_Append" меньше входных переменных, чем у "Append"
    RS.Open();
    RS.AddNew();
    var bytes = RS(0);
    sourceRead = function(bytesCount)
    {
        bytes.AppendChunk(source.ReadStream(1, bytesCount, 3));
        RS.Update();
        //return bytes.Value;                                                    //работает медленнее, чем "GetChunk", возвращает лишний нулевой байт
        return bytes.GetChunk(bytesCount);                          //работает медленнее, чем конвертация при помощи "ADODB.Stream"
    }
}();

Несмотря на проведенную оптимизацию, использование "ADODB.Recordset" для конвертации приводит к снижению производительности по сравнению с использованием "ADODB.Stream".

Вывод: "ADODB.Stream" - самый быстрый ActiveX объект из всех известных мне для итеративной работы с бинарными данными в оперативной памяти. Он в том числе работает быстрее на JScript, чем "SAPI.SpMemoryStream" на VBScript.

Также стоит отметить, что чтение при помощи "WindowsInstaller.Installer" на JScript происходит немногим, но быстрее, чем при помощи "SAPI.SpFileStream" на VBScript.