1

Тема: JavaScript: Bencode - формат хранения и передачи данных BitTorrent

на досуге написал небольшой скрипт разбора и упаковки данных в Bencode - формат, используемый p2p системой распространения файлов BitTorrent для хранения и передачи свободно структурированных данных. Когда начинал писать были какие-то мысли на счет его использования. Однако со временем актуальность потерялась, а код остался. Некоторое время спустя возникло желание завершить написанное и поделиться с общественностью. Возможно кому-нибудь пригодится. Насколько помню, проблема упирается в корректную трансляцию порции бинарных данных. Полностью решить не удалось.
.
Ссылки
Подробно о формате данных можно почитать по следующим ссылкам:
http://ru.wikipedia.org/wiki/Bencode - русская википедия
http://en.wikipedia.org/wiki/Bencode - английская википедия
http://wiki.theory.org/BitTorrentSpecification - спецификация от разработчиков

На этих страницах можно найти и другие реализации с использованием различных языков программирования (также и JScript/JavaScript).
.
Описание библиотеки
Bencode.stringify(value)
Конвертирует объект в строку в формате bencode.

Bencode.parse(value)
Разбирает bencode строку в объект JavaScript. В случае некорректно сформированной строки бросает исключение RangeError. Это самая основная функция для разбора данных торрент-файла.

Bencode.PATH_DELIMITER = '\\';
Разделитель путей, по умолчанию. Это значение может быть изменено до вызова Bencode.torrentInfo().

Bencode.torrentInfo(value, key)
Рассматривает входной объект как торрент-объект и возвращает информацию, заданную параметром key. Если входное значение - строка, то она первоначально будет разобрана в торрент-объект.

Есть несколько специально заданных ключей для упрощения доступа к данных:
'name'
название торрента

'piece length'
количество байт в каждом фрагменте

'pieces'
строка, состоящая из 20-ибайтых SHA1-кодированных хешей. До сих пор эта информация у меня не вызывает доверия в силу того, что JScript не умеет корректно работать с байтовыми строками без привязки к кодировке. Возможно в NodeJS этих проблем не будет. Я не проверял.

'creation date'
дата создания торрент-файла. Эта информация конвертируется в формат типа Date.

'announce-list'
упрощенный список (массив) анонсов (в отличие от используемого для внутренних целей списка списков строк)

'file-names'
'file-sizes'
всегда (даже если файл один) список имен и список размеров всех файлов в торренте
.
Исходный код
Полная и актуальная версия файла находится по адресу http://code.google.com/p/jsxt/source/br … Bencode.js


var Bencode = Bencode || {};

(function()
{

var toString = Object.prototype.toString;

var stringify = function(value)
{
	var typeOf = toString.call(value);

	if ( typeOf == '[object Number]' ) {
		return 'i' + parseInt(value) + 'e';
	}
	if ( typeOf == '[object String]' ) {
		return value.length + ':' + value.toString();
	}
	if ( typeOf == '[object Date]' ) {
		return 'i' + Math.floor(value.getTime() / 1000) + 'e';
	}

	var result = [];

	if ( typeOf == '[object Array]' ) {
		for (var i = 0; i < value.length; i++) {
			result.push(stringify(value[i]));
		}
		return 'l' + result.join('') + 'e';
	}

	for (var p in value) {
		if ( ! value.hasOwnProperty(p) ) {
			continue;
		}
		result.push(stringify(String(p)) + stringify(value[p]));
	}
	return 'd' + result.join('') + 'e';
};


var i = 0;
var text;

var parser = function()
{
	var c = text.charAt(i);

	var err;

	switch (c) {
	case 'i':
		err = 'integer';

		var matches = text.slice(i).match(/^i(-?\d+)e/);
		if ( ! matches ) {
			break;
		}

		i += matches.lastIndex;
		return Number(matches[1]);

	case 'l':
		err = 'list';

		i++;

		var result = [];
		while ( i < text.length && text.charAt(i) != 'e' ) {
			result.push(parser());
		}

		if ( text.charAt(i) != 'e' ) {
			break;
		}

		i++;
		return result;

	case 'd':
		err = 'dictionary';

		i++;

		var result = {};
		while ( i < text.length && text.charAt(i) != 'e' ) {
			var k = parser();
			var v = parser();
			result[k] = v;
		}

		if ( text.charAt(i) != 'e' ) {
			break;
		}

		i++;
		return result;

	default:
		err = 'string';

		var matches = text.slice(i).match(/^(\d+):/);
		if ( ! matches ) {
			break;
		}

		var len = Number(matches[1]);
		var a = i + matches.lastIndex;
		var b = a + len;

		var result = text.slice(a, b);
		if ( result.length != len ) {
			break;
		}

		i = b;
		return result;
	}

	// Here is abnormal ending
	throw new RangeError('Bencode.parse: Illegal ' + err + ' at ' + i + ' (0x' + i.toString(16).toUpperCase() + ')');
};


var torrentInfo = function(value, key)
{
	switch ( key ) {
	case 'name': 
	case 'piece length': 
	case 'pieces': 
		return value.info[key];
	case 'creation date': 
		// convert date to the standard Date object
		return new Date(value[key] * 1000);
	case 'announce-list': 
		var input = value['announce-list'];
		var result = [];
		for (var i = 0; i < input.length; i++) {
			// convert a list of lists of strings to 
			// the simple list of strings
			result.push.apply(result, input[i]);
		}
		return result;
	case 'file-names':
		var input = value.info.files;
		if ( ! input ) {
			// torrent contains the single file
			return [value.info.name];
		}
		var result = [];
		for (var i = 0; i < input.length; i++) {
			result.push(input[i].path.join(Bencode.PATH_DELIMITER));
		}
		return result;
	case 'file-sizes': 
		var input = value.info.files;
		if ( ! input ) {
			// torrent contains the single file
			return [value.info.length];
		}
		var result = [];
		for (var i = 0; i < input.length; i++) {
			result.push(input[i].length);
		}
		return result;
	default:
		return value[key];
	}
};

Bencode.PATH_DELIMITER = '\\';

Bencode.stringify = stringify;

Bencode.parse = function(value)
{
	i = 0;
	text = (value || '').toString();
	return parser();
};

Bencode.torrentInfo = function(value, key)
{
	if ( toString.call(value) == '[object String]' ) {
		value = Bencode.parse(value);
	}

	if ( ! value || ! value.info ) {
		return;
	}

	return torrentInfo(value, key);
};

})();

.
Пример использования
В качестве функции чтения торрент-файла использовалось стороннее решение http://demon.tw/my-work/javascript-bencode.html. Насколько я разобрался, это самый корректный из известных мне способов прочитать бинарные данные в JScript.


// http://demon.tw/my-work/javascript-bencode.html
function read(path) {
    var cp1252Chars = [/\u20AC/g,/\u201A/g,/\u0192/g,/\u201E/g,/\u2026/g,/\u2020/g,/\u2021/g,/\u02C6/g,/\u2030/g,/\u0160/g,/\u2039/g,/\u0152/g,/\u017D/g,/\u2018/g,/\u2019/g,/\u201C/g,/\u201D/g,/\u2022/g,/\u2013/g,/\u2014/g,/\u02DC/g,/\u2122/g,/\u0161/g,/\u203A/g,/\u0153/g,/\u017E/g,/\u0178/g];
    var latin1Chars = ["\u0080","\u0082","\u0083","\u0084","\u0085","\u0086","\u0087","\u0088","\u0089","\u008A","\u008B","\u008C","\u008E","\u0091","\u0092","\u0093","\u0094","\u0095","\u0096","\u0097","\u0098","\u0099","\u009A","\u009B","\u009C","\u009E","\u009F"];
    var binstream = new ActiveXObject("ADODB.Stream");
    binstream.Type = 2;
    binstream.Charset = "iso-8859-1";
    binstream.Open();
    binstream.LoadFromFile(path);
    var s = binstream.ReadText();
    for (var i = 0; i < 27; i++)
        s = s.replace(cp1252Chars[i], latin1Chars[i]);
    return s;
}

var filename = 'foo.torrent';

var text = read(filename);
var torrent = Bencode.parse(text);

WScript.Echo( Bencode.torrentInfo(torrent, 'name') );
WScript.Echo( Bencode.torrentInfo(torrent, 'creation date') );
WScript.Echo( Bencode.torrentInfo(torrent, 'file-names') );
( 2 * b ) || ! ( 2 * b )