пятница, 18 июля 2025 г.

Байтитура Zip-файла

В старые добрые времена MS-DOS, когда hardware были большими, а software маленькими, были таланты (без всяких ковычек!) которые по подобному текстовому дампу говорили:

здесь открывается файл и что-то записывается в конец.
я таких талантов не имел, поэтому добавил в hiew дизассемблер.

Смотреть на дампы всяких файлов данных тоже было интересно но хотелось лучше видеть, что же там за конкретные данные внутри. Для этого я в те ламповые времена написал StructLook или сокращенно STL - смотрелку различных структурированных файлов.  (Кто ж знал, что английских букв мало и что MS так назовет свою библиотеку)

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

Например, вот так описывались компоненты Zip-файла:

--- 8< ---
/Zip
ZIPSIG:  w    1           Signature(PK)
#if zipsig != "KP"

:ERROR!!! Invalid signature PK

#else

ZIPSUBSIG:  w    'zipsubsig  directory type

#if( zipsubsig == 0x0403 )

ZIPVER:     w    1           Version needed to extract
            t16  `zipflags   General purpose bit flag
ZIPMTHD:    u16  'zipcompres Compression method
            td   1           Last mod file time (MS-DOS)
ZIPCRC:     d    1           CRC-32
ZIPSIZE:    u32  1           Compressed size
ZIPUNCMP:   u32  1           Uncompressed size
ZIPFNLN:    w    1           Filename length
ZIPXTRALN:  w    1           Extra field length
ZIPNAME:    c    ZIPFNLN     filename
ZIPXTRA:    c    ZIPXTRALN   extra field
            +    ZIPSIZE

#elseif( zipsubsig == 0x0201 )

ZIPCVER:    i8   1           Version made by
ZIPCOS:     u8   'ziphost    Host operating system
ZIPCVXT:    i8   1           Version needed to extract
ZIPCEXOS:   i8   1           O/S of version needed for extraction
            t16  `ZipFlags   General purpose bit flag
ZIPCMTHD:   u16  'zipcompres Compression method
            td   1           Last mod file time (MS-DOS)
ZIPCCRC:    d    1           CRC-32
....
--- 8< ---


И вот так StruсtLook показывал Zip-файл по этому описанию:

Время шло-бежало-летело.

И вот через 30 лет я написал Байтитуру (Bytitura, от Byte(en)+Partitura(it)).
Точнее, она была написана несколько лет назад только для личного использования, отчасти потому что язык описания от StructLook никак не добавлялся, отчасти потому что это была платформа для тренировок со всякими GUI и потоками и поэтому время от времени серьезно переписывалась.
Файлы описания были только как внешние DLL.
Потом смысл своего описателя полностью отпал при наличии той же Lua, и которую я наконец-то добавил в уже коммерческий продукт.
А в этом году добавились .sture-файлы - это текстовые файлы со стандартными С-шными структурами.
И теперь эта связка Lua + .sture-файл помогает достаточно просто описать то что надо увидеть, без расписывания каждого адреса и каждого байта.


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

Начнем со .sture-файла.
В нем записаны С-структуры, которые понадобятся для разбора целевого файла. Имя .sture-файла совпадает с именем скрипта и подхватывается автоматически при выборе целевого файла в диалоге открытия. Один и тот же .sture-файл используется как для парзеров в .lua так и для .btt32

Байтитура знает только стандартные типы:
(signed/unsigned) char/short/int/long/long long  для десятичного представления
(signed/unsigned) BYTE/WORD/DWORD/QWORD  для шестнадцатиричного
float/double  для вещественного
WCHAR для юникода

есть два нюанса (куда же без них!)
char без указания signed/unsigned представляет текст, в т.ч. отдельный символ
BYTE без signed/unsigned отображается как дамп

типы можно переопределять через стандартный typedef, см (1)

в размере массива может быть имя целой переменой из этой же структуры, см (2)

--- 8< ---
typedef unsigned short ushort;                      (1)
typedef unsigned long  ulong;                       (1)

typedef struct{
  DWORD        marker; // если будет комментарий то отобразиться именно он
  ushort       version_needed_to_extract;
  WORD         general_purpose_bit_flag;
  WORD         compression_method;
  WORD         last_mod_file_time;
  WORD         last_mod_file_date;
  DWORD        crc_32;
  ulong        compressed_size;                     (2a)
  ulong        uncompressed_size;
  ushort       file_name_length;                    (2b)
  ushort       extra_field_length;                  (2c)
  char         file_name[ file_name_length ];       (2b)
  BYTE         extra_field[ extra_field_length ];   (2c)
  BYTE         compressed_data[ compressed_size ];  (2a)
  }ZIP_LOCAL;
 ....
--- 8< ---

Теперь к собственно Lua-скрипту.

Минимальный скрипт состоит из трех обязательных функций:

--- 8< ---
-- минимальный скрипт который загрузится
-- но не знает ни про какие файлы

BTT_API_VERSION = 130 -- следите за актуальностью !

-- возвращает версию API которой пользуется
function  ApiVersionUsed()
  return  BTT_API_VERSION
end

-- отвечает хосту знает ли этот файл
function  IKnowThisFile( filename )
  return -1
end

-- собственно разборщик файла
function  ParseFile( file_index, filename, base_address, codesize )
  return -1
end
--- 8< ---

Но поскольку мы хотим что-то полезное от скрипта, то придется открывать файл, читать двойное слово и проверять на совпадение с маркером.


--- 8< ---
-- константы из bytitura.h
BTT_API_VERSION            = 130  -- следите за актуальностью !
decode_engine_none         = 0
decode_engine_ia32         = 1
ascii_cp_ansi              = 0
ascii_cp_oem               = 1
ascii_cp_utf8              = 2

-- zip константы
ZIP_MARKER_CENTRAL         = 0x02014b50
ZIP_MARKER_LOCAL           = 0x04034b50
ZIP_MARKER_CENTRALEND      = 0x06054b50
ZIP_MARKER_DATADESCRIPTOR  = 0x08074b50

function  IKnowThisFile( filename )

-- Имя файла передается в utf-8.

  local  file = io.open( filename, "rb" )
  local  sign = ReadDword( file, 0 )
  file:close()

  if sign ~= ZIP_MARKER_LOCAL then
    return -1
  end

 return  0,                  -- базовый адрес
         0,                  -- это данные    
         decode_engine_none, -- кода нет
         0                   -- файловые флаги, например может ли пользователь менять базовый адрес
end
--- 8< ---


В Lua прочитать двойное слово оказалось еще тем квестом,

простой вариант

local dword = 12345
dword = file:read( 4 )

 

конечно же не работал

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

--- 8< ---
function  ReadDword( file, address )
  file:seek( "set", address )
  local  byte0 = file:read( 1 )
  local  byte1 = file:read( 1 )
  local  byte2 = file:read( 1 )
  local  byte3 = file:read( 1 )
  if byte3 == nil
    then return nil
  end
  local  dword = (string.byte(byte3)<<24)
               + (string.byte(byte2)<<16)
               + (string.byte(byte1)<<8)
               +  string.byte(byte0)
  return dword
end
--- 8< ---


хотя документация и говорила что-то про диапазон подстроки

string.byte (s [, i [, j]])
Returns the internal numerical codes of the characters s[i], s[i+1], ···, s[j].
The default value for i is 1; the default value for j is i.


но это совсем не про что хотелось бы

local text  = file:read(4)
local dword = string.byte( text, 1, 4 )


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

Буквально рыдал когда представлял с какой скоростью в интерпретаторе скриптового языка будет читаться каждое значение.

Ну да ладно, написал и оно даже работает как надо, теперь собственно к разбору Zip-файла.


--- 8< ---
function  ParseFile( file_index, filename, base_address, codesize )
-- file_index идентифицирует файл с которым работает скрипт
-- все вызовы к хосту проходят с ним

-- готовой функции в Lua нет, пишем свою
  local  filesize = Filesize( filename )
                                                           
-- сначала требуется определить сегмент(-ы) файла
-- будет один сегмент данных на весь файл
  BttFileAddDataSegment( file_index,
                         0,           -- виртуальный адрес
                         0,           -- файловый адрес
                         filesize,    -- размер в файле
                         0 )          -- виртуальный размер = размеру в файле

-- установим нужную кодировку для имен файлов
  BttFileStringAsciizCP( file_index, ascii_cp_oem )

-- чтобы не скучно ждать интерпретатора будем показывать хотя бы количество файлов
  local nfiles  = 0                                        

  local address = base_address
-- установим начальный адрес для разбора файла
  BttFileSetRunAddress( file_index, address )

  local  file = io.open( filename, "rb" )

  local  result = true
-- цикл пока result истинный
  while  result  do
-- читаем маркеры и смотрим ...
    local  marker = ReadDword( file, address )             

-- конец файла - конец цикла
    if     marker == nil                        then result = false

-- для соответствующих маркеров вызывается соответствующая структра из .sture-файла

    elseif marker == ZIP_MARKER_LOCAL           then result = BttFileRunStruct(  file_index, "ZIP_LOCAL"          )

                                                              nfiles = nfiles + 1
-- вызов функции для отображения количества насчитанных файлов
                                                              BttFileParseStage( file_index, string.format( "%i files", nfiles ) )

    elseif marker == ZIP_MARKER_DATADESCRIPTOR  then result = BttFileRunStruct(  file_index, "ZIP_DATADESCRIPTOR" )

    elseif marker == ZIP_MARKER_CENTRAL         then result = BttFileRunStruct(  file_index, "ZIP_CENTRAL"        )

    elseif marker == ZIP_MARKER_CENTRALEND      then reuslt = BttFileRunStruct(  file_index, "ZIP_CENTRALEND"     )

-- если прилетел незнакомый маркер, расскажем об этом в log-окне
    else                                             result = false
                                                              BttFileLog(        file_index, string.format( "Unknow marker %08X at address %X", marker, address ) )
    end

-- получим следующий адрес после применения структуры
    address = BttFileGetRunAddress( file_index )
-- цикл OFF
  end                                                      

  file:close()

-- в любом случае сообщаем что разбор прошел успешно
  return  1                                                

end
--- 8< ---


по сути, все делают вызовы BttFileRunStruct,  
BttFileSet/GetRunAddress обвязка,
а BttFileStringAsciizCP/BttFileParseStage/BttFileLog для рюшек

вызов этого скрипта покажет байтитуру Zip-файла,
т.е. куда относится каждый байт файла

празднолюбопытствующие могут скачать набор Lua + .sture для Zip-файла здесь,
естественно ни в каких Demo оно работать не будет.

В следующий раз расскажу об уникальной возможнности, и это не дизассемблер.

2 комментария:

  1. На Lua не пишу, но видимо это делается так:
    file = io.open(filename, "rb")
    data = file:read(4)
    dword = string.unpack("I4", data)

    ОтветитьУдалить
  2. быстрее, но не в разы.
    ожидаю комментарии от тех кто пишет на lua давно и много.

    ОтветитьУдалить