jParser: анализ двоичных файлов работает просто

Original author: vjeux (Christopher Chedeau)
  • Translation
  • Tutorial
jParser делает простым чтение структур данных из двоичных файлов джаваскриптом.

  • Вы один раз описываете структуру, анализ её происходит автоматически.
     
  • Процесс анализа данных может быть расширен самописными функциями. Чем упрощается разбор нетривиальных файлов.
     
  • jParser действует и во браузере, и в NodeJS, потому что работает на основе jDataView.

API

Элементарные структуры:

  • Целые числа без знака: uint8, uint16, uint32
     
  • Со знаком: int8, int16, int32
     
  • Дробные с плавающей точкой: float32, float64
     
  • Строковые: char, string(len)
     
  • Массив: array(type, len)
     
  • Положение: tell, skip(len), seek(pos), seek(pos, func)

Методы jParser:

  • parse(value) — запускает анализ, может использоваться рекурсивно. Поведение зависит от типа аргумента:

    • функция: вызывает функцию;
       
    • строка: разыменование (по указанному имени берётся значение из структуры);
       
    • массив: вызов функции, имя которой — первый элемент массива, а последующие элементы служат аргументами;
       
    • объект: возвращает объект с теми же именами полей, но проанализированными значениями.

  • tell() — возвращает текущее положение (смещение) в анализируемом файле.
     
  • skip(count) — передвинуться в файле на count байтов вперёд (пропустить их).
     
  • seek(position) — перейти к указанному смещению в анализируемом файле.
     
  • seek(position, callback) — перейти к указанному смещению, там выполнить callback(), затем возвратиться к прежнему положению.
     
  • current — Текущий объект, подвергаемый анализу. Используйте для доступа к результатам, ужé достигнутым во время анализа.

Конструктор jParser:

  • new jParser(data, structure)

    • data — это jDataView с данными, подвергаемыми анализу. Можете передать строку байтов (String), можете передать ArrayBuffer или буфер Node.js эти типы данных преобразуются в jDataView автоматически.
       
    • structure — объект с описанием всех структур данных.


Примеры


Обычная структура Си

Вы можете описывать Си-подобные структуры данных. Это джаваскриптовый объект, в котором поля являются именами данных, а их значения — типами.

var parser = new jParser(file, {
  header: {
    fileId: 'int32',
    recordIndex: 'int32',
    hash: ['array', 'uint32', 4],
    fileName: ['string', 256],
  }
});
parser.parse('header');
// {
//   fileId: 42,
//   recordIndex: 6002,
//   hash: [4237894687, 3491173757, 3626834111, 2631772842],
//   fileName: ".\\Resources\\Excel\\Items_Weapons.xls"
// }


Ссылки

Структуры могут содержать другие структуры. Используйте имя структуры в виде строки, чтобы сослаться на её описание. Нижеследующий пример является частью структуры файла, содержащего модель для World of Warcraft:

nofs: {
  count: 'uint32',
  offset: 'uint32'
},

animationBlock: {
  interpolationType: 'uint16',
  globalSequenceID: 'int16',
  timestamps: 'nofs',
  keyFrame: 'nofs'
},

uvAnimation: {
  translation: 'animationBlock',
  rotation: 'animationBlock',
  scaling: 'animationBlock'
}


Функции-помощники

Вам будет несложно определять новые элементарные типы. Вы можете использовать существующие виды конструкций, такие как объекты (float3 в нижеследующем примере) или массивы (float4 в примере). Если же вы желаете определить более сложный тип, то всегда есть возможность определить новую функцию, полагающуюся на метод this.parse для первичного анализа (hex32 и string0 в нижеследующем примере):

float3: {
  x: 'float32',
  y: 'float32',
  z: 'float32'
},
float4: ['array', 'float32', 4],
hex32: function () {
  return '0x' + this.parse('uint32').toString(16);
},
string0: function (length) {
  return this.parse(['string', length]).replace(/\0+$/g, '');
}


Обратные связи

Если размер массива заранее не известен, то можете поместить на его место в структуре функцию, возвращающую целое число. В этой функции вы можете через this.current обратиться к объекту, в настоящее время подвергающемуся анализу, считывать ужé полученные поля его.

image: {
  width: 'uint8',
  height: 'uint8',
  pixels: [
    'array',
    ['array', 'rgba', function () { return this.current.width; }],
    function () { return this.current.height; }
  ]
}


Развитый анализ данных

Приятнейшая особенность jParser заключается в том, что сложные алгоритмы обработки можно выразить функциями внутри описания структуры данных. Это позволяет анализировать сложные файлы, не отделяя описание структуры от кода её обработчиков.

entryHeader: {
  start: 'int32',
  count: 'int32'
},

entry: function (type) {
  var that = this;
  var header = this.parse('entryHeader');

  var res = [];
  this.seek(header.start, function () {
    for (var i = 0; i < header.count; ++i) {
      res.push(that.parse(type));
    }
  });
  return res;
},

name: {
  language: 'int32',
  text: ['string', 256]
},

file: {
  names: ['entry', 'name']
}


Начало работы


На движке NodeJS

Просто используйте npm для установки jParserи вы готовы :-)

npm install jParser

var fs = require('fs');
var jParser = require('jParser');

fs.readFile('file.bin', function (err, data) {
  var parser = new jParser(data, {
    magic: ['array', 'uint8', 4]
  });
  console.log(parser.parse('magic'));
});


Во браузере

Я пропатчил jQuery, обеспечив скачивание двоичных файлов в наилучшем двоичном формате. Подключите этот пропатченный код jQuery, а также jDataView и jParser — и вы готовы :-)

<script src="https://raw.github.com/vjeux/jDataView/master/jquery/jquery-1.7.1-binary-ajax.js"></script>
<script src="https://raw.github.com/vjeux/jDataView/master/src/jdataview.js"></script>
<script src="https://raw.github.com/vjeux/jParser/master/src/jparser.js"></script>

<script>
$.get('file.bin', function (data) {
  var parser = new jParser(data, {
    magic: ['array', 'uint8', 4]
  });
  console.log(parser.parse('magic'));
}, 'dataview');
</script>


Предосторожности


Этот иструмент в своей работе полагается на недокументированную особенность JavaScript: обход полей объекта работает в том порядке, в каком они указывались. Имейте в виду, что в Chrome и в Opera это неявное правило не работает для полей с цифровыми именами.

Следуйте вот каким двум правилам, чтобы эта библиотека работала во всех существующих реализациях языка JavaScript:

  • Не начинайте имя поля цифрою в описании структуры данных.
     
  • Не переопределяйте поле с однажды заданным именем, не помещайте в тот же объект другое одноимённое поле.


Демо


Анализ ICO. Это простой пример анализа двоичного файла на движке NodeJS. Он показывает, как решаются многие типичные задачи, возникающие при разборе двоичных данных.

  • ico.js — структура данных для jParser.
     
  • ico.json — итог анализа.

Распаковщик Tar. Это простой пример разбора двоичного файла внутри браузера.

  • tar.html — структура jParser.

Средство показа моделей World of Warcraft. Использует jParser для чтения двоичного кода трёхмерной модели, затем WebGL для её отображения.

  • m2.js — структура jParser.
     
  • model.json — итог анализа.

[скриншот]

Внутренние файлы Diablo 3

 
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 29

    +2
    Комментарий переводчика:  перед вами — перевод документации (README на Гитхабе), описывающей ту самую библиотеку jParser (действующую на основе jDataView того же автора), которая употребляется для чтения BMP во блогозаписи у RReverser.
      0
      Когда читал оригинал пропустил важный раздел «предосторожности», теперь понял причину возникших проблем :)
      Спасибо за перевод.
        +1
        Спасибо за статью! Проэксперементируем.
          0
          Использовал для написания парсера node-strtok, рекомендую.
            –1
            А чем это лучше protojs?
              0
              Какого protojs?

              На свете много продуктов, носящих это имя.
                0
                Этого, который реализация protocol buffer.
                    0
                    Случайно тег не закрыл (надавил кнопку «Написать» раньше времени), так что текст стал весь подчёркнутым и читается много хуже.

                    Повторю суть в двух словах: я не понял, как jParser и https://github.com/sirikata/protojs можно сравнивать по принципу «лучше / хуже» — в чём они аналогичны, разве делают они одну и ту же работу?

                    Возможно, в моём непонимании свою роль сыграла недостаточная подробность документации к protojs.
                      0
                      Это реализвция гугловского Protocol Buffers для компактного хранения и передачи структурированных данных с минимумом оверхеда.

                      Похоже, я не уловил назначения сабжа. Это только парсер, обратной операции не подразумевается?
                        0
                        Да, только парсер.

                        Кажется, jParser способен анализировать (парсить) данные более сложной структуры, чем Protocol Buffers, и тем он лучше.

                        С другой стороны, если речь идёт не о чтении существующих структур, а просто о двоичной передаче данных, то Protocol Buffers попривлекательнее, ибо не только читать умеет.

                        (Кстати, окромя protojs, ещё и protobuf есть для той же цели под Node, оказывается.)
                          +1
                          Ну тогда это просто разные инструменты, извиняюсь.
                    0
                    Он, между прочим, первый в выдаче яндекса и гугла.
                      0
                      Верно. Я просто не был уверен, что речь именно об этом продукте.
                  0
                  Спасибо за перевод README
                    0
                    Давно мучаюсь проблемой (как только начал писать на node.js) о том, как на node.js распарсить текстовый файл конфигурации с записями типа ключ=«значение». Понимаю что задача тривиальная. Но, видимо, из-за ее тривиальности ее решение нигде не могу найти. Может кто в курсе?
                      0
                        –1
                        Ой, какие мы крутые! Вы думаете что только вы умеете пользоваться гуглем? Естественно я его перерыл насколько мог. Может быть смог плохо, но все что смог сделал. Поэтому и спрашиваю.
                          –1
                          Тем не менее, хотя там в результатах и выдается парсинг ini файлов — НЕ то, что мне нужно, спасибо за наводку. Как минимум, могу использовать исходный код парсера ini как пример того, как нужно парсить текстовые файлы.
                            0
                            Вы описали что ищите парсер ini, но вам нужны было другое? Ну что же :)
                              –1
                              Я написал про «текстовый файл конфигурации с записями типа ключ=«значение»». Насколько я знаю, ini-файл, это немного другое. Там еще и секции есть. У меня нет.

                              Задачи похожие, поэтому использую iniparser как пример для своего парсера.
                                0
                                Позвольте порекомендовать Вам тысяча пятьдесят третий комикс xkcd.
                        +1
                        Неужели у этой штуки нет ни каких минусов? Кто на практике использовал, и какие подводные камни возникают?
                        • UFO just landed and posted this here
                            0
                            Я использовал, вон там изложил итоги.
                            +1
                            github.com/squaremo/bitsyntax-js — шаг на светлую сторону силы)
                            Без использования недокументированных возможностей!!!
                              +1
                              Развлекся — переписал ico-парсер из примеров с JS на Erlang, получилось 57 строк против 69 на JS.
                              В принципе, можно до 44-х сократить.
                              И тут не используются внешние библиотеки — все встроенное.
                              -module(parse_ico).
                              -export([parse_ico/1]).
                              
                              parse_ico(FileName) ->
                              	{ok, File} = file:read_file(FileName),
                              	<<Reserved:16/unsigned-integer-little,
                              	Type:16/unsigned-integer-little,
                              	ImageCount:16/unsigned-integer-little,
                              	Images/binary>> = File,
                              	{{header,
                              	  {reserved, Reserved},
                              	  {type, Type},
                              	  {image_count, ImageCount}},
                              	 {images, parse_images(Images, ImageCount, [])}}.
                              
                              parse_images(_, 0, Result) ->
                              	Result;
                              parse_images(ImagesBin, ImageCount, Result) ->
                              	<<Width,
                              	Height,
                              	PaletteCount,
                              	Reserved,
                              	ColorPlanes:16/unsigned-integer-little,
                              	BitsPerPixel:16/unsigned-integer-little,
                              	Size:32/unsigned-integer-little,
                              	Offset:32/unsigned-integer-little,
                              	Content/binary>> = ImagesBin,
                              	{ok, Palette, PixelsBin} = parse_palette(Content, PaletteCount, []),
                              	{ok, Pixels, NextImgBin} = parse_pixels(PixelsBin, BitsPerPixel, Width, Height),
                              	Image = {
                              	  {width, Width},
                              	  {height, Height},
                              	  {palette_count, PaletteCount},
                              	  {reserved, Reserved},
                              	  {color_planes, ColorPlanes},
                              	  {bits_per_pixel, BitsPerPixel},
                              	  {size, Size},
                              	  {offset, Offset},
                              	  {content, {
                              		{palette, Palette},
                              		{pixels, Pixels}}}},
                              	parse_images(NextImgBin, ImageCount -1, [Image | Result]).
                              
                              
                              parse_palette(Binary, 0, Result) ->
                                  {ok, lists:reverse(Result), Binary};
                              parse_palette(<<R, G, B, A, Rest/binary>>, Count, Result) ->
                                  parse_palette(Rest, Count - 1, [{R, G, B, A} | Result]).
                              
                              parse_pixels(PixelsBin, BitsPerPixel, Width, Height) ->
                              	CurImageSize = trunc(BitsPerPixel * Width * Height / 8),
                              	CurRowSize = trunc(BitsPerPixel * Width / 8),
                              	<<MyPixels:CurImageSize/binary, NextImgBin/binary>> = PixelsBin,
                              	Pixels = [lists:flatten([[Px2, Px1]
                              							 || <<Px1:BitsPerPixel, Px2:BitsPerPixel>> <= Row])
                              			  || <<Row:CurRowSize/binary>> <= MyPixels],
                              	{ok, Pixels, NextImgBin}.
                              
                                +1
                                Ну сравнения нельзя назвать корректным в контексте топика. Маханизм матчинга в эрланг просто создан для работы с двоичными структурами. В JS подобных задач по сути не возникало, поэтому это технологии разных калибров.
                                  0
                                  Ну, коммент с эрланговским кодом я написал чтобы подтвердить, что библиотека из первого коммента, вероятно, лучше подходит для парсинга бинарных данных и на нее тоже стоит обратить внимание.

                                  В дополнение, совершенно не понятно как библиотека из топика относится к «big-endian vs little-endian».

                            Only users with full accounts can post comments. Log in, please.