Тема: 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").
Работа с исходным кодом.
Для запуска приложения с примером скопируйте его исходный код и сохраните его в текстовом файле, сменив его расширение на "hta".
Запуск приложения с примером осуществляется двойным нажатием, как и в случае с обычной программой.
Рекомендую использовать 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-конвертации.
Желаю всем удачных экспериментов с бинарными данными! Но, пожалуйста, будьте осторожны.