Assembler
.NET
Delphi
Windows
Reversing&Cracking
Шаолинь
Other
Форум Monah'а

Примитивный парсер .avi-файла

Автор: Adrax

С надавних пор увлекли меня форматы медиафайлов. Интересно стало: как же внутри устроен тот же .avi, .vob или .mp3
И вот, странное дело: вроде бы, и форматы открытые, некоммерческие (почти все), и компонентов для работы с ними на современных языках программирования - хоть задницей ешь (для той же студии - и стандартный MediaPlayer, и Managed DirectX с кучей возможностей), и сорцов готовых в Сети немало. Только в сорцах тех нету совсем нормальных комментариев, а документации по форматам не то, что на русском - на английском днём с огнём не сыщешь! Вот и приходится по Сети рыскать, в чужом коде ковыряться (отдельное спасибо авторам VirtualDub - такая мощная прога, да ещё и Open Source!), MSDN взатяг курить - ради жалких крох информации. Но во время таких суматошных поисков какая-то ясность в голове наступает. И пока эта ясность не рассеялась - я пишу эту статью:)

Давайте-ка смастерим небольшой парсер .avi-файла - пусть он показывает нам содержимое его заголовков и визуализирует структуры данных
Начальные понятия о формате .avi вы можете почерпнуть из этой статьи. Лично я пользовался именно ей:) плюс распотрошил hex-редактором все имеющиеся у меня .avi-шники

В данный момент я экспериментирую, создавая парсеры на C# и Ассемблере. Перед вами самый первый рабочий прототип: он парсит только .avi-файлы "старого" формата и не справляется с вложенными LIST'ами, так что практической ценности не имеет. Тем не менее, он может оказаться полезным для тех, кто делает свои первые шаги в этой области - поэтому я приведу его полный исходный код в этой статье

GUI

Запускаем студию, командуем создать New Visual C# Project, уточняем - Windows Application. Кидаем на форму следующие компоненты: Menu, TreeView, OpenFileDialog

Форму я назвал "Форма" и указал её размер 392*88. В заголовке написал AVI_analyzer v.0.0.0.1 :)
TreeView я обозвал "Дерево" (а что? похож!) и свойство Visible установил в False
OpenFileDialog получил имя "Open", а его свойство Filter я установил "Файл AVI|*.avi|Все файлы|*.*"
Menu получило имя "Меню" и следующие пункты: Item1 (само меню, с текстом "Файл"), Item2 (с текстом "Открыть"; свойство Shortcut я установил CtrlO, чтобы этот пункт меню срабатывал по нажатию горячих клавиш Ctrl+O) и Item3 ("Выход", CtrlX)

Код

Чтобы не отвлекаться на мелочи, давайте сразу оформим обработчик мышиного клика по Item3 - по образцу из прошлой статьи:

private void Item3_Click(object sender, System.EventArgs e)
   {
    if (MessageBox.Show("Хотите выйти из программы?",
                        "Завершение",
                        MessageBoxButtons.YesNo)==DialogResult.Yes)
                        Application.Exit(); 
   }

Теперь поразмыслим - как же мы будем парсить .avi-файл? Для начала давайте вспомним его структуру. Типичный .avi-файл схематично можно представить так:

Схема, конечно, кривовата, но суть отображает верно

Выше приведена структура RIFF "AVI "-чанка. В "старых" .avi-файлах такой чанк был единственным. В случае .avi 2-го поколения за вышеописанной структурой могут следовать вот такие чанки:

Приведённый здесь пример парсера будет читать только самый первый RIFF "AVI "-чанк
Для добавления новых элементов (узлов) в TreeView, мы будем использовать самописный метод DobUzel(), принимающий строковые данные и создающий узел с таким текстом:

public void DobUzel(string text)
  {
    TreeNode node = Дерево.SelectedNode;
    //получаем выделенный узел
    if (node==null) {TreeNode nod = Дерево.Nodes.Add(text); Дерево.SelectedNode = nod;}
    //если нет выделенного узла - мы в корне дерева,
    //создаём новый узел и выделяем его
    else {TreeNode nod = node.Nodes.Add(text); Дерево.SelectedNode = nod;}
    //иначе добавляем подузел к выделенному узлу
    //и выделяем подузел
  }

Ещё нам необходимо учесть, что данные в RIFF-файлах выровнены на границу слова (2-х байт), т.е. если мы получили нечётный размер чанка, необходимо добавить к нему 1, чтобы получилось чётное число - иначе следующую порцию данных мы считаем с неправильной позиции. Для выравнивания создадим такой метод:

public int Aligned(int a)
  {
    if ((a&1)!=0) a++;
    return a;
  }

Для непосредственно парсинга .avi-файла я решил написать отдельный класс и незамысловато назвал его Parser (идея была подсмотрена у Giora Tamir). Объявляем класс так:

public class Parser
  {
    private byte[] восемьбайт = new byte[8];
    private byte[] четыребайта = new byte[4];
    FileStream stream;
  }

Эти два байтовых массива (восемьбайт и четыребайта) нам конкретно пригодятся. В переменной stream у нас будет поток, из которого мы будем читать данные (т.е. открытый файл). Теперь начнём заполнять наш класс методами, придавая ему работоспособность

Ксати, непередаваемое ощущение:). Вы фильм "Восставший из Ада" (Hellraiser) смотрели? Там были такие красивые кадры воскрешения: кости обрастают мясом, мясо покрывается кожей - и вот, восставший мертвец вернулся в мир живых...
При написании нового класса у меня возникает чувство, что я своими руками ввожу в этот мир новое живое существо. Класс потихоньку обрастает методами - как скелет мясом - и через некоторое время он уже держит на себе весь функционал программы. Наверное, ради этого чувства я и пишу программы... А, возможно, я просто извращенец...

Все методы класса будут иметь доступ к переменным класса - т.е. к массивам восемьбайт и четыребайта и к файловому потоку stream

Но сперва этот файловый поток надо будет создать. Поэтому первым методом класса Parser будет OpenFile(), получающий строку с именем файла и открывающий файд на чтение:

public FileStream OpenFile(string filename)
   {
     stream = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read);
     return stream;
   }

Имя файла мы будем получать из диалога открытия файла

Следующие два метода будут заниматься побайтовым чтением из файла в байтовые массивы, а из байтовых массивов - в переменные. Придётся использовать указатели, и мы столкнёмся с сопротивлением со стороны компилятора, который не пожелает компилировать такой код. Что ж, придётся, во-первых, объявить эти методы небезопасными (unsafe) и разрешить компиляцию небезопасного кода (в окошке справа переключиться с Solution Explorer на Class View, щёлкнуть правой кнопкой мыши, в Properties найти Allow unsafe code blocks и выставить True). После этого мы сможем напрямую работать с памятью по указателям
Итак, эти методы:

public unsafe void ReadTwoInts(out int fourcc, out int size)                            
   {
     int прочитано = stream.Read(восемьбайт,0,8);
     if (прочитано!=8) 
       {
        MessageBox.Show("Ошибка чтения файла","Ошибка!",MessageBoxButtons.OK);
       }
    fixed (byte* bp = &восемьбайт[0]) 
       {
        fourcc = *((int*)bp);
        size = *((int*)(bp + 4));
       }
   }

              
public unsafe void ReadOneInt(out int fourcc)
   {
    int прочитано = stream.Read(четыребайта,0,4);
    if (прочитано!=4) 
       {
        MessageBox.Show("Ошибка чтения файла","Ошибка!",MessageBoxButtons.OK);
       }
    fixed (byte* bp = &четыребайта[0]) 
       {
        fourcc = *((int*)bp);
       }
   }

Метод ReadTwoInts() придётся, видимо, прокомментировать. Этот метод будет читать из файлового потока 8 байт и возвращать их в двух переменных типа int. Используя стандартный механизм (т.е. указав тип возвращаемого результата), мы можем вернуть только одну переменную, поэтому поступим иначе - объявим метод как void, а возвращать переменные будем с помощью ключевого слова out
В теле метода мы читаем 8 байт из файлового потока в байтовый массив восемьбайт. Если 8 байт прочитать не удалось - выдаём ошибку. Если всё в порядке - создаём указатель на первый байт массива. Здесь необходимо использовать ключевое слово fixed. Дело в том, что CLR (среда исполнения .NET) в целях оптимизации может располагать элементы массива или поля структуры в произвольном порядке. Нам же необходимо, чтобы элементы массива располагались один за другим - потому и используем fixed
Ну а дальше всё просто: считываем FourCC (сигнатуру), передвигаем указатель на 4 байта и считываем поле размера
Метод ReadOneInt() отличается от ReadTwoInts() тем, что считывает не 8, а 4 байта

Мы получаем FourCC чанка в виде числа, но нам необходимо его строковое представление - для этой цели служит следующий метод:

public string FourCC(int fourcc)
   {
     char[] chars = new char[4];
     chars[0] = (char)(fourcc & 0xFF);
     chars[1] = (char)((fourcc >> 8) & 0xFF);
     chars[2] = (char)((fourcc >> 16) & 0xFF);
     chars[3] = (char)((fourcc >> 24) & 0xFF);
     return new string(chars);
  }

Логика проста: создаём массив из 4-х char'ов. Далее побайтово делаем для FourCC AND с FFh и заносим в массив

Ещё нам понадобится метод для того, чтобы смещать позицию чтения внутри файла. Я написал для этого метод SkipData(), юзающий стандартный метод Seek(), принимающий два параметра: количество байт, на которое нужно сместиться, и стартовую позицию. Стартовую позицию задаём SeekOrigin.Current (т.е. текущая) и будем передавать методу SkipData() только количество байт:

public void SkipData(int сколько_байт)
   {
     stream.Seek(сколько_байт,SeekOrigin.Current);
   }

Метод для закрытия файла совсем простенький:

public void CloseFile()
   {
    stream.Close();
   }

Ну, и ещё один метод, проверяющий сигнатуры "RIFF" и "AVI ":

public int ReadRiff()
   {
     int fourcc, size, filetype;
     ReadTwoInts(out fourcc, out size);
     ReadOneInt(out filetype);
       if ((FourCC(fourcc)!="RIFF")||(FourCC(filetype)!="AVI "))
        {
         MessageBox.Show("Не AVI","Неверный файл",MessageBoxButtons.OK);
         CloseFile();
         return 0;
        }
     return size;
   }

Его логика предельно проста. Объявляем три переменных типа int, считываем в них сигнатуру "RIFF", размер RIFF-чанка и сигнатуру "AVI ". Если не "RIFF", и не "AVI " - объявляем об ошибке, иначе - возвращаем размер RIFF-чанка

Итак, все самописные методы перед вами. Осталось лишь грамотно использовать их - и распарсить-таки этот проклятый .avi!
Вот он: обработчик мышиного клика по Item2:

private void Item2_Click(object sender, System.EventArgs e)	
  {
   string name = "AVI_analyzer v.0.0.0.1";
   int riffavisz,type,elfourcc,elsize;			

       Parser x = new Parser();
//создаём объект x - экземпляр класса Parser
       Дерево.Nodes.Clear();
       this.Text = name;
//очистили Дерево, дали Форме заголовок
       if (Open.ShowDialog() == DialogResult.OK)
	   {
	     string filename = Open.FileName;
             //срисовали имя файла из диалога открытия
	     FileInfo fi = new FileInfo(filename);
	     long filesize = fi.Length;
	     this.Text += " - " + fi.Name;
             //добавили имя файла в заголовок окна
	     FileStream stream = x.OpenFile(filename);
             //открываем файл для чтения
	     riffavisz = x.ReadRiff();
             //получаем размер RIFF "AVI "-чанка
	     if (riffavisz==0)
		{
		  x.CloseFile();
		  this.Text = name;
		}
             /*
             если облажались - закрываем файл
             и приводим заголовок к
             изначальному виду
             */ 
	     else
		{
		  this.Height = 685;
		  Дерево.Visible = true;
             /*
             а в этом я усмотрел красоту:)
             окно резко растягивается в высоту,
             и в его нижней части становится видимым
             TreeView
             */
		  DobUzel("RIFF"); DobUzel("AVI ");
             	  int bytesleft=riffavisz-12;
             /*
             в переменной bytesleft храним
             количество байт, оставшееся
             до конца RIFF "AVI "-чанка.
             Будет для нас главным счётчиком:
             стала равной нулю - заканчиваем
             */
		  int size,fourcc;
		  m1:
		  x.ReadTwoInts(out fourcc, out size);
                  //считали FourCC и поле размера
		  bytesleft-=8;
		  if (bytesleft<0) goto Out;
		  else
		     {
		      if (x.FourCC(fourcc)=="LIST")
                      //LIST-чанк
			  {
			   x.ReadOneInt(out type);
                           //считали сигнатуру
			   bytesleft-=4; size-=4;
			   DobUzel("LIST");
			   DobUzel(x.FourCC(type));
                           //добавили узлы
			   if (bytesleft<0) goto Out;
			       else
				  {
				   m2:
                                   //парсим LIST
				   x.ReadTwoInts(out elfourcc, out elsize);
                                   /*
                                   считали сигнатуру и размер
                                   вложенного в LIST чанка
                                   */
				   bytesleft-=8; size-=8;
                                   DobUzel(x.FourCC(elfourcc));
                                   //добавили узел
				   x.SkipData(Aligned(elsize));
                                   //переместили позицию чтения
				   bytesleft-=Aligned(elsize); size-=Aligned(elsize);
                                   /*
                                   уменьшаем наш счётчик bytesleft на выровненный размер
                                   прочитанного чанка;
                                   на эту же величину уменьшаем переменную size,
                                   в которую записан размер LIST'а.
                                   Когда size станет равным 0 - LIST закончился
                                   */
				   if (size>0)
                                   //заносим в Дерево элемент LIST'а
				       {
                                        Дерево.SelectedNode = Дерево.SelectedNode.Parent;
                                        goto m2;
                                       }
                                   else

                                   //LIST закончился, поднимаемся по Дереву выше
				       {
                                        Дерево.SelectedNode = Дерево.SelectedNode.Parent;
                                        Дерево.SelectedNode = Дерево.SelectedNode.Parent;
                                        Дерево.SelectedNode = Дерево.SelectedNode.Parent;
                                        goto m1;
                                       }                                                        
                                 }
                          }
                     else

                     //не-LIST-чанк
			  {
                           DobUzel(x.FourCC(fourcc));

                           //добавили узел
			   x.SkipData(Aligned(size));
                           //переместили позицию чтения
			   bytesleft-=Aligned(size);
                           //уменьшили счётчик
			   if (bytesleft<0) goto Out;
                           else
                              {
                               Дерево.SelectedNode = Дерево.SelectedNode.Parent;
                               //поднялись на уровень выше
			       goto m1;
                               //повторно читаем 8 байт
			      }
                          }
                   }                            
             }
                 Out:MessageBox.Show("Анализ RIFF AVI окончен");
        }
  }

Внушает? То-то же! А ведь это - самый примитивный вариант, который даже стандартный C:\Windows\clock.avi неправильно анализирует (потому что у того есть один нестандартный чанк после RIFF "AVI "-шного)


Окно программы после запуска


Окно программы после открытия файла

Напомню, что лажового в этом коде: парсится только первый RIFF "AVI "-чанк, вложенные LIST'ы не парсятся. В принципе, доработать этот код не так уж и сложно. Дам подсказку: неплохо бы написать отдельный метод для парсинга LIST'ов с учётом вложенности. Думаю, в одной из будущих статей я выложу доработанный вариант, ну а пока - жду ваших писем, вопросов, предложений

С наилучшими пожеланиями, Adrax

Главная страница Windows Delphi Assembler .NET Delphi Reversing Шаолинь Other Форум Monah'а

Создатель команды, главный редактор, художник и web-мастер: Adrax

Дизайн сайта: WargaL

Ответственный за форум: Monah

RussianFuckersTeam©