Пользователь
0,0
рейтинг
18 июля 2013 в 20:18

Разработка → Определение MIME-типов

Привет, хабр!

Недавно задался вопросом а сколько байт необходимо для корректного определения mime типа файла. В первую очередь погуглив, полученными ответами не удовлетворился и поэтому решил сам провести маленькое исследование на эту тему.

На изучение данного вопроса меня натолкнула следующая задача: определение MIME-типа файла, находящегося на smb-сервере. Лучшее, что я придумал — копировать кусок файла на локальную машину и потом, по этой части пытаться распознать его MIME-тип.



Для начала расскажу, что я нагуглил и почему мне это не понравилось:



Stack Overflow дает 2 ссылки на википедию:

  1. File Signature говорит, что в большинстве случаев достаточно 2-4 байтов. Однако к сожалению это не так, например, для такого популярного формата, как pdf.
  2. List of signatures приводит некоторый список подписей для файлов разных форматов, но он далеко не полный. Потом нашел File Signatures, здесь вроде как все.
    Однако вернемся все к тому же pdf. Если верить этим источника, то для определения того, что файл является pdf достаточно четырех байт (0x25 0x50 0x44 0x46), однако исходя из первых четырех байтов libmagic говорил, что MIME-тип pdf-файла — text/plain, а из пяти — верное application/pdf. Затрудняюсь точно ответить с чем это связано, надо смотреть исходники.


Теперь давайте перейдем, собственно говоря, к тому, что сделал я. Я написал очень маленькую программку, которая считывала все файлы из одной директории, копировала первые N байт в другую директорию, а затем по частичным копиям полученных файлов пыталась определить, а что это собственно говоря такое было. И так до тех пор, пока MIME-тип части файла не совпадет с MIME-типом оригинала. По результатам работы программа рапортовала, сколько байт понадобилось для определения того или иного типа. Вот ее код:

#include <stdio.h>
#include <stdlib.h>
#include <magic.h>
#include <sys/types.h>
#include <dirent.h>
#include <errno.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define TEST_DIR "test-dir/"
#define TMP_DIR "tmp-dir/"

magic_t cookie;

// Detects how many bytes required for correct MIME-type of this file
void detect_size(char *filename) {
  int bytes = 1;
  int infd, outfd;
  char strin[100], strout[100], type[100];
  char buf[4096];

  strcpy(strin, TEST_DIR);
  strcat(strin, filename);

  strcpy(strout, TMP_DIR);
  strcat(strout, filename);

  while(1) {
    // Make a copy of given file
    infd = open(strin, O_RDONLY);
    outfd = open(strout, O_RDWR | O_CREAT, 00666);
    read(infd, &buf, bytes);
    write(outfd, &buf, bytes);
    lseek(infd, 0, SEEK_SET);
    lseek(outfd, 0, SEEK_SET);

    // Detect mime types of old and new 
    const char *mime_type = magic_descriptor(cookie, infd);
    strcpy(type, mime_type);
    mime_type = magic_descriptor(cookie, outfd);

    // Check if mime type detected correctly
    if (strcmp(mime_type, type) == 0) {
      printf("%s detected correctly in %d bytes\n", type, bytes);
      unlink(strout);
      return;
    }

    unlink(strout);
    bytes++;
  }
}

int main() {
  DIR *dirfd = opendir(TEST_DIR);
  struct dirent entry, *result = NULL;

  cookie = magic_open(MAGIC_MIME_TYPE | MAGIC_ERROR);
  magic_load(cookie, NULL);

  while(1) {
    readdir_r(dirfd, &entry, &result);

    if (result == NULL)
      break;  // No more entries in this directory

    if (!strcmp(entry.d_name, ".") || !strcmp(entry.d_name, ".."))
      continue;  // Ignore "." and ".."

    detect_size(entry.d_name);
  }

  magic_close(cookie);
  closedir(dirfd);
  exit(EXIT_SUCCESS);
}


Потом накидав кучку разных файлов в папку test-dir я начал экспериментировать. Конечно то, что я сделал ни как не тянет на полномасштабное и серьезное исследование, но некоторые результаты все таки интересны. Приведи их краткую сводку:

application/x-sharedlib detected correctly in 18 bytes
application/msword detected correctly in 1793 bytes
image/gif detected correctly in 4 bytes
application/zip detected correctly in 4 bytes
application/x-dosexec detected correctly in 2 bytes
application/vnd.oasis.opendocument.presentation detected correctly in 85 bytes
text/html detected correctly in 14 bytes
image/jpeg detected correctly in 2 bytes
application/x-executable detected correctly in 18 bytes
text/x-makefile detected correctly in 1594 bytes
application/x-executable detected correctly in 18 bytes
application/x-gzip detected correctly in 2 bytes
audio/mpeg detected correctly in 2291 bytes
text/x-c detected correctly in 27 bytes
audio/x-flac detected correctly in 4 bytes
application/pdf detected correctly in 5 bytes

Отмечу некоторые вещи, которые мне показались интересными:

  • Ну во-первых, конечно уже упомянутый pdf, который распознается в 5 байт, а не в 4, как вроде бы следовало ожидать.
  • И напоследок хочется отметить, что не смотря на всю крутость идеи определять тип файла по первым N байтам она, на мой взгляд, провалилась.


Ну это пожалуй все, что я хотел рассказать в этот раз, не люблю много писать. Надеюсь, что это статья окажется кому-нибудь интересной.
Спасибо за внимание.
Евгений Юлюгин @yulyugin
карма
33,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (25)

  • +3
    Почему #define JPEG «audio/jpeg»?
    • +8
      а честно говоря не могу ответить, как и не могу вспомнить о чем думал в момент, когда делал эту картинку.
    • +2
      C-c C-v же
  • 0
    PDF вполне себе нормально идентифицируется по первым 4 байтам. То, что в libmagic сигнатура записана в 5 байт — это, видимо, для надёжности, вдруг кто-нибудь захочет написать в текстовом файле "%PDF", и тогда этот файл засчитается за pdf файл. Но как в таком случае поможет сигнатура из 5 байт — непонятно, ведь добавляется символ "-", который тоже вполне может встретиться в текстовом файле :)

    Однажды помнится задался идеей собрать базу сигнатур файлов, для эксперимента, раздал программу нескольким сотням добровольцев. Программа сканировала все имеющиеся в системе носители и читала по первые 4 байта из каждого файла. В отчёт записывалось 4 байта + расширение, которое было у файла. В результате понял, что 4 байт достаточно в большинстве случаев, но есть исключения, типа контейнеров, например doc и xls, или ещё хуже docx и xlsx.
    • 0
      Спасибо за комментарий, надо будет увеличить диапазон файлов для более точного исследования.

      Исходя из полученный в мною данных, audio/mpeg определилось корректно за 2291 байт.

      Кстати, если посмотреть таблицы, то можно заметить, что есть интересные исключения, например, iso-файлы, у которых заголовок находится со смещением от начала файла.
      • 0
        mpeg — это вообще такая штука: особого заголовка для него не определено, плеер сканирует поток в поисках знакомых структур (кадров звука или видео) и их воспроизводит

        короче, на mpeg-данные вообще не ориентируйтесь, вполне такое возможно (для огромного битрейта и сложного transport stream) что и за двадцать килобайт вы файл не узнаете — а играться он тем не менее будет без проблем
    • +1
      PDF в общем-то текстовый формат, как и PostScript. Просто в них есть поддержка бинарных данных :)
  • +1
    Если правильно помню libmagic хорошо справляется с бинарными файлами — там огромная база магических байт-последовательностей, по ним либа и распознает. А текстовые файлы имеет смысл просто смотреть на расширение, и если надо вывести попросить libmagic определить кодировку — определилась считаем что файл текстовый и используем mime расширения, а если нет то по умолчанию application/octet-stream.
    По похожему алгоритму работает гитхаб и его либа libguist она внутри использует libmagic.
    • 0
      Ну вот с a.out, полученным в результате компиляции приведенного выше кода libmagic справился в 18 байт.
  • +3
    И в чем смысл статьи?.. Можно не брутфорсить, оставляя N байт от файла, а заглянуть в /usr/share/misc/magic и увидеть все своими глазами.

    А у PDF просто первые 5 байт жестко закреплены спецификацией:
    Развернуть
    image

    Тут ведь трэйдофф между сложностью и надежностью определения типа. То есть можно определять хоть по одному байту, если например стоит задача выбрать тип из PDF, PNG, EXE и WAV, с очень низкой надежностью. При этом в случае PDF можно без увеличения сложности алгоритма расширить этот размер до 5 байт, и выиграть в надежности. Ну а с другой стороны весов (максимальная надежность, максимальная сложность) — просто парсить файл по спецификации. Распарсился как PDF? Это PDF.
    • –8
      Смысл статьи очень прост — поделиться своими наблюдениями. Кому-то они показались интересными, кому-то нет, с этим ничего не поделаешь. Вам как я вижу они показались вообще бесполезными, это ваше мнение.

      Лезть в libmagic я не стал потому что были плохие погодные условия, болела голова, было не когда было лень. Да и алгоритм его работы мне примерно понятен.

      То, что '-' входит в спецификацию pdf пятым символом я не знал, спасибо.
      Просматривать все спецификации всех форматов было бы глупо в плане затрат времени и, на мой взгляд, бессмысленно.
  • 0
    Ну вот с a.out, полученным в результате компиляции приведенного выше кода libmagic справился в 18 байт.
  • 0
    Товарищи минусующие, если вы будете писать, что вам не понравилось пользы будет гораздо больше.
    Например, в следующий раз я напишу или не напишу что-то или напишу это по-другому.
    • 0
      Ну, например, далеко не все mime-типы можно определить по начальным байтам файла.
      Всякие mpeg-и, например, нельзя, потому что для них вообще не определено понятие «файла». Например, в ip или спутниковом телевидении никаких файлов нет, а транспортный поток mime-типа video/mpeg — есть:) rtfm transport stream (2), etc. То же самое касается радио.
      Поэтому, кстати, большинство видеофайлов завернуты в контейнеры типа риффов (ави) и т.п.
  • +4
    А чем Вам не угодили Apache MimeMagic или freedesktop как отправная точка исследования?
    httpd.apache.org/docs/2.2/mod/mod_mime_magic.html
    freedesktop.org/wiki/Software/shared-mime-info
    • 0
      На этот вопрос не могу ответить, так как не знаком с этими инструментами.

      Спасибо за наводку, примусь изучать.
  • +8
    я конечно дико извиняюсь, но подскажите, чем православный file не устраивает, который конечно же использует libmagic?

    Извините, но ответ мне было лень разбираться с ним и я сделал свой велосипед, с косоугольными колёсами ещё не повод делать очередной костыль.
    • 0
      file не устраивает по простой причине, так как для того, чтоб определить с помощью него MIME-тип файл должен находится локально, а мне понадобилось определять MIME-тип файлов, находящихся где-то на smb серверах. Лучшее, что я придумал — копировать часть файла с сервера на локальную машину и потом с помощью libmagic определять его тип. Поэтому я и задался вопросом, а сколько мне надо скопировать с этого сервера, чтоб точно определить тип файла.

      А зачем мне лезть в детальное устройство libmagic? Чем мне это поможет, подскажите?
      • +2
        А, так вот оно что, что же вы молчали!

        У SMB есть удивительная возможность — протокол умеет произвольный доступ к файлу (чтение из любого места). Получается, чтобы сэкономить время и трафик, и при этом не париться со странными мерками «сколько байт из начала хватит чтоб распознать любой файл», достаточно дёргать из файла те области, которые проверяются сигнатурами libmagic (см всё-таки /usr/share/misc/magic).

        Вот это было бы интересно: модификация утилиты file, которая читает из файла только нужные области, ну и кеширует то что уже прочитала, чтоб несколько раз не дёргать один участок.
        Судя по strace-у, сейчас file всегда сразу грузит первые 98304 байта файла. Наверно это максимальный оффсет, который встречается в моей базе magic.
  • 0
    мы используем imagemagick identify, exiftool, самописные детекторы + быстрое определение по расширению, правда требования немного другие.
  • +1
    Ещё стандартные методы распознавания типов можно увидеть в проекте Apache Tika, в неплохом и понятном xml.
    • 0
      Хотя Tika легко путает message/rfc822 и text/plain, например. По вполне простым и понятным причинам)
  • +1
    Есть еще один тонкий момент в определении типа файла — гибридные файлы, являющиеся одновременно валидными файлами двух типов. Например, пресловутый RARJPEG или, частый для *nix-софта случай, tar.gz архив с bash-скриптом установщика в начале.

    Подозреваю, что для автора поста этот случай не очень актуален, но если есть задача найти все файлы определенного типа для последующего анализа, такие гибриды могут сбить поисковый алгоритм с толку.
  • 0
    Если на скрине Ваш код, то там опечатка:
    #define PDF «applicatiion/pdf» :)
    • 0
      Это «код» был написан, специально для этой картинке) Спасибо за обнаруженную опечатку, исправил.

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