Переводим из Bencode в XML

в 8:37, , рубрики: bencode, метки: ,

Здравствуй!
В данном посте хочу предложить попробовать создать приложение, которое позволяет перевести файл в формате bencode в XML файл. ЯП возьмем C#.
(Да, мы попробуем создать велосипед. Для чего это необходимо? Если не уметь решать типовые задачи, не будут удаваться и нестандартные)

Bencode. Как он есть

Итак. Давайте разберем, что «подразумевает» формат bencode:
Он включает (поддерживает) 4 типа данных

  1. Строка байт
  2. Целое число
  3. Список
  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) (можно сделать один элемент, но возьмем более общий случай)
Графически это выглядит следующим образом:
Переводим из Bencode в XML

Кодирование чтения файла

Класс BItem содержит

  • Строку
  • Число
  • Флаг принадлежности элемента к числу
  • Конструкторы для строки и числа
  • Метод перевода в строку (перегрузка ToString())
 /// <summary>
    /// integer value
    /// start - 'i'
    /// end - 'e'
    /// Example - i145e => 145
    /// string value
    /// start - length 
    /// end -
    /// Example 5:hello => "hello"
    /// </summary>
     public class BItem
    {
        protected string strValue = "";
        protected int intValue = 0;
        protected bool IsInt = true;
         public bool isInt
        {
            get
            {
                return IsInt;
            }
        }
        public BItem(string A)
        {
            strValue = A;
            IsInt = false;
        }
        public BItem(int A)
        {
            IsInt = true;
            intValue = A;
        }
        public string ToString()
        {
            if (IsInt)
                return intValue.ToString();
            return strValue;
        }
    }

Класс BList содержит

  • Непосредственно список элементов (BElement)
  • Индексацию
  • Свойство Count
  • Метод для добавления элементов(Add)
    /// <summary>
    /// List
    /// start - 'l'
    /// end - 'e'
    /// Example - l5:helloi145e => ("hello",145)
    /// </summary>
    public class BList
    {
        List<BElement> Items = null;

        public BList()
        {
            Items = new List<BElement>();
        }

        public BElement this[int index]
        {
            get
            {
                if (Items.Count > index)
                {
                    
                    return Items[index];
                }
                return new BElement();
            }
            set
            {
                if (Items.Count > index)
                {
                    Items[index] = value;
                }
                else
                {
throw new Exception("Выход за пределы списка. Ничего не записано!");
                }
            }
        }
       public int Count
        {
            get
            {
                return Items.Count;
            }
        }

        /// <summary>
        ///Добавляем новый элемент в список
        /// </summary>
        public void Add(BElement inf)
        {
            Items.Add(inf);
        }
    }

Класс BDictionary содержит

  • Два списка с элементами (для организации структуры ключ-значение)
  • Свойство Count
  • Индексацию
  • Метод для добавления элементов(Add)

  /// <summary>
    /// Dictionary
    /// start - 'd'
    /// end - 'e'
    /// Example - d2:hi7:goodbyee => ("hi" => "goodbye")
    /// </summary>
    public class BDictionary
    {
        protected List<BElement> FirstItem = null;
        protected List<BElement> SecondItem = null;

        public BDictionary()
        {
            FirstItem = new List<BElement>();
            SecondItem = new List<BElement>();
        }

        public int Count{
            get
            {
                return FirstItem.Count;
            }
        }
        /// <summary>
        /// Индексация
        /// </summary>
        /// <param name="index"></param>
        /// <returns></returns>
        public BElement[] this[int index]
        {
        get{
                if (FirstItem.Count > index)
                {
                BElement[] Items = new BElement[2];
                Items[0] = FirstItem[index];
                Items[1] = SecondItem[index];
                return Items;
                }
                return new BElement[2];
            }
        set{
            if (FirstItem.Count > index)
                {
                    FirstItem[index] = value[0];
                    SecondItem[index] = value[1];
                }
            else
                {
                    //FirstItem.Add(value[0]);
                   // SecondItem.Add(value[1]); - данный метод не рекомендую, т.к. может возникнуть затем путаница!!!!! В этом случае лучше
throw new Exception("Выход за пределы массива. Ничего не записано");
                }
            }
        }
        /// <summary>
        /// Добавление в словарь
        /// </summary>
        /// <param name="First">ключ</param>
        /// <param name="Second">значение</param>
        public void Add(BElement First, BElement Second)
        {
            FirstItem.Add(First);
            SecondItem.Add(Second);
        }
    }

Теперь переходим к «универсальному» классу BElement.
Класс BElement содержит

  • Переменную типа BItem — реализация строкичисла
  • Переменную типа BList — реализация списка
  • Переменную типа BDictionary — реализация словаря
  • Метод для чтения простого типа данных
  • Метод для чтения списка
  • Метод для считывания словаря
  • Определение типа данных (метод)
    /// <summary>
    /// "универсальный" класс
    /// </summary>
    public class BElement
    {
       public BItem STDItem = null;
       public BList LSTItem = null;
       public BDictionary DICItem = null;
        /// <summary>
        /// Создание и чтение простого типа данных stringinteger
        /// </summary>
        /// <param name="Reader">поток чтения</param>
        /// <param name="CurrentCode">Считанный заранее символ</param>
        public void AddToBItem(StreamReader Reader, char CurrentCode)
        {
            char C;
            if (CurrentCode == 'i')
            {//считывание числа
                string Value= "";
                C = (char)Reader.Read();
                while (C != 'e')
                {//конец
                    Value += C;
                    C = (char)Reader.Read();
                }
                try
                {
                    int Res = Int32.Parse(Value);
                    STDItem = new BItem(Res);
                }
                catch (Exception ex)
                {
                    //Здесь можно вызвать throw ошибку. А можно сделать объект null'ом
                    STDItem = null;
                }
                return;
            }
            int length = (int)CurrentCode - (int)'0';
            C = (char)Reader.Read();
            while (C != ':' && (C>='0' && C<='9'))
            {
                length = length * 10 + (int)C - (int)'0';
                C = (char)Reader.Read();
            }
            if (C!= ':') 
            {//Можно вызвать ошибку (так же как выше, просто обнулим объект, вместо throw new Exception("Неверно задан объект");
                // так как второй способ throw требует обработки выше по коду... а пока пишем только класс =)
                STDItem = null;
                return;
            }
            string value = "";
            for (int CurrentCount = 0; CurrentCount < length; CurrentCount++)
            {
                value += (char)Reader.Read();
            }
            STDItem = new BItem(value);

        }
       /// <summary>
       /// список. Считаем, что l была считана
       /// </summary>
       /// <param name="Reader">ридер файла</param>
        public void AddToBList(StreamReader Reader)
        {
            LSTItem = new BList();
            BElement Temp = GetNewBElement(Reader);
            while (Temp != null)
            {
                LSTItem.Add(Temp);
                Temp = GetNewBElement(Reader);
            }
            if (LSTItem.Count == 0) LSTItem = null;//опять же - здесь можно генерировать ошибку о неверной структуре файла.
        }
        /// <summary>
        /// Считывание словаря
        /// </summary>
        /// <param name="Reader">поток чтения файла</param>
        public void AddToBDic(StreamReader Reader)
        {
            DICItem = new BDictionary();
            BElement FirstTemp = GetNewBElement(Reader);
            BElement SecondTemp = GetNewBElement(Reader);
            while (FirstTemp != null || SecondTemp != null)
            {
                DICItem.Add(FirstTemp, SecondTemp);
                FirstTemp = GetNewBElement(Reader);
                SecondTemp = GetNewBElement(Reader);
            }
            if (DICItem.Count == 0) DICItem = null;//Либо писать об ошибке в структуре файла
        }

        /// <summary>
        /// Определяем тип следующего элемента. Запускаем создание
        /// </summary>
        /// <param name="Reader">поток чтения</param>
        /// <returns>Новый элемент</returns>
        public static BElement GetNewBElement(StreamReader Reader)
        {
            char C = (char)Reader.Read();
            switch (C)
                {
                    case '0':
                    case '1':
                    case '2':
                    case '3':
                    case '4':
                    case '5':
                    case '6':
                    case '7':
                    case '8':
                    case '9':
                    case 'i':
                        {//простой тип данных
                            BElement STDElement = new BElement();
                            STDElement.AddToBItem(Reader, C);
                            return STDElement;
                        }
                    case 'l':
                        {//список
                            BElement LSTElement = new BElement();
                            LSTElement.AddToBList(Reader);
                            return LSTElement;
                        }
                
                case 'd':
                        {//словарь
                            BElement DICElement = new BElement();
                            DICElement.AddToBDic(Reader);
                            return DICElement;
                        }
                default://("e")
                        return null;
                }
        }
    }

Последний класс, «сердце» нашей структуры — FileBEncoding
В нем и будет реализован алгоритм чтения BEncodeзаписи XML.

Для начала реализуем чтение.
Пусть данный класс будет содержать:

  • Список BElement (т.к. .torrent файлы, обычно имеют только один элемент, можно задать просто переменную типа BElement)
  • Индексацию (вариант списка)
  • Конструктор со строкой, описывающую путь к файлу
  public class FileBEncoding
    {
       List<BElement> BenItems;//Если подразумеваем только один элемент на файл, то пишем BElement BenItem

       BElement this[int index]
       {
           get
           {
               if (BenItems.Count > index)
                   return BenItems[index];
               return null;
           }
           set
           {
               if (BenItems.Count > index)
               {
                   BenItems[index] = value;
               }
               else throw new Exception("Выход за пределы. Ничего не записано");
           }
       }

       public FileBEncoding(string Path)
       {
           if (!File.Exists(Path)) return;
           BenItems = new List<BElement>();
           StreamReader Reader = new StreamReader(Path, Encoding.ASCII);
           while (!Reader.EndOfStream)
           {
               BElement temp = BElement.GetNewBElement(Reader);
               if (temp != null)
                   BenItems.Add(temp);
           }
           Reader.Close();
       }

Более понятный вывод в строку

Если Вам хочется уже посмотреть вывод в xml, то данную часть можно пропустить.
Здесь хотелось бы предложить «структурированный» вывод информации в файлконсоль.
Что для этого нам понадобится?
Все классы в C# производные от класса Object. Данный класс имеет метод ToString(). По умолчанию данный метод выводит имя типа. Переопределим его.

       private string BElementToSTR(BElement CurrentElement, int TabCount, bool Ignore = true)
       {
           //для корректной обработки некорректных файлов
           if (CurrentElement == null) return "";//Так как выше по ходу, при неверное структуре файла не выдавали исключение. 

           string Result = "";//строка результата
           if (Ignore)//табуляция строки для словаря не нужна
           PasteTab(ref Result, TabCount);
           if (CurrentElement.STDItem != null)
           {
               Result += CurrentElement.STDItem.ToString();
               return Result;
           }
           if (CurrentElement.LSTItem != null)
           {//обработка списка
               Result += "List{n";
               for (int i = 0; i < CurrentElement.LSTItem.Count; i++)
                   Result += BElementToSTR(CurrentElement.LSTItem[i], TabCount + 1) + 'n';
               PasteTab(ref Result, TabCount);
               Result += "}Listn";
               return Result;
           }
           if (CurrentElement.DICItem != null)
           {//обработка словаря
               Result += "Dict{n";
               for (int i = 0; i < CurrentElement.DICItem.Count; i++)
               {
                   Result += BElementToSTR(CurrentElement.DICItem[i][0], TabCount + 1) +" => "+ BElementToSTR(CurrentElement.DICItem[i][1], TabCount+1,false) + 'n';
               }
               PasteTab(ref Result, TabCount);
               Result += "}Dictn";
               return Result;
           }
           return "";//Если все элементы null, то возвратим пустую строку
       }

       private string PasteTab(ref string STR,int count)
       {//табуляция
           for (int i = 0; i < count; i++)
               STR += 't';
            return STR;
       }

       public string ToString()
       {
           string Result = "";
           for (int i = 0; i < BenItems.Count; i++)
           {
               Result += BElementToSTR(BenItems[i], 0) + "nn";
           }
           return Result;
       }

Здесь мы использовали дополнительно 2 функции. Первая служит для обработки отдельно взятого BElement (можно заметить, что она рекурсивна), вторая — для создания отступов.

Создаем XML файл

Прежде чем приступим к кодированию, хотелось бы сказать пару слов о самом языке XML.
Данный язык получил весьма широкое распространение как единый язык инфо-обмена. В C# (а точнее платформе .NET) реализована прекрасная поддержка XML. Для удобной работы используем встроенные средства, подключив пространство имен System.XML.

Есть целый ряд способов создать XML-документ. Мой выбор пал на класс XmlWriter. Данный класс позволяет создать объект «с пустого места». Причем происходит запись каждого элемента и аттрибута по порядку. Главным преимуществом данного способа является высокая скорость работы.

Для создания документа определим два метода
void ToXMLFile(string path) и void BElementToXML(BElement Current, XmlWriter Writer, int order = 0)
Первый метод будет создавать объект нужного нам XmlWriter класса и вызывать BElementToXML для каждого элемента из списка.
Второй метод занимается «раскручиванием» BElement (если он списоксловарь) и, собственно, формированием файла.

    private void BElementToXML(BElement Current, XmlWriter Writer, int order = 0)
       {
           if (Current == null) return;//корректная обработка некорректных файлов
           if (Current.STDItem != null)
           {//простой элемент запишем как аттрибут
               Writer.WriteAttributeString("STDType"+'_'+order.ToString(), Current.STDItem.ToString());
               return;
           }
           if (Current.LSTItem != null)
           {//список
               Writer.WriteStartElement("List");//данный метод записывает ноду <List>
               for (int i = 0; i < Current.LSTItem.Count; i++)
                   BElementToXML(Current.LSTItem[i],Writer,order);//рекурсивное раскручивание
               Writer.WriteEndElement();//закрываем ноду </List>
               return;
           }
           if (Current.DICItem != null)
           {//словарь (аналогично списку)
               Writer.WriteStartElement("Dictionary");
               for (int i = 0; i < Current.DICItem.Count; i++)
               {
                   Writer.WriteStartElement("Dictionary_Items");
                   BElementToXML(Current.DICItem[i][0], Writer,order);
                   BElementToXML(Current.DICItem[i][1], Writer,order+1);
                   Writer.WriteEndElement();
               }
               Writer.WriteEndElement();
               return;
           }
           return;
       }
       public void ToXMLFile(string path)
       {
           using (XmlTextWriter XMLwr = new XmlTextWriter(path, System.Text.Encoding.Unicode))
           {
               XMLwr.Formatting = Formatting.Indented;
               XMLwr.WriteStartElement("Bencode_to_XML");
               foreach (BElement X in BenItems)
               {
                   XMLwr.WriteStartElement("BenItem");
                   BElementToXML(X, XMLwr);
                   XMLwr.WriteEndElement();
               }
               XMLwr.WriteEndElement();    
           }
       }

В итоге получаем более удобные способы восприятия BEncode-файла:
(Для примера использовал торрент-файл ubuntu. SHA ключи удалил.)

Консоль: (метод ToString())
Переводим из Bencode в XML

Excel: (xml)
Переводим из Bencode в XML

Заключение

В данном небольшой статье — уроку мы научились: обрабатывать BEncode файлы, формировать XML файл.
А создание из BEncode файла — XML может пригодиться, к примеру, для написания редактора BEncode-файлов. Здесь собранный воедино код

Литература

  • Википедия — Bencode-файлы
  • Бен Ватсон — C# 4.0

Автор: xnim

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js