Здравствуй, Хабр!
В данном посте хочу предложить попробовать создать приложение, которое позволяет перевести файл в формате bencode в XML файл. ЯП возьмем C#.
(Да, мы попробуем создать велосипед. Для чего это необходимо? Если не уметь решать типовые задачи, не будут удаваться и нестандартные)
Bencode. Как он есть
Итак. Давайте разберем, что «подразумевает» формат bencode:
Он включает (поддерживает) 4 типа данных
- Строка байт
- Целое число
- Список
- Словарь
В виде разделителей Bencode принимает ASCII символы и цифры.
Как эти данные содержатся?
На мой взгляд, можно выделить простые и сложные типы данных.
Простые типы:
1) Строка байт — Перед строкой содержится ее длина (число), затем знак двоеточия («:») и, собственно, строка. Например: 5:hello или 12:hello, habr!
2) Целое число — записывается в виде: символ «i»(integer), число, символ «e»(end). Например: i9e или i199e. Также, данный тип «поддерживает» отрицательные числа. Запись разберем на примере i-9e (-9)
Сложные типы данных(состоят из простых и сложных типов):
1) Список — (он же массив) — содержит другие bencode типы, которые записываются последовательно. Способ записи — символ «l» (list), описание типов данных, символ «e». Пример: l1:I3:You2:Wee [«I»,«You»,«We»]
2) Словарь — (ассоциативный массив) — Содержит данные ключ-значение. Причем в виде ключ стоит обязательно строка байт, и данные отсортированны в лексикографическом порядке по полю «ключ». Задается словарь следующим образом — символ «d» (dictionary), элементы ключ-значение, символ «e». Например d5:alpha4:beta10:filesCounti9ee [«alpha» — «beta», «filesCount» — 9]
Где используется BEncode?
Bencode используется во всех нами любимых .torrent файлах. Данные файлы представляют ассоциативных массив (словарь).
Останавливаться на устройстве .torrent файлов не будем.
Организуем структуры данных
На мой взгляд, логично будет завести несколько классов, для «раскладывания по полочкам».
Поэтому, создадим класс простых элементов(BItem) (он будет обрабатывать как число, так и строку, затем создадим класс списка(BList), а затем класс — словарь(BDictionary).
Так как при обработки BEncode файла принимаем, что мы не знаем, как элементы следуют друг за другом, создаем класс(BElement), в которой инкапсулируем методы для работы с элементами и все классы списка, словаря и простых данных. Получаем составной класс.
И последний 5-й класс будет содержать список элементов.(FileBEncoding) (можно сделать один элемент, но возьмем более общий случай)
Графически это выглядит следующим образом: 
Кодирование чтения файла
Класс BItem содержит
- Строку
- Число
- Флаг принадлежности элемента к числу
- Конструкторы для строки и числа
- Метод перевода в строку (перегрузка ToString())
Класс BList содержит
- Непосредственно список элементов (BElement)
- Индексацию
- Свойство Count
- Метод для добавления элементов(Add)
Класс BDictionary содержит
- Два списка с элементами (для организации структуры ключ-значение)
- Свойство Count
- Индексацию
- Метод для добавления элементов(Add)
Теперь переходим к «универсальному» классу BElement.
Класс BElement содержит
- Переменную типа BItem — реализация строкичисла
- Переменную типа BList — реализация списка
- Переменную типа BDictionary — реализация словаря
- Метод для чтения простого типа данных
- Метод для чтения списка
- Метод для считывания словаря
- Определение типа данных (метод)
Последний класс, «сердце» нашей структуры — FileBEncoding
В нем и будет реализован алгоритм чтения BEncodeзаписи XML.
Для начала реализуем чтение.
Пусть данный класс будет содержать:
- Список BElement (т.к. .torrent файлы, обычно имеют только один элемент, можно задать просто переменную типа BElement)
- Индексацию (вариант списка)
- Конструктор со строкой, описывающую путь к файлу
Более понятный вывод в строку
Если Вам хочется уже посмотреть вывод в xml, то данную часть можно пропустить.
Здесь хотелось бы предложить «структурированный» вывод информации в файлконсоль.
Что для этого нам понадобится?
Все классы в C# производные от класса Object. Данный класс имеет метод ToString(). По умолчанию данный метод выводит имя типа. Переопределим его.
Здесь мы использовали дополнительно 2 функции. Первая служит для обработки отдельно взятого BElement (можно заметить, что она рекурсивна), вторая — для создания отступов.
Создаем XML файл
Прежде чем приступим к кодированию, хотелось бы сказать пару слов о самом языке XML.
Данный язык получил весьма широкое распространение как единый язык инфо-обмена. В C# (а точнее платформе .NET) реализована прекрасная поддержка XML. Для удобной работы используем встроенные средства, подключив пространство имен System.XML.
Есть целый ряд способов создать XML-документ. Мой выбор пал на класс XmlWriter. Данный класс позволяет создать объект «с пустого места». Причем происходит запись каждого элемента и аттрибута по порядку. Главным преимуществом данного способа является высокая скорость работы.
Для создания документа определим два метода
vo > Первый метод будет создавать объект нужного нам XmlWriter класса и вызывать BElementToXML для каждого элемента из списка.
Второй метод занимается «раскручиванием» BElement (если он списоксловарь) и, собственно, формированием файла.
В итоге получаем более удобные способы восприятия BEncode-файла:
(Для примера использовал торрент-файл ubuntu. SHA ключи удалил.)
Консоль: (метод ToString()) 
Excel: (xml) 
Заключение
В данном небольшой статье — уроку мы научились: обрабатывать BEncode файлы, формировать XML файл.
А создание из BEncode файла — XML может пригодиться, к примеру, для написания редактора BEncode-файлов. Здесь собранный воедино код
I’m implementing a bencoding system for a torrent downloading system I’m making.
Bencoding a string is very easy, you take a string, for instance «hello», and you encode it by writing the string length + a ‘:’ character, followed by the string itself. Bencoded «hello» will be «5:hello»
Currently I’m having this code.
It works, but I have the feeling that it could be done ways better. What is the best way to do this?
Note: the string could be any size (thus more than one digit before the string)
Solved already, thanks to everybody that replied here 🙂
I have been working on a Bencoded string parser in order to improve my knowledge of Haskell. After running the code through hlint, I still have a few questions:
As I noted in the comments, the key value in the definition of BMapT should always be a Bstr. Is there any way to enforce a particular data constructor when pattern matching an algrebraic datatype?
Since Bencoded dictionaries are generally stored as human readable text, I made an instance of the Show typeclass so that I could take Bencode structures and turn them into text. I was wondering if this is the right thing to do, or if Haskell has another typeclass for storage/serialization a la Python’s repr .
This question is kind of nebulous, but would you consider this code ‘idiomatic’? Code without style isn’t worth writing 🙂
Code:

1 Answer 1
Below is your code with some minor tweaks and the comments moved into standard Haddock form.
Answers to your questions.
- Without getting REAL fancy there is no good way limit the BMap argument to a Bstr while it is an member of algebraic type.
- Your analogy to repr from Python is a good one. Typically if there is a Haskell Show instance there is also a Read instance so show . read . show is valid. In your case the result of show needs to be parsed again and you do not expose a parse from string/bytestring. I added a bencode and bencodeBS which takes a BencodedValue and outputs a String or ByteString.
- Look at where I simplified or renamed your items to see suggestions on idiomatic usage. On the whole you did pretty well. If I had more time I would have liked to transform your parser functions into Applicative style. By transitioning to the mkFoo functions I started that process for you if you like.
- In the code you ask about why this is a String when the ByteString is the input. It is because the parsers are returning Strings parsed from the ByteString. If the performance really matters you could transition over to Attoparsec.