Статьи

Извлечение объектов из базы данных Access с помощью PHP, часть 1

Простой поиск в Интернете показывает, что многим программистам необходимо работать с устаревшими базами данных, такими как Microsoft Access, но они оказываются в тупике, когда сталкиваются с задачей извлечения файлов из поля OLE Object в Access. Частично проблема заключается в недостатке доступной информации о том, как объекты хранятся в поле OLE.

Что еще хуже для программиста PHP, так это то, что каждый экземпляр запросов, публикуемых на форумах и веб-сайтах поддержки, поступает от не-PHP программистов — .Net кажется наиболее распространенным, а также есть вопросы от программистов на Java и Delphi.

Однако у всех этих разработчиков есть одна общая черта — это необходимость извлекать информацию из этих проблемных OLE-полей, чтобы перейти с устаревшей базы данных. Но не одна статья специально предназначена для программистов PHP. Ни один из них не обращается к «пакетам», только к определенным типам данных, таким как JPEG, PNG, GIF и т. Д.

В этой статье мы увидим, как PHP можно использовать для извлечения объектов из двух типов OLE: пакетов и документов Acrobat PDF. Это первая часть серии из двух частей, в которой мы рассмотрим пакеты OLE, которые, как мы увидим, могут быть идентифицированы как «пакет» или «объект оболочки Packager». Значения, выраженные в этой статье, основаны на базе данных Access 2000.

Контейнер OLE

Хранение большого двоичного объекта (большого двоичного объекта) в базе данных никогда не бывает простым делом, и база данных Microsoft Access не является исключением. Давайте суммируем некоторые ключевые аспекты контейнера OLE, используемого при вставке объекта в поле OLE в Access:

  • Ни один внешний файл не был бы вставлен в поле Access OLE без предварительной упаковки в контейнер. Для наших целей этот контейнер может рассматриваться как заголовок перед интересующим нас объектом и трейлер после него.
  • Длина заголовка не фиксирована. Даже для типа пакета OLE длина заголовка может отличаться в зависимости от объекта, встроенного в контейнер.
  • Данные не представлены в удобочитаемой форме, что затрудняет визуальный поиск важных элементов, таких как оригинальное имя встроенного объекта, его имя или начало компонента объекта.
  • Трейлер можно игнорировать. Здесь мы сосредоточимся на компонентах заголовка и объекта.

Еще один момент: кодируется не только компонент объекта, но и заголовок. Прежде чем мы сможем разобраться в каких-либо данных, заголовок должен быть сначала декодирован.

Кодированный заголовок и данные

Тестовая база данных определяется настолько просто, насколько это возможно, со встроенными файлами, содержащимися в поле imageTable1

ole01-1

В этом работающем примере мы будем использовать GIF-логотип Apache, который я сохранил в поле OLE. Извлечение содержимого поля без декодирования, затем проверка выходных данных в шестнадцатеричном средстве просмотра показывает, ну, в общем, бессмысленность:

ole01-2

После преобразования извлеченного заголовка из шестнадцатеричного в десятичное кодирование с помощью PHP-функции hex2dec()

ole01-3

Это выглядит непривлекательно, но ясно, что теперь мы можем разобраться в данных. Чтобы упростить задачу, первые 20 байтов можно игнорировать.

Следующий блок символов говорит нам, что объект OLE имеет тип Packager Shell Object. Пакет OLE — это контейнер общего назначения для тех типов файлов, которые не были распознаны базой данных при вставке файлов.

По словам профессора Фарнсворта: «Хорошие новости, все!». Если мы посмотрим ближе на столбец ASCII шестнадцатеричного дампа, то увидим имя исходного файла, встроенного в контейнер, — apache_02.gif Это будет полезно позже, когда мы попытаемся найти конец заголовка и начало данных, которые нам нужно извлечь. Более того, имя файла всегда начинается с байта 84 заголовка объекта оболочки Packager и с байта 70 заголовка пакета.

Прежде чем перейти к тому, как мы извлекаем GIF из пакета, вот небольшой тизер: в обоих этих двух шестнадцатеричных дампах есть последовательность 0A0600, подчеркнутая желтым. На данный момент, принять, что 0A0600 важно. Мы увидим почему через мгновение.

Извлечение объекта

Прежде чем мы сможем извлечь внедренный объект, нам нужно знать, где он начинается и заканчивается в данных. Тщательная проверка шестнадцатеричного представления показывает, что последовательность символов для имени файла происходит три раза — один раз на байте 84 (или 70), затем снова дважды как часть полного пути.

ole01-4

По крайней мере, так, как кажется в этом примере. Просто чтобы усложнить ситуацию, возможно, что полный путь может появиться только один раз. Кроме того, Access, возможно, изменил регистр пути к файлу или изменил каждый уровень пути к файлу в старомодном формате 8: 3 (это означает, что имена длиннее 8 символов будут усечены, а символы 7 и 8 заменены на ~ 1 ). К счастью, в этом примере всегда будет экземпляр полного пути где-то около местоположения второго полного пути. «Где-то вокруг местоположения?» Так же, как длина заголовка не фиксирована, так же как и места полного пути. Нам нужно обойти эти проблемы.

«Больше хороших новостей, всем!». Последний экземпляр имени файла — второй по важности элемент в заголовке. Он заканчивается нулем (00), после которого шестнадцатеричная последовательность 0A0600, которую мы видели ранее. Это оригинальный размер в байтах внедренного объекта. Он хранится в формате с прямым порядком байтов, поэтому перед его использованием необходимо обратить его к значению с прямым порядком байтов: 0A0600 становится 00060A, что означает 1546 байтов. Эта группа из трех байтов дает максимально допустимый размер встроенного файла 16 МБ.

После этой последовательности есть объект, который нам нужен. Он начинается с последовательности символов 474946383961. Преобразование этого в ASCII приводит к «GIF89a». Он не отображается в столбце ASCII, поскольку Access не сохраняет данные в заголовке OLE в согласованных двухсимвольных блоках. Вот почему первый экземпляр полного пути хорошо отображается до «DocumentsWri», а затем идет не так, как надо. В этом примере символ после «Wri» должен быть «t», как в «Writing», но Access вставил нулевой символ в последовательность. Почему? Да, Microsoft, почему! Обратите внимание, что «ting» (74646E67) следует сразу за нулем. Еще одна нигда, чтобы код вокруг.

Итак, подведем итоги. Что у нас сейчас? У нас есть оригинальное имя внедренного файла, размер в байтах исходного файла, и у нас есть начальное местоположение данных, которые нам нужно извлечь. Таким образом, у нас есть все, что нам нужно, чтобы извлечь встроенный файл из устаревшей базы данных Access, а затем сохранить его на диск, используя его оригинальное имя и расширение.

Применение теории на практике

Далее следует PHP, который я использовал для извлечения из моей тестовой базы данных деталей объекта, которые мы обсуждали выше. Это без излишеств, процедурный PHP, который должен быть легким для любого, чтобы понять шаги, связанные с извлечением объекта из пакета OLE.

Вы заметите, что некоторая логика умножила определенные числа в два раза. Например, смещение для имени встроенного файла определяется как 168, а не 84 байта, упомянутых выше. Это потому, что некоторые части кода работают с необработанными данными из поля OLE базы данных Access, которое закодировано в шестнадцатеричном формате. То есть каждый отдельный байт хранится в виде двух шестнадцатеричных символов.

 <?php
if (!function_exists("hex2bin")) {
   function hex2bin($hexStr) {
      $hexStrLen = strlen($hexStr);
      $binStr = "";
      $i = 0;
      while ($i < $hexStrLen) {
         $a = substr($hexStr, $i, 2);
         $c = pack("H*", $a);
         $binStr .= $c;
         $i += 2;
      }
      return $binStr;
   }
}

$dbName = "db1.mdb";
$db = new PDO("odbc:DRIVER={Microsoft Access Driver (*.mdb)}; DBQ=$dbName; Uid=; Pwd=;");

$sql = "SELECT * FROM Table1 WHERE id = 2";
foreach ($db->query($sql) as $row) {
   $objName = "";

   switch (getOLEType($row["image"])) {
      case "Packager Shell Object":
          list($objName, $objData) = extractPackagerShellObject($row["image"]);
          break;
      case "Package":
          list($objName, $objData) = extractPackage($row["image"]);
          break;
      default:
          throw new Exception("Unknown OLE type");
   }
   if ($objName != "") {
      file_put_contents($objName, $objData);
   }
}

function flipEndian($data, $size) {
   $str = "";
   for ($i = $size - 2; $i >= 0; $i -= 2) {
      $str .= substr($data, $i, 2);
   }
   return $str;
}

function findNullPos($str) {
   // must start on a two-character boundary
   return floor((strpos($str, "00") + 1) / 2) * 2;
}

function getOLEType($data) {
   // fixed position of OLE type
   $offset = 40;

   $tmp = substr($data, $offset, 255);
   $nullPos = findNullPos($tmp);
   $tmp = substr($tmp, 0, $nullPos);
   $type = hex2bin($tmp);

   return $type;
}

function extractPackagerShellObject($data) {
   $headerBlock = 500; // usable header size
   $offset = 168; // location of name

   // find name
   $tmp = substr($data, $offset, 255);
   $nullPos = findNullPos($tmp);
   $name = substr($tmp, 0, $nullPos);
   $pos = $offset + strlen($name);

   // find data
   $path1 = strpos($data, $name, $pos); // 1st full path
   $pos = $path1 + strlen($name);
   $path2 = strpos($data, $name, $pos); // 2nd full path
   // check if only one full path
   if ($path2 > $pos) {   
      $pos = $path2 + strlen($name);
   }
   $oleSizePos = $pos + 2;
   $oleObjSize = flipEndian(substr($data, $oleSizePos, 8), 8);
   $oleHeaderEnd = $oleSizePos + 8;
   $objName = hex2bin(substr($tmp, 0, $nullPos));

   // extract object
   $data = substr($data, $oleHeaderEnd, hexdec($oleObjSize) * 2);
   $objData = hex2bin($data);

   return array($objName, $objData);
}

function extractPackage($data) {
   $headerBlock = 500; // usable header size
   $offset = 140; // location of name

   // find name
   $tmp = substr($data, $offset, 255);
   $nullPos = findNullPos($tmp);
   $name = substr($tmp, 0, $nullPos);
   $pos = $offset + strlen($name);

   // find data
   $path1 = strpos($data, $name, $pos); // 1st full path
   $pos = $path1 + strlen($name);
   $path2 = strpos($data, $name, $pos); // 2nd full path
   // check if only one full path
   if ($path2 > $pos) {   
      $pos = $path2 + strlen($name);
   }
   $oleSizePos = $pos + 2;
   $oleObjSize = flipEndian(substr($data, $oleSizePos, 8), 8);
   $oleHeaderEnd = $oleSizePos + 8;
   $objName = hex2bin(substr($tmp, 0, $nullPos));

   // extract object
   $data = substr($data, $oleHeaderEnd, hexdec($oleObjSize) * 2);
   $objData = hex2bin($data);

   return array($objName, $objData);
}

Обратите внимание, что специальная hex2bin() Кроме того, оператор switch

Две функции пакета выглядят очень похоже. Они будут рассмотрены в следующей части этой статьи, когда код будет подвергнут рефакторингу из-за схожего характера логики, требуемой для этих и других типов объектов.

Я проверил этот PHP со следующими типами файлов: BMP, GIF, JPEG, PNG, DOC, XLS и PPT, все из которых были сохранены в виде пакетов. Представленная выше логика должна позволять спасать любой файл, хранящийся в пакете OLE, из устаревшей базы данных, а не только GIF, используемый в этом примере.

Резюме

В этой статье мы рассмотрели основные элементы извлечения объектов пакета из полей OLE в базе данных Microsoft Access с использованием PHP. Изучив базовую структуру объекта OLE и узнав, как извлечь содержащийся в нем файл, представленный выше метод можно применить к другим типам OLE, чтобы обеспечить возможность извлечения как можно большего количества данных из устаревшей базы данных Access.

Во второй части этой серии мы рассмотрим более сложную проблему извлечения известных типов объектов, уделяя особое внимание извлечению документов Acrobat PDF из устаревшей базы данных Access. В то же время, не стесняйтесь клонировать репозиторий GitHub с кодом этой статьи, чтобы играть и исследовать.

Изображение через Fotolia