Доступ к MD-файлам при помощи VBA

Файлы метаданных V7 (*.md) представляют собой структурированные хранилища (structured storage), организованные по правилам файловой системы от Microsoft. В терминологии OLE2 сами дисковые файлы носят название "составной файл" (compound file). Compound file состоит из целого числа блоков данных, размер каждого блока равен 512 байт, т.е. соответствует одному дисковому сектору, поэтому в дальнейшем я буду пользоваться термином "сектор" для обозначения блока размером 512 байт.

Сергей Новодворский

Источник http://www.hare.ru/

Файлы метаданных V7 (*.md) представляют собой структурированные хранилища (structured storage), организованные по правилам файловой системы от Microsoft. В терминологии OLE2 сами дисковые файлы носят название "составной файл" (compound file).

Compound file состоит из целого числа блоков данных, размер каждого блока равен 512 байт, т.е. соответствует одному дисковому сектору, поэтому в дальнейшем я буду пользоваться термином "сектор" для обозначения блока размером 512 байт.

Нумерация секторов в составном файле начинается с -1: -1,0,1,2…

В стуктуру составного файла входят следующие области данных:

  • заголовок файла;
  • данные, организованные в виде "больших блоков", занимающие целиком весь сектор;
  • данные, организованные в виде "малых блоков" размером по 64 байта, занимают весь сектор, но в количестве 8 штук;
  • данные, представляющие собой "объекты" каталога, размером по 128 байт, занимают весь сектор в количестве 4 штук;
  • таблица размещения в составном файле больших блоков, по сути это FAT (File Allocation Table), далее "FAT больших блоков";
  • таблица размещения собственно самого FAT больших блоков (может отсутствовать, если FAT вмещается в один сектор);
  • таблица размещения в составном файле малых блоков – FAT малых блоков;
  • неструктурированные данные (lock-bytes ???).
Прежде чем начать описание структуры и способов получения данных из составного файла, проведем некоторую подготовительную работу.

Нам придется работать с двоичными данными и переводить числа из шестнадцетеричного в десятичный формат (учитывая, что числа в файле хранятся в шестнадцатеричном формате начиная с младшего байта). Для этого в разделе деклараций нашего VBA-модуля создадим две UDT-структуры:

Public Type DWORD_C   'структура для чтения DWORD из файла
   b1 As Byte   '1 (младший байт)
   b2 As Byte   '2 байт
   b3 As Byte   '3 байт
   b4 As Byte   '4 (старший) байт
End Type

Public Type DWORD_B   'структура для перевода DWORD в LONG
   n As Long
End Type

и напишем функцию для перемещения содержимого из одной структуры в другую

Public Function HDec(vByte As DWORD_C) As Long
' функция преобразования знаковых длинных целых
Dim mLong As DWORD_B
   LSet mLong = vByte
   HDec = mLong.n
End Function

Объявим там же константу со значением размера сектора и байтовый массив, в котором будет хранится наш составной файл:

Public Const SECTORSIZE As Long = 512
Public fileBuf() as Byte

Далее пишем функцию, которая считает составной файл целиком в байтовый массив

Public Function GetFile() As Boolean
' прочитать файл
Dim iFile As Long ' номер файла
Dim lFile As Long ' размер файла
Dim rc As Boolean ' результат выполнения
Dim mdFileName as Strin ' полное имя файла

On Error Goto errHandler
rc = False ' результат выполнения

' здесь любым доступным способом присвоим переменной mdFileName имя
' составного файла
' . . .


If Len(mdFile) = 0 Then GoTo myExit
iFile = FreeFile()
Open mdFile For Binary As #iFile ' откроем файл в режиме двоичного доступа
lFile = LOF(iFile) ' размер файла
If lFile = 0 Then GoTo myExit

ReDim fileBuf(1 To lFile)
' считать весь файл
Get iFile, , fileBuf()
' закрыть файл
Close iFile
If UBound(fileBuf) = lFile Then
' если файл *.md считан правильно
   rc = True
End If

myExit:
   GetFile = rc
   Exit Function
errHandler:
   GetFile = rc
' . . . вывод сообщения об ошибке
End Function

Если функция вернула True, значит файл считан в массив fileBuf().

Заголовок составного файла

Заголовок представляет собой запись размером 80 байтов в секторе номер -1. Нас будут интересовать следующие поля заголовка:

Смещение Размер поля Описание
Dec Hex
+1 +00h DWORD Магическое число E011CFD0h (-535703600 при выполнении функции HDec)
+45 +2Ch DWORD Количество секторов, которые занимает FAT больших блоков
+49 +30h DWORD Номер стартового сектора каталога
+61 +3Ch DWORD Номера стартового сектора FAT малых блоков
+69 +44h DWORD Номер сектора доп.таблицы размещения FAT больших блоков
+77 +4Ch DWORD Номер стартового сектора FAT больших блоков

Что сие означает – будет рассказано ниже, а пока что в разделе деклараций модуля создаем UDT структуру:

Public Type FILEHEADER ' это структура для заголовка файла
   SizeOfBBD As Long ' кол-во секторов FAT больших блоков
   StartBBD As Long ' стартовый сектор FAT больших блоков
   StartBBDex As Long ' стартовый сектор доп.таблицы для FAT больших блоков
   StartSBD As Long ' стартовый сектор FAT малых блоков
   StartRoot As Long ' стартовый сектор каталога
End Type

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

Public HeaderMD As FILEHEADER

и пишем функцию, которая заполнит стуктуру заголовка составного файла

Public Function GetHeader() As Boolean
' прочитать заголовок файла
Dim i As Long
Dim sText As String
Dim dWord As DWORD_C
Dim x As Long
Dim rc As Boolean

On Error Goto errHandler
rc = False ' результат выполнения

' сектор, в котором расположен заголовок,
' нумеруется как (-1), поэтому для оптимизации
' сразу прибавляем по 1 к стартовым значениям
' т.к. сами считать будем от головы файла

' сигнатура файла

dWord.b1 = fileBuf(1)
dWord.b2 = fileBuf(2)
dWord.b3 = fileBuf(3)
dWord.b4 = fileBuf(4)
sText = Hex(HDec(dWord))
' проверяем на наличие магического числа
If sText "E011CFD0" Then
   GoTo myExit
End If
' количество секторов в FAT больших блоков
i = &H2C
dWord.b1 = fileBuf(i + 1)
dWord.b2 = fileBuf(i + 2)
dWord.b3 = fileBuf(i + 3)
dWord.b4 = fileBuf(i + 4)
HeaderMD.SizeOfBBD = HDec(dWord)
' стартовый сектор каталога
i = &H30
dWord.b1 = fileBuf(i + 1)
dWord.b2 = fileBuf(i + 2)
dWord.b3 = fileBuf(i + 3)
dWord.b4 = fileBuf(i + 4)
HeaderMD.StartRoot = HDec(dWord) ' здесь порядковый номер сектора
' стартовый сектор FAT малых блоков

i = &H3C
dWord.b1 = fileBuf(i + 1)
dWord.b2 = fileBuf(i + 2)
dWord.b3 = fileBuf(i + 3)
dWord.b4 = fileBuf(i + 4)
HeaderMD.StartSBD = HDec(dWord)
' стартовый сектор таблицы размещения доп.FAT больших блоков
i = &H44
dWord.b1 = fileBuf(i + 1)
dWord.b2 = fileBuf(i + 2)
dWord.b3 = fileBuf(i + 3)
dWord.b4 = fileBuf(i + 4)
x = HDec(dWord)
If x > 0 Then
' вычислить абсолютный адрес только если он больше 0
   x = 1 + (x + 1) * SECTORSIZE
End If
HeaderMD.StartBBDex = x
' стартовый сектор FAT больших блоков
i = &H4C
dWord.b1 = fileBuf(i + 1)
dWord.b2 = fileBuf(i + 2)
dWord.b3 = fileBuf(i + 3)
dWord.b4 = fileBuf(i + 4)

' вычисляем абсолютный адрес в файле
HeaderMD.StartBBD = 1 + (HDec(dWord) + 1) * SECTORSIZE
rc = True

myExit:
   GetHeader = rc
   Exit Function
errHandler:
   GetHeader = rc
' . . . вывод сообщения об ошибке
End Function

Если функция вернула True, значит заголовок считан и наш файл является составным файлом OLE.

FAT

FAT представляет собой последовательный список 4-байтовых "строчек" DWORD, каждая из которых соответствует порядковому номеру сектора в файле, начиная с сектора под номером 0 (сектор -1 вообще не учитывается). Значение строчки – это номер сектора, следующего за текущим. Отрицательное содержимое "строчки" означает следующее:

  • -1=FFFFFFFFh – специальный сектор;
  • -2=FFFFFFFEh – последний сектор в цепочке;
  • -3=FFFFFFFDh – этот сектор не используется.
Все, сказанное о FAT, применяется к FAT больших блоков, однако с малыми блоками и каталогом дело обстоит не так.

Номер строчки для FAT малых блоков и каталога – это не порядковые номера секторов, а порядковый номер записи (размером 64 байта или 128 байт соответственно) от начала области, где располагаются собственно малые блоки или каталог. Первая запись имеет номер 0.

Но вернемся к FAT больших блоков. Первый сектор расположен в файле по абсолютному адресу, который мы определили из заголовка файла (смещение +4Ch) и записали в HeaderMD.StartBBD. Но непосредственно в сектор может поместиться только 128 номеров секторов (т.е. файл в принципе не может быть больше 512 + 128 Х 512 = 66048 байт), а где же тогда искать продолжение FAT?

Вот это был секрет за семью замками и только методом проб и ошибок была обнаружена область, которая из себя представляет FAT для FAT. Находится она в секторе -1 и занимает оставшуюся от заголовка файла область начиная с 81 байта (+50h) и до конца сектора. Каждая строчка этой области указывает на номер сектора, в котором расположен следующий сектор FAT( поэтому FAT для FAT это не совсем FAT, т.к. значение строчки указывает не на следующий за текущим номер сектора, а прямо адресует к сектору, где расположен следующий "кусочек" настоящего FAT).

Таким образом мы имеем 109 секторов FAT, каждый из которых адресует 128 секторов составного файла, т.е. максимальный размер файла может составлять 512 + (109 Х 128 Х 512) = 7 143 936 байт, но физические-то файлы больше!

Методом всё того же тыка был обнаружен еще один сектор "FAT для FAT", номер которого находится в заголовке файла (смещение +44h), причем если значение равно -1 (FFFFFFFFh), то такого сектора в файле нет.

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

Public Type FATSTRUCTURE ' это структура для FAT
   Adres As Long ' абс. адрес текущего сектора
   Next As Long ' номер следующего сектора
End Type

Public fatBBD() As FATSTRUCTURE ' массив FAT больших блоков

И пишем функцию:

Public Function GetFatBBD() As Boolean
' чтение FAT больших блоков
Dim i As Long
Dim x As Long
Dim j As Long
Dim iCount As Long
Dim iMax As Long
Dim dWord As DWORD_C
Dim rc As Boolean

rc = False ' результат выполнения
' определить размер массива для FAT
' размер сектора умн. на кол-во секторов FAT
' и разделить на 4 (кол-во байт DWORD)

x = (SECTORSIZE * HeaderMD.SizeOfBBD)
i = x / 4
If i = 0 Then Exit Function
' определяем массив FAT (начинаем с 0, т.к. сектора в файле нумеруются с 0)
ReDim fatBBD(0 To i - 1)
' записываем данные в массив
x = HeaderMD.StartBBD ' стартовый адрес BBD (абсолютный)
For i = 0 To 127 ' количество слов в секторе
' адрес сектора=порядковый номер сектора в FAT
   fatBBD(i).Adres = 1 + (i + 1) * SECTORSIZE
' определяем следующий сектор в FAT
   dWord.b1 = fileBuf(x)
   dWord.b2 = fileBuf(x + 1)
   dWord.b3 = fileBuf(x + 2)
   dWord.b4 = fileBuf(x + 3)
   fatBBD(i).Next = HDec(dWord)
   x = x + 4
Next
' если секторов FAT больше 1
' читаем таблицу в секторе -1
If HeaderMD.SizeOfBBD > 1 Then
   iCount = 128 ' следующий номер массива FAT
   ' цикл по считанным номерам секторов
   ' адреса номеров блоков FAT BBD начинаются с абс.адреса 81 (десят.)
   ' по 512 (десят.)
   For j = 81 To 512 Step 4
      dWord.b1 = fileBuf(j)
      dWord.b2 = fileBuf(j + 1)
      dWord.b3 = fileBuf(j + 2)
      dWord.b4 = fileBuf(j + 3)
      x = HDec(dWord)
      If x > 0 Then
' вычислить абсолютный адрес только если он больше 0
         x = 1 + (x + 1) * SECTORSIZE
' записываем данные в массив
         iMax = iCount + 127 ' число слов в секторе
         For i = iCount To iMax ' количество слов в секторе
' адрес сектора=порядковый номер сектора в FAT
            fatBBD(i).Adres = 1 + (i + 1) * SECTORSIZE
' определяем следующий сектор в FAT
            dWord.b1 = fileBuf(x)
            dWord.b2 = fileBuf(x + 1)
            dWord.b3 = fileBuf(x + 2)
            dWord.b4 = fileBuf(x + 3)
            fatBBD(i).Next = HDec(dWord)
            x = x + 4
         Next
' увеличиваем номер массива
         iCount = iMax + 1
      End If
   Next

' если таблица размещения FAT еще не кончилась
   If HeaderMD.StartBBDex > 0 Then

' читаем доп.таблицу размещения FAT
      ' цикл по считанным номерам секторов
      For j = HeaderMD.StartBBDex To HeaderMD.StartBBDex + 511 Step 4
         dWord.b1 = fileBuf(j)
         dWord.b2 = fileBuf(j + 1)
         dWord.b3 = fileBuf(j + 2)
         dWord.b4 = fileBuf(j + 3)
         x = HDec(dWord)
         If x > 0 Then
' вычислить абсолютный адрес только если он больше 0
            x = 1 + (x + 1) * SECTORSIZE
' записываем данные в массив
            iMax = iCount + 127 ' число слов в секторе
            For i = iCount To iMax ' количество слов в секторе
' адрес сектора=порядковый номер сектора в FAT
               fatBBD(i).Adres = 1 + (i + 1) * SECTORSIZE
' определяем следующий сектор в FAT
               dWord.b1 = fileBuf(x)
               dWord.b2 = fileBuf(x + 1)
               dWord.b3 = fileBuf(x + 2)
               dWord.b4 = fileBuf(x + 3)
               fatBBD(i).Next = HDec(dWord)
               x = x + 4
            Next
' увеличиваем номер массива
            iCount = iMax + 1
         End If
      Next
   End If
End If
rc = True

myExit:
   GetFatBBD = rc
End Function

Если функция вернула True, значит FAT считан. Функцию можно разбить на две, но это не существенно.

После того, как мы получили из составного файла FAT больших блоков, пришла пора приступить к получению FAT малых блоков. Номер стартового сектора FAT малых блоков указан в заголовке файла (смещение +3С), мы его записали в HeaderMD.StartSBD. Этот номер равен индексу массива FAT больших блоков, и дальше по цепочке вытягиваем все сектора, в которых расположен FAT малых блоков.

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

Public Function GetNextBigFATSector(iSector As Long) As Long
' получить значение следующего сектора (большой FAT)
Dim x As Long
   GetNextBigFATSector = 0
   x = fatBBD(iSector).Next
   If x > 0 Then
      GetNextBigFATSector = x
   End If
End Function

Почему нужна была структура FATSTRUCTURE? Если для FAT больших блоков можно было умножением индекса массива на размер сектора плюс 512 узнать абсолютный адрес сектора, то номера в FAT малых блоков означают относительные номера 64-байтовых записей от начала области данных малых блоков и, используя указанную структуру, мы будем в процессе создания массива FAT малых блоков сразу записывать абсолютные адреса для каждого малого блока.

Но для этого надо определить абсолютный адрес сектора, с которого начинается область данных малых блоков. Адрес этого сектора находится в 128-байтовой записи объекта каталога, являющегося корнем каталога (Root Entry) по смещению +74h, а сам объект представлен в заголовке файла (смещение +30h) как стартовый сектор каталога (об объектах каталога речь пойдет позже).

В разделе деклараций объявляем массив FAT малых блоков:

Public fatBBD() As FATSTRUCTURE ' массив FAT малых блоков

и пишем функцию для получения FAT из файла

Public Function GetFatSBD() As Boolean
' получить FAT малых блоков
Dim i As Long
Dim x As Long
Dim j As Long
Dim iCount As Long
Dim iMax As Long
Dim dWord As DWORD_C
Dim rc As Boolean

rc = False
' определяем количество записей (малых блоков) малого FAT
iMax = 0
' получаем абс.номер сектора FAT
i = fatBBD(HeaderMD.StartSBD).Adres
' номер из FAT
x = HeaderMD.StartSBD
If i ' организуем цикл , если абс.сектор>0
Do While i > 0
   iMax = iMax + 128
' получаем значение следующего сектора
   x = GetNextBigFATSector(x)
   If x > 0 Then
      i = fatBBD(x).Adres
   Else
      i = 0
   End If
Loop
' определяем размерность массива малого FAT
ReDim fatSBD(0 To iMax - 1)
' опять получаем абс.номер сектора FAT
i = fatBBD(HeaderMD.StartSBD).Adres
' номер слова из FAT
x = HeaderMD.StartSBD
iCount = 0
' опять организуем цикл , если абс.сектор>0
Do While i > 0
' читаем файл
   For j = 0 To 127 ' количество слов в секторе
' смещение малого блока=порядковый номер слова в малом FAT
      fatSBD(iCount).Adres = 0 ' пока записываем 0
' определяем следующий сектор в FAT
      dWord.b1 = fileBuf(i)
      dWord.b2 = fileBuf(i + 1)
      dWord.b3 = fileBuf(i + 2)
      dWord.b4 = fileBuf(i + 3)
      fatSBD(iCount).Next = HDec(dWord)
      iCount = iCount + 1
      i = i + 4
   Next
' получаем значение следующего сектора из большого FAT
   x = GetNextBigFATSector(x)
   If x > 0 Then
      i = fatBBD(x).Adres
   Else
      i = 0
   End If
Loop

' теперь вытягиваем абс.адреса блоков SBD

' получаем абс.номер стартового сектора каталога
i = 1 + (HeaderMD.StartRoot+1) * SECTORSIZE
' получаем абс.адрес слова, где указан стартовый блок FAT(+74H)
i = i + 116
dWord.b1 = fileBuf(i)
dWord.b2 = fileBuf(i + 1)
dWord.b3 = fileBuf(i + 2)
dWord.b4 = fileBuf(i + 3)
x = Hdec(dWord)
' получаем абс.номер сектора FAT
i = fatBBD(x).Adres
iCount = 0
' организуем цикл , если абс.сектор>0
Do While i > 0
' всего в секторе восемь 64 байтных блоков
   For j = 0 To 511 Step 64
' записываем абсолютный адрес в малый FAT
      If fatSBD(iCount).Next -1 Then
' если этот сектор используется
         fatSBD(iCount).Adres = i + j
      End If
      iCount = iCount + 1
   Next
' получаем значение следующего сектора
   x = GetNextBigFATSector(x)
   If x > 0 Then
      i = fatBBD(x).Adres
   Else
      i = 0
   End If
Loop

rc = True

myExit:
   GetFatSBD = rc
End Function

и сразу пишем функцию, которая будет возвращать номер следующего малого блока или 0, если текущий блок последний:

Public Function GetNextSmallFATSector(iSector As Long) As Long
' получить значение следующего сектора (малый FAT)
Dim x As Long
   GetNextSmallFATSector = 0
   x = fatSBD(iSector).Next
   If x > 0 Then
      GetNextSmallFATSector = x
   End If
End Function

Каталог

Каталог представляет собой описание структуры объектов составного файла, упорядоченных в виде дерева. Сам объект представлен 128-байтной записью со следующими полями (только те, что нас интересуют):

Смещение Размер поля Описание
Dec Hex
+1 +00h 64 байта Имя объкта (Unicode)
+65 +40h WORD Фактическая длина имени объекта (вместе с завершающим 0)
+67 +42h BYTE Тип объекта (1-подкаталог,2-поток(данные),5-корневой каталог)
+69 +44h DWORD Номер предыдущего объекта
+73 +48h DWORD Номер следующего объекта
+77 +4Ch DWORD Номер первого подчиненного объекта
+117 +74h DWORD Номер стартового сектора объекта
+121 +78h DWORD Размер объекта в байтах

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

Единственное, чего не имеет объект, так это своего собственного номера. А нумеруются они начиная с нуля 128-байтными "кусочками" относительно области данных каталога, номер стартового сектора этой области находится в заголовке файла (смещение +30h), а сама область вытягивается из FAT больших блоков.

Еще одно существенное замечание (оно не касается стартового объекта каталога Root Entry) – если размер объекта (смещение +78h) больше или равен 4096 байтам (1000h), то номер стартового сектора (смещение +74h) указывает на FAT больших блоков, в противном случае – на FAT малых блоков. Стартовый объект всегда находится в FAT больших блоков (ведь, как было показано выше, там живет адрес области данных малых блоков).

Для построения дерева из связанного списка существует много алгоритмов, предложу свой (не претендую ни на что! ;-) – просто он как-то сразу заработал.

Создадим модуль класса, назовем его clsNode и напишем следующий код

Option Explicit

Public PrevID As Long
Public NextID As Long
Public NodeName As String
Public NodeType As Long
Public StartNumber As Long
Public NodeSize As Long
Public SmallFat As Boolean
Public Key As String
Public NodeID As Long

Private mCol As New Collection
Private m_FirstChild As Long

Public Function Count() As Long
   Count = mCol.Count
End Function

Public Function Item(vItem As Variant) As clsNode
   Set Item = mCol.Item(vItem)
End Function

Public Property Get FirstChild() As Long
   FirstChild = m_FirstChild
End Property

Public Property Let FirstChild(ByVal vNewValue As Long)
   m_FirstChild = vNewValue
   If m_FirstChild > 0 Then
      GetNode m_FirstChild, mCol
   End If
End Property

Private Sub GetNode(vId As Long, mCol As Collection)
' получить данные о подчиненных узлах
Dim cn As clsNode
Dim xPrev As Long
Dim xNext As Long
Dim xLen As Long
Dim xFirst As Long
Dim xType As Long
Dim xStart As Long
Dim xText As String
Dim xSmall As Boolean
Dim i As Long
Dim x As Long
Dim iFile As Long
Dim dWord As DWORD_C
Dim sKey As String


   If vId > 0 Then
' получаем абсолютный адрес блока
      i = Root(vId)
' определяем длину заголовка
      dWord.b1 = fileBuf(i + 64)
      dWord.b2 = fileBuf(i + 65)
      dWord.b3 = 0
      dWord.b4 = 0
      xLen = HDec(dWord)
      If xLen > 0 Then
' если есть длина заголовка, считываем заголовок
' -3 - удаляем нулевой терминатор строки
         xText = vbNullString
         For x = 0 To xLen - 3 Step 2
            xText = xText & Chr$(fileBuf(i + x))
         Next
' определяем тип объекта
         xType = fileBuf(i + 66)
' определяем предыдущий объект
         dWord.b1 = fileBuf(i + 68)
         dWord.b2 = fileBuf(i + 69)
         dWord.b3 = fileBuf(i + 70)
         dWord.b4 = fileBuf(i + 71)
         xPrev = HDec(dWord)
' определяем сдедующий объект
         dWord.b1 = fileBuf(i + 72)
         dWord.b2 = fileBuf(i + 73)
         dWord.b3 = fileBuf(i + 74)
         dWord.b4 = fileBuf(i + 75)
         xNext = HDec(dWord)
' определяем подчиненный объект
         dWord.b1 = fileBuf(i + 76)
         dWord.b2 = fileBuf(i + 77)
         dWord.b3 = fileBuf(i + 78)
         dWord.b4 = fileBuf(i + 79)
         xFirst = HDec(dWord)
' определяем номер стартового блока в FAT
         dWord.b1 = fileBuf(i + 116)
         dWord.b2 = fileBuf(i + 117)
         dWord.b3 = fileBuf(i + 118)
         dWord.b4 = fileBuf(i + 119)
         xStart = HDec(dWord)
' определяем размер объекта
         dWord.b1 = fileBuf(i + 120)
         dWord.b2 = fileBuf(i + 121)
         dWord.b3 = fileBuf(i + 122)
         dWord.b4 = fileBuf(i + 123)
         xLen = HDec(dWord)
         If xLen             xSmall = True
         Else
            xSmall = False
         End If
' записываем объект в колекцию
         Set cn = New clsNode
         cn.NodeName = xText
         cn.NextID = xNext
         cn.PrevID = xPrev
         cn.NodeSize = xLen
         cn.NodeType = xType
         cn.SmallFat = xSmall
         cn.FirstChild = xFirst
         cn.StartNumber = xStart
         cn.NodeID = vId
         If xType = 2 Then
' что бы по значению ключа обозначить поток
            sKey = "S" & Format$(vId, "00000000")
         Else
            sKey = "C" & Format$(vId, "00000000")
         End If
         cn.Key = sKey
         mCol.Add cn, sKey
      End If
      Set cn = Nothing
' рекурсивно вызываем сами себя
      If xPrev > 0 Then GetNode xPrev, mCol
      If xNext > 0 Then GetNode xNext, mCol
   End If

End Sub


Private Sub Class_Terminate()
   Set mCol = Nothing
End Sub

В разделе деклараций нашего модуля (не класса!) объявим массив, который будет содержать ссылки на абсолютные адреса объектов каталога, а так же объявим наш класс:

Public Root() As Long ' здесь коллекция объектов *.md файла
Public tv As clsNode ' сюда копируется структура файла

и напишем функцию, которая достанет из файла структуру каталога

Public Function GetRootTree() As Boolean
' создание дерева каталога
Dim i As Long
Dim x As Long
Dim y As Long
Dim j As Long
Dim iCount As Long
Dim iMax As Long
Dim dWord As DWORD_C
Dim rc As Boolean

   rc = False
' определяем количество объектов каталога
   x = HeaderMD.StartRoot
   i = 1 + (HeaderMD.StartRoot + 1) * SECTORSIZE
   iMax = 0
' организуем цикл , если абс.сектор>0
   Do While i > 0
' всего в секторе четыре 128 байтных блоков
      iMax = iMax + 4
' получаем значение следующего сектора
      x = GetNextBigFATSector(x)
      If x > 0 Then
         i = fatBBD(x).Adres
      Else
         i = 0
      End If
   Loop
' определяем массив абс.адресов каталога
   ReDim Root(0 To iMax)
   x = HeaderMD.StartRoot
   i = 1 + (HeaderMD.StartRoot + 1) * SECTORSIZE
   iCount = 0
' организуем цикл , если абс.сектор>0
   Do While i > 0
' всего в секторе два 128 байтных блоков
      For j = 0 To 511 Step 128
' записываем абсолютный адрес в массив каталога
         Root(iCount) = i + j
         iCount = iCount + 1
      Next
' получаем значение следующего сектора
      x = GetNextBigFATSector(x)
      If x > 0 Then
         i = fatBBD(x).Adres
      Else
         i = 0
      End If
   Loop

' получаем абс.номер стартового сектора каталога
   i = 1 + (HeaderMD.StartRoot + 1) * SECTORSIZE
' получаем абс.адрес слова, где указан первый потомок(+4CH)
   i = i + 76
   dWord.b1 = fileBuf(i)
   dWord.b2 = fileBuf(i + 1)
   dWord.b3 = fileBuf(i + 2)
   dWord.b4 = fileBuf(i + 3)
   x = HDec(dWord)

   Set tv = New clsNode
   tv.NodeName = mdFileName ' называем корень именем составного файла
   tv.NodeID = 0
   tv.FirstChild = x ' всю структуру определит сам класс

   rc = True

myExit:
   GetRootTree = rc
End Function

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

* * *

В работе использованы материалы из статьи К.Е. Климентьева "Внутренний формат документов MS Word" и собственный опыт автора.

Пример реализации методики доступа к MD на VBA (Excel)

Комментарии

1
  • Хранитель_врат
    Замечательная статья. Но к сожалению, при попытке повторить программу обнаружил ошибку. В статье указано, что в секторе заголовка по смещению 44h находится номер начального сектора доп. таблицы размещения FAT больших блоков. Увы, это не так.
    У меня есть файл, размер которого >13Мб. Внутри используется 201 большой блок. Т.е. 201-109 блоков получается 92 блока, которые должны быть адресованы в дополнительной таблице. Однако по смещению 44h лежит 0. И приведенный пример программы не может считать оставшуюся часть FAT.
    Где же тогда следует искать дополнительную таблицу FAT?

Амнистия за дробление будет после декларирования всех подконтрольных компаний

Добровольный механизм декларирования взаимосвязанных юрлиц позволит властям определить, кто подпадет под амнистию.

Курсы повышения
квалификации

20
Официальное удостоверение с занесением в госреестр Рособрнадзора

Власти хотят увеличить НДФЛ для тех, кто зарабатывает 1 млн руб. в год. Мнение властей и бухгалтеров

В России хотят изменить прогрессивную шкалу налогообложения. Президент Владимир Путин предложил подумать над увеличением налоговой нагрузки на людей с более высокими доходами. Власти планируют поднять ставку НДФЛ до 15% для россиян с годовым доходом от 1 млн руб., или примерно от 83,3 тыс. в месяц.

Иллюстрация: Вера Ревина/Клерк.ру

Алексей Иванов рассказал как блог на Клерке помог компании «Мое дело» выйти из кризиса. В костюме фирмы Adidas

Стратегии монетизации контента на платформе «Клерк» с использованием вебинаров, рассылок и лит-магнитов.

2
Лучшие спикеры, новый каждый день

С серой зарплатой будут бороться комиссии

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

3

Криминальный электромобиль из Тольятти, а также крышесносная ИИ-ассистентка от OpenAI

Все самые важные и интересные финансовые новости в России и мире за неделю: Яндекс стал на две трети российским, банк Юникредит подготовили к ощипыванию, Ревущий котенок вернул мемность в акции, исход ИИ-безопасников из OpenAI, а также приговор разработчику Open Source кода для крипто-миксера Tornado Cash.

Минфин хочет, чтобы аудиторы страховали свою ответственность перед клиентами

Пока что аудиторские компании не обязаны страховать свою ответственность, они могут делать отчисления в компенсационный фонд СРО или обращаться добровольно за страховкой.

Опытом делятся эксперты-практики, без воды

КЭП, ПЭП и НЭП: как бизнесу подписывать документы различными видами электронной подписи

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

КЭП, ПЭП и НЭП: как бизнесу подписывать документы различными видами электронной подписи

Офис Клерка номинирован на премию Best Office Awards 2024

Объявлены проекты-номинанты премии Best Office Awards 2024. Награждение победителей премии состоится 31 мая в MTC Live Hall финале форума

Офис Клерка номинирован на премию Best Office Awards 2024
9

Иностранные компании и ИП из ЕАЭС будут платить НДС при онлайн-продаже товаров российским физлицам

При продаже товаров физлицам через электронные площадки продавцами из стран ЕАЭС местом реализации будет считаться Россия. В этой связи такая онлайн-торговля будет облагаться НДС в России.

Что ест бухгалтер. Вкусная еда и прогулки по Москве

Добрый день, коллеги! Расскажу, что у меня было на обед в выходные, а также поделюсь фотографиями с интересных прогулок.

Что ест бухгалтер. Вкусная еда и прогулки по Москве

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

Узнай все про субсидиарную ответственность бухгалтера, чтобы не допустить ошибок и свести риск субсидиарной ответственности к нулю.

Бесплатно с Бухгалтерский учет

Как отразить в учете на УСН «доходы минус расходы» расходы по чекам от директора за прошлые годы. Мини-курс

Директор принес бухгалтеру чеки на материалы за 2021 и 2022 год. Что с ними делать, можно ли принять их к учету в 2024 году? Как поступить, рассказываем в сегодняшнем мини-курсе.

Как отразить в учете на УСН «доходы минус расходы» расходы по чекам от директора за прошлые годы. Мини-курс
Миникурсы, текстовые и видеоинструкции для бухгалтеров

Какие налоги платят в Грузии и сложно ли это делать?

Грузия — одна из самых популярных стран для релокации у россиян. Сюда можно переехать только по загранпаспорту, снять жилье стало дешевле, а большинство релокантов платят всего 1% налога после открытия ИП. Вместе с экспертами гайд разобрался с тем, какие налоги нужно платят и кому доступны сниженные ставки.

Центробанк определит параметры кредитных каникул для бизнеса

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

Переплата по налогам, которая не вошла в сальдо ЕНС, не идет в расходы

Старая переплата по налогам на 01.01.2023 не вошла в сальдо ЕНС, но и в состав внереализационных расходов ее включать нельзя.

Кратко за день: В Думе сегодня начнется публичное обсуждение налоговой реформы

Утром на рынке небольшой позитив. Нефть торгуется по 84,1$. Доллар стоит 91,1₽. Биткоин снизился до 66700$.

Кратко за день: В Думе сегодня начнется публичное обсуждение налоговой реформы
1

☀️ Акция «Жаркие скидки в любую погоду»! Курсы по учету на маркетплейсах, УСН, ВЭД, финмоделированию, ФСБУ и бухгалтерии с нуля за 4 290 рублей

Сегодня последний день акции на курсы по учету на маркетплейсах, УСН, ВЭД, финмоделированию, ФСБУ и бухгалтерии с нуля. Успейте купить топовые онлайн-курсы со скидкой за 4 290 рублей! 21 мая цена вырастет.

☀️ Акция «Жаркие скидки в любую погоду»! Курсы по учету на маркетплейсах, УСН, ВЭД, финмоделированию, ФСБУ и бухгалтерии с нуля за 4 290 рублей
2

О рисках при экспорте товара в случае выставления счета-фактуры в неверной валюте

Разбираемся, вправе ли налоговый орган отказать в применении ставки 0% по НДС при экспорте товаров в случае выставления счета-фактуры, в котором неверно указана валюта платежа.

🔥 Акция «Жаркие скидки в любую погоду»! Курсы по учету на маркетплейсах, УСН, ВЭД, финмоделированию, ФСБУ и бухгалтерии с нуля за 4 290 рублей

Сегодня последний день акции на курсы по учету на маркетплейсах, УСН, ВЭД, финмоделированию, ФСБУ и бухгалтерии с нуля. Успейте купить топовые онлайн-курсы со скидкой за 4 290 рублей! 21 мая цена вырастет.

Интересные материалы

Утвердят новый показатель для расчета накопительной пенсии

В следующем году накопительная пенсия будет меньше.