PHP

индекс
206,76

RAR: получение списка файлов без PECL

Не так давно я писал о получении текста из всевозможных файловых форматов, будь то DOC или PDF. Сегодня мы рассмотрим не менее интересный формат — формат сжатия RAR. Не буду обнадёживать страждущих — сегодня мы только прочитаем список файлов без каких-либо дополнительных расширений PHP. Итак, кому интересно, прошу под кат...

RAR — хороший «плохой» архиватор


Напомню, что RAR разрабатывается нашим соотечественником Евгением Рошалом. От него же и получил своё имя Roshal Archiver. Формат закрытый, что абсолютно не сказалось на его распространении как в России, так и по миру. Почти все рабочие станции, что мне приходилось видеть, были с установленным и подчас крякнутым архиватором RAR.

За время своей разработки и бытности архиватор дорос до 3ей (предположу, что скоро будет и 4ая) версии, что сказалось на большинстве «самопальных» разархиваторов: третья версия привнесла новые алгоритмы сжатия, от чего последние впадали в паранойю и ересь. Тем не менее сайт разработчика содержит достаточный объём всевозможных исходных кодов для разархивации RAR-файлов под разные платформы и среды разработки.

Что до PHP, то PECL-расширение доросло до «стабильной» первой версии и редко когда установлено на хостингах. Расширение, кстати, использует тот самый «unrar», чьи исходные коды лежат на сайте программы. Более того, признаюсь честно, мне не удалось запинать заставить работать расширение под 5.3 (под Windows), под 5.2.11 php_rar.dll заработало, но большинство архивов прочитать не смогло. Не удивлюсь, что все варианты скомпилированной библиотеки под Windows-систему были для «какой-то» другой версии, а компилировать самому не хотелось… поэтому под вечер я сел поглядеть, да посмотреть, что представляет из себя unrar.dll, что можно собрать из исходников на сайте.

RAR — как это?


В связи с закрытостью формата — документация по нему скудная, даже не смотря на тот факт, что исходные коды для разжатия данных есть. Что ж неудивительно — порядка 600 кб исходников рассматривать мало кому захочется. Но тем не менее энтузиасты таки есть (боже упаси, если вы подумали на мою персону :) — поэтому в своё время был создан проект The UniquE RAR File Library, который в разы сократил исходные коды для разархивации файлов, созданных 2ой версией архиватора.

Так вот мне попались на глаза исходники вышеупомянутой библиотеки, а также минимальная, но хоть какая-то, документация по престарелой 2.02 версии архиватора. Что ж, давайте погрузимся в то, как выглядят наши RAR-архивы.

RAR-архив состоит из блоков переменной длины с заголовками по 7 байт каждый. Любой архив содержит как минимум два блока MARK_HEAD и MAIN_HEAD. Первый содержит информацию о том, что перед нами RAR, и выглядит как "52 61 72 21 1a 07 00" в HEX'ах. Третий байт 0х72 как раз таки указывает на то, что это Marker Header. Слово 00 07 в little-endian содержит длину блока. Как раз таки 7 байт.

Второй блок Main Header начинается сразу же после первого и должен содержать 13 байт и иметь маркировочный байт равным 0x73. После него в файле уже начинаются данные — будь-то сжатый файл (маркет 0х74 в третьем байте заголовка блока), комментарий к архиву, дополнительная информация или, к примеру, recovery-запись.

Алгоритм получения списка файлов не сложен (если не брать в расчёт архивы с шифрованной структурой каталогов, чтение которых осталось за рамками этой статьи).

  1. Читаем первые семь байт заголовка. Находим там длину заголовка и дочитываем его до конца;
  2. Проверяем является ли блок «файловым»;
  3. Если «Да», то DWORD на седьмой позиции размер заархивированного файла (а также тот объём данных, что нужно прочесть до следующего блока), следующее двойное слово — размер исходного файла, на позиции №28 — лежат аттрибуты файла (DWORD), а по адресам 26 и 32 находится длина имени файла (2 байта) и само имя. Кроме того, там можно найти дату создания, код ОС в которой был создан файл и CRC;
  4. Если же блок не «файловый», то мы должны прочитать слово на третьей позиции и проверить значение его 15ого бита, что отвечает за дополнительный объём информации, что может идти с блоком. В случае «1» по этой позиции, мы должны пропустить ADD_SIZE байтов (первое двойное слово после заголовка блока);
  5. И так до конца файла...


Сложно? Не очень, в сравнении с каким-нибудь DOC-файлов.

Исходный код


  1. // Функция чтения списка файлов из $filename без использования
  2. // PECL-расширения rar.
  3. function rar_getFileList($filename) {
  4.     // Функция для получения COUNT байтов из строки (little-endian).
  5.     // Чтобы не засорять глобальное пространство функций - отправляем её 
  6.     // вовнуть материнской.
  7.     if (!function_exists("temp_getBytes")) {
  8.         function temp_getBytes($data, $from, $count) {
  9.             $string = substr($data, $from, $count);
  10.             $string = strrev($string);
  11.  
  12.             return hexdec(bin2hex($string));
  13.         }
  14.     }
  15.  
  16.     // Попытка открыть файл
  17.     $id = fopen($filename, "rb");
  18.     if (!$id)
  19.         return false;
  20.  
  21.     // Проверка - является ли файл RAR-архивом
  22.     $markHead = fread($id, 7);
  23.     if (bin2hex($markHead) != "526172211a0700")
  24.         return false;
  25.  
  26.     // Пытаемся прочесть MAIN_HEAD блок
  27.     $mainHead = fread($id, 7);
  28.     if (ord($mainHead[2]) != 0x73)
  29.         return false;
  30.     $headSize = temp_getBytes($mainHead, 5, 2);
  31.  
  32.     // Сдвигаемся на позицию первого "значащего" блока в файле
  33.     fseek($id, $headSize - 7, SEEK_CUR);
  34.  
  35.     $files = array();
  36.     while(!feof($id)) {
  37.         // Читаем загловок блока
  38.         $block = fread($id, 7);
  39.         $headSize = temp_getBytes($block, 5, 2);
  40.         if ($headSize <= 7)
  41.             break;
  42.  
  43.         // Дочитываем остаток блока исходя из длины заголовка по 
  44.         // соответствующему смещению
  45.         $block .= fread($id, $headSize - 7);
  46.         // Если это файловый блок, то начинаем его обрабатывать
  47.         if (ord($block[2]) == 0x74) {
  48.             // Смотрим сколько занимает в архиве запакованный файл и
  49.             // смещаемся к следующей позиции.
  50.             $packSize = temp_getBytes($block, 7, 4);
  51.             fseek($id, $packSize, SEEK_CUR);
  52.  
  53.             // Читаем атрибуты файла: r - read only, h - hidden,
  54.             // s - system, d - directory, a - archived
  55.             $attr = temp_getBytes($block, 28, 4);
  56.             $attributes = "";
  57.             if ($attr & 0x01)
  58.                 $attributes .= "r";
  59.             if ($attr & 0x02)
  60.                 $attributes .= "h";
  61.             if ($attr & 0x04)
  62.                 $attributes .= "s";
  63.             if ($attr & 0x10 || $attr & 0x4000)
  64.                 $attributes = "d";
  65.             if ($attr & 0x20)
  66.                 $attributes .= "a";
  67.  
  68.             // Читаем имя файла, размеры до и после запаковки, CRC и аттрибуты
  69.             $files[] = array(
  70.                 "filename" => substr($block, 32, temp_getBytes($block, 26, 2)),
  71.                 "size_compressed" => $packSize,
  72.                 "size_uncompressed" => temp_getBytes($block, 11, 4),
  73.                 "crc" => temp_getBytes($block, 16, 4),
  74.                 "attributes" => $attributes,
  75.             );
  76.         } else {
  77.             // Если данный блок не файловый, то пропускаем с учётом возможного
  78.             // дополнительного смещения ADD_SIZE
  79.             $flags = temp_getBytes($block, 3, 2);
  80.             if ($flags & 0x8000) {
  81.                 $addSize = temp_getBytes($block, 7, 4);
  82.                 fseek($id, $addSize, SEEK_CUR);
  83.             }
  84.         }
  85.     }
  86.     fclose($id);
  87.  
  88.     // Возвращаем список файлов
  89.     return $files;
  90. }


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

Литература


Ну и как обычно литература для ознакомления:



Перспективы


Что до чтения файлов из архивов, то… это теоретически можно сделать на PHP путём рефакторинга библиотеки от UniquE, но это подойдёт лишь для архивов, созданных версией до 2.90. Новые архивы библиотека не прочитает… а разбираться в полутысяче килобайт кода вы сами понимаете.
+28
28 октября 2009, 19:31
55

комментарии (31)

–4
DonRamon #
Позволю себе чуть-чуть обнаглеть — в общей шумихе и суматохе во время раздачи инвайтов на Google.Wave мне так ничего не досталось. Если вдруг у кого-нибудь завалялся не нужный, я бы с радостью принял бы оный в дар :) Спасибо.
+6
grayhex #
Уровнём выше — комментарий автора поста.
0
DonRamon #
Я понимаю Вашу иронию и понимаю за что мне поставили минусы. Дело в том, что как разработчику очень хочется глянуть на новые технологии, ан нет — никак. Что ж, раз никак — то никак.
0
psylosss #
Если бы я мог, я бы дал вам инвайт и заплюсовал бы комментарий так, чтобы на него обратили внимание инвайт-имущие. Но увы :(

P.S. действительно, как-то некрасиво и несправедливо поступила масса.
+1
akral #
Я думаю, это не была ирония, а наоборот — попытка сдержать массы минусующих.
+1
Mirowind #
Господа, это, так сказать, не очень красиво заминусовывать комментарий, написанный автором статьи ;)
+1
psylosss #
Спасибо за статью, у меня в голове наконец стало проясняться, почему же все так плохо с RAR.
+1
Fredy314 #
я было как-то от нечего делать написал функции для РНР для архивации файла в многотомный архив, простым сохранением без сжатия, за-то без использования любых сторонних библиотек или расширений. единственно что плохо так то что CRC32 пришлось вычислять отдельной функцией так как встроеная в РНР не вычисляла CRC32 по частям.
Если тема интересует могу найти свои исходники и выложить.
0
akalend #
думаю что будет полезно посмотреть исходники
34/64 платформа как-то влияет на определение CRC32?
0
DonRamon #
Хм… Хм?.. Хм! Какой Вы молодец, я даже не сразу же сообразил, что используя store вполне себе можно «заархивировать» данные в rar. Надо будет поразвлекаться на досуге.
0
Arris #
Интересует, если вам нетрудно — поищите пожалуйста.
0
Fredy314 #
К сожалению в результате поиска исходников не обнаружено, единственно нашел текстовик с описанием формата.
–5
okopok #
Так и не понял чем вложенная функция лучше, чем всё это в класс обернуть :)
Сколько лет пхпую, так и не видел такой вот конструкции
if ($attr & 0x01)
но это нюансы стиля написания конечно. Програмку завтра проверю, если так хорошо работатет, то респект и уважуха. Останется только допилять архивацию и дезархивацию :)
+5
DonRamon #
Огоспаде, сударь, Вы читали статью? RAR закрытый формат, ни один сторонний продукт не может заархивировать в rar без dll-ки с сайта rarlabs.com. В статье же рассказывается о способе получения списка заархивированных файлов из rar-архива без сторонних dll, что вполне по силам PHP или любому другому продукту — исходные коды unrar открыты.

Что до ($a & 0x01) — это проверка заданного бита. Скорее всего Вы мало работали с бинарно представленными данными, отсюда и «не видел конструкции».
0
DonRamon #
Кроме «store-сжатие», естественно, как отмечалось выше.
–1
DonRamon #
Ну и к вопросу о вложенных процедурах — зачем плодить классы, где они не нужны?
+1
LoneCat #
Конечно незачем, но только здесь они — нужны. И вообще везде где всплывает этот аргумент, классы — нужны. Объектно-ориентированная методология это не кунг-фу, который нужно использовать только в случае крайней необходимости, это штатная замена методологии процедурного программирования.
З.Ы. Насчет вложенных процедур — afaik в PHP их нет, и любая функция объявленная внутри другой функции — все равно таки окажется в глобальной области видимости. А возможность объявлять функции внутри функций дана для того чтобы объявлять функции например по условию, иже:
function define_sinus($wrong = false) {
  if($wrong) {
    function sinus($angle) {
      return cos($angle);
    }
  } else {
    function sinus($ange) {
      return sin($angle);
    }
  }
}
0
DonRamon #
Так, специально проверил на echo temp_getBytes("abcd", 1, 2);, вызванной снаружи скрипта — ответ PHP 5.3 однозначен: Fatal error: Call to undefined function temp_getBytes().

+1
DonRamon #
Что-то нажало мне Enter. Продолжаю. :)

Что до использования классов в данном конкретном случае. Я абсолютно не против использования классов, но здесь функция получения нескольких байтов из строки с учётом little-endian и функция сортировки дерева по ключам абсолютно не к месту в глобальном пространстве имён, но расширять из-за них две самодостаточные функции из модуля до класса, совершенно не нужно. Использование классов — не панацея, а как вы правильно заметили, другое кунг-фу. Здесь я посчитал это кунг-фу избыточным. Кроме того, в конечном итоге, это не важно — функционал модуля выдержан, пользуйтесь на здоровье :)
+3
LoneCat #
Это я нажал вам Enter :P
Насчет самодостаточности — да, функции самодостаточны, выполняют то что нужно, и не выполняют того что не нужно, однако-же будь это класс, например использующий типовое решение «итератор», так как по-сути функция им и является, иже бежит по файлу вычленяя из него содержащуюся в нем информацию — его можно было-бы в последствии расширить, например недостающим функционалом распаковки, поиском определенного файла и т.д. и т.п., в случае-же процедуры — чтобы это сделать — нужно лезть потными ручками в этот самый рабочий, самодостаточный функционал, который после этого не суть что останется рабочим и самодостаточным :)
З.Ы. Ну и это конечно ни в коем случае не обвинения, за функционал спасибо, было интересно посмотреть на принцип его работы, вы молодец, однако не могу удержаться от холивара когда говорят что классы «здесь не нужны», если использовать ООП-методологию — они имхо везде нужны.
0
Arris #
> если использовать ООП-методологию — они имхо везде нужны.
уж не удержусь: и даже в программе Hello World!? ;-)
0
LoneCat #
public class HelloWorld {
  public static void main(String[] args) {
    System.out.println("Hello, world!");
  }
}
Helloworld на Java, пока от него вроде никто не умер. От продолжения ответа удержусь — не вижу смысл метать бисер.
0
Arris #
/me обреченно возводит руки к небесам.
+1
LoneCat #
Ну так правильно :) Чтобы вызвать функцию снаружи скрипта — ее нужно объявить, а объявляется она внутри другой функции, приведу простой пример:
function outer() {
	function inner() {
		echo "tratata";
	}
}

outer();
inner();

Выведет «tratata», а вот
function outer() {
	function inner() {
		echo "tratata";
	}
}

inner();

Выведет критическую ошибку, потому что пока outer не вызвана — inner не существует, однако когда outer будет вызвана — inner будет существовать в глобальном пространстве имен.
0
DonRamon #
Уговорили. Буду знать на будущее. Тогда стоит добавить if (!function_exists()). Спасибо, век живи — век учись.
0
okopok #
С бинарными данными действительно мало работал. Собсно сейчас работаю в команде, где очень чёткое структурирование кода идёт, очень всё стандартизировано вплоть до «эта скобочка сюда, эта сюда», поэтому глаз и целпяется за такие вот конструкции.

В общем, прошу извинить, глаза после рабочего дня уж замылены :)
0
Lux_In_Tenebris #
С каждым дистрибутивом RAR'а (по крайней мере WinRAR) идёт описание формата RAR архивов. Не пойму, какой вообще смысл уповать на чьи-то древнейшие разработки, которые не умеют читать архивы RAR 3.x (между прочим, введённый в обращение уже ой как давно), если можно совсем чуть-чуть покопавшись написать собственную «читалку»?
Читать же бинарники средствами самого PHP, IMHO, моветон. :)
0
Bonch #
Читать же бинарники средствами самого PHP, IMHO, моветон. :)


И что в этом плохого? Какая разница чем их читать? Можете, конечно, написать консольную программу хоть на С, и вызывать ее из PHP, однако смысл?
0
DonRamon #
Эх, Сударь-Сударь. Знали бы какие проблемы возникают во время разработки — хочу чтобы я загрузил rar-архив (указал на rar на сервере), а оно оттуда весь текст достало? Не знакомая ситуация? Тогда представьте, что у вас не выделенный хостинг, а какой-то проплаченный «у дяди», и всё встанет на свои места.

Ну а к документации, ссылку на которую Вы дали. Ха-ха, Сударь! Она отличается от той английской тем, что она русская и что добавлены новые флаги, что были введены с 3.х версиями. Попробуйте с помощью неё разархивировать данные. Попробуйте-попробуйте.
0
Lux_In_Tenebris #
Не знакомая ситуация? Тогда представьте, что у вас не выделенный хостинг, а какой-то проплаченный «у дяди», и всё встанет на свои места.

Можно конечно всю жизнь героически бороться с ограничениями российских shared-хостингов за $3-4, которые на начальных тарифах могут даже PHP+MySQL не предоставлять, а можно один раз подойти к вопросу выбора посерьёзней и остановиться на одном наиболее вменяемом, не имеющего ограничений ни на запуск внешних программ (например, чтобы парсить вывод unrar), ни на подключение произвольных PHP extensions.
Помню, когда мне требовалось оперировать RAR архивами на Linux shared-хостинге, я обычно заливал туда консольную версию RAR и пользовался ей через PHP/CGI web-shell.

Ну а к документации, ссылку на которую Вы дали. Ха-ха, Сударь! Она отличается от той английской тем, что она русская и что добавлены новые флаги, что были введены с 3.х версиями.

Я не заострял внимание на содержании указанного документа (к дистрибутиву архиватора по любому прилагается наиболее актуальная версия), хотел лишь отметить, что описание файлового формата RAR-архивов вполне себе общедоступно и располагает к написанию собственных парсеров, а не допиливания каких-то сомнительных сторонних решений.
0
DonRamon #
В данном случае речь идёт абсолютно не о Российских и СНГшных хостинг-провайдерах. Я живу в Праге, общаюсь с пражанами, где у них сервера мне в конечном итоге всё равно — ставятся задачи, я их решаю в поставленных рамках.

Повторюсь, что данная спецификация рассказывает лишь о блочной структуре rar-файла. Прочитав файловый блок, нужно его потом ещё и разархивировать. Если вы знаете, где можно найти неисходный код, а принцип разархивации, то я тоже хочу эту ссылку — сильно-сильно. И скажу Вам за неё спасибо, нет — так о чём разговор-то опять.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.