XLS для программиста

Автор:
Оригинал:
Дата:
Распространение:
Семенов Олег
http://www.oldi.ru/review/xls/xls.htm
август 2002
GPL

Документ описывает формат файла MS Exel различных версий, и делится на две логические части: описание формата OLE2 файла и описание формата BIFF файла. Описание собрано на основе проработки отдельных статей и ... (censored. Some companies don't like debugging).

Документ основывается на разрозненном англоязычном материале. Описание OLE2 основано на документе "DIG2000 file format proposal" и личных опытах работы с OLE2 файлами. Описание BIFF основано на документации Microsoft к формату BIFF и трудах OpenOffice проекта с дополнениями автора. Основное внимание уделено форматам BIFF5, BIFF7 как наиболее универсальные в плане чтения разными версиями Excel, но рассмотрены и другие форматы. К сожалению, полной документации нет даже у Microsoft (некоторые функции помечены как требующие документации). Данный документ не претендует на полное описание форматов Excel. Главная цель документа - дать описание основных функций, которые позволят читателю создать полноценный файл Excel, написать программу чтения или записи формата Excel. Документ также будет полезен людям, работающим с любыми форматами, использующими OLE2 контейнер (MS Office документы, OpenOffice документы и т.п.)

Отмазка:

Здесь уместно процитировать предисловие к главе "A: Structured Storage" из статьи "DIG2000 file format proposal":

Бинарный формат OLE2 файла является собственностью Microsoft. Мы надеемся, что они не лицензируют и не взимают плату за создание третьими сторонами независимых реализаций программ чтения и записи файлов OLE2.

Содержание:
  1. Введение
  2. Формат OLE2 контейнера
    2.1. Типы данных
    2.2. Заголовок
    2.3. FAT
      2.3.1. Мини FAT
      2.3.2. DIF сектора
    2.4. Property Set Storage
    2.5. Мини-поток
    2.6. Чтение файловой системы
    2.7. Property Sets
  3. Формат BIFF
  4. Литература

1. Введение

1.1. Версии форматов Excel

Формат файла Excel называется BIFF (Binary Interchange File Firmat).

Верия Excel Версия BIFF Тип документа Формат файла
Excel 2.1 BIFF2 Worksheet Stream
Excel 3.0 BIFF3 Worksheet Stream
Excel 4.0 BIFF4 Worksheet or Workbook Stream
Excel 5.0 BIFF5 Workbook OLE2 Storage
Excel 7.0 (Excel 95) BIFF7 Workbook OLE2 Storage
Excel 8.0 (Excel 97) BIFF8 Workbook OLE2 Storage
Excel 9.0 (Excel 2000) BIFF8 Workbook OLE2 Storage
Excel 10.0 (Excel XP) BIFF8 Workbook OLE2 Storage

С версии Excel 5.0 BIFF файл инкапсулируется в OLE2 контейнер. Этот контейнер представляет собой настоящую файловую систему. И заслуживает отдельного разговора. С формата OLE2 контейнера и начнем рассмотрение формата Excel.

Замечание: если Вы пишите в среде MS Windows, то здесь реализованы интерфейсы для работы с OLE2 контейнером: IStream и IStorage.

2. Формат OLE2 контейнера

Что бы не нарушать никаких авторских прав, в разных источниках формат OLE имеет различные названия: LAOLA, POIFS, но фактически они описывают одну и ту же структуру файловой системы.

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

OLE файл построен как файловая система. Все пространство файла разбито на сектора. Размер сектора определяется при создании файла и как правило равен 512 байтам. Виртуальный поток состоит из последовательности секторов. Сектора нумеруются от -1 (Header) с шагом 1. Почти все переменные ссылаются на номер сектора, а не на смещение.

Файл имеет различные типы секторов: FAT, Directory, MiniFAT, DIF, Storage. Отдельный тип сектора - заголовок (Header). Основное отличие его от всех секторов, это то, что заголовок всегда 512 байт и всегда начинается со смещения 0. Остальные сектора могут иметь другой размер (определенный при создании) и располагаться в любом месте файла.

Для простоты, при создании Excel файла можно исключить мини-потоки и MiniFAT, как это сделано, например, в библиотеке Spreadsheet-WriteExcel для Perl. Мини поток в обычном файле Exel используется в основном для внедренных объектов. Если таковых нет, то можно обойтись и без мини-потока. А если файл небольшой (менее 7 мб), то исключить и DIF сектора.

2.1 Типы данных

Для описания данных в структуре файла вводятся новые типы данных:

typedef unsigned long
typedef unsigned short
typedef short
typedef ULONG
typedef ULONG
typedef USHORT
typedef ULONG
typedef unsigned char
typedef unsigned short
typedef unsigned long
typedef WORD
typedef ULONG
typedef CLSID

typedef struct tagFILETIME {
  DWORD
  DWORD
} FILETIME, TIME_T;

const SECT
const SECT
const SECT
const SECT

ULONG;
USHORT;
OFFSET;
SECT;
FSINDEX;
FSOFFSET;
DFSIGNATURE;
BYTE;
WORD;
DWORD;
DFPROPTYPE;
SID;
GUID;


dwLowDateTime;
dwHighDateTime;


DIFSECT = 0xFFFFFFFC;
FATSECT = 0xFFFFFFFD;
ENDOFCHAIN = 0xFFFFFFFE;
FREESECT = 0xFFFFFFFF;

// 4 bytes
// 2 bytes
// 2 bytes
// 4 bytes
// 4 bytes
// 2 bytes
// 4 bytes
// 1 bytes
// 2 bytes
// 4 bytes
// 2 bytes
// 4 bytes
// 16 bytes

// 8 bytes




// 4 bytes
// 4 bytes
// 4 bytes
// 4 bytes

2.1.1. Порядок байт

Все данные, содержащие более одного байта, хранятся, используя Intel (Little-Endian) нотацию. Это значит, что менее значимый байт хранится первым, а наиболее значимый байт - последним. Например, 32-битное значение 12345678H преобразуется в 78H 56H 34H 12H

2.2. Заголовок

Самый первый сектор в файле - заголовок (Header), имеющий совершенно определенную структуру.

Смещ.
HEX
Тип Размер
байт
Переменная Значение, как в файле Описание
000 BYTE 8 _abSig[8]; D0CF11E0 A1B11AE1 Идентификатор. Всегда постоянная (0 x E011CFD0, 0 x E11AB1A1), в старых версиях (beta 2, до 92 года) имел значение {0E11FC0D D0CF11E0}
008 CLSID 16 _clid; 00000000 00000000
00000000 00000000
Class ID. Устанавливается WriteClassStg, считывается GetClassFile/ReadClassStg. Для Excel как правило = 0
018 USHORT 2 _uMinorVersion; 3E00 Младшее значение версии формата. Для Excel константа 3EH
01A USHORT 2 _uDllVersion; 0300 Старшее значение версии Dll/формата. Для Excel константа 3H
01С USHORT 2 _uByteOrder; FEFF 0 x FFFE говорит, что используется Intel нотация
01E USHORT 2 _uSectorShift; 0900 Размер сектора. Обычно равно 9, что указывает на размер 512 байт (29)
020 USHORT 2 _uMiniSectorShift; 0600 Размер мини-сектора. Обычно равно 6, что указывает на размер 64 байт (26)
022 USHORT 2 _usReserved; 0000

Зарезервировано, должно быть равно 0

024 ULONG 4 _ulReserved1; 00000000 Зарезервировано, должно быть равно 0
028 ULONG 4 _ulReserved2; 00000000

Зарезервировано, должно быть равно 0

02C FSINDEX 4 _csectFat;   Число секторов, в которых размещается FAT. Если файл <7Мб, то равно 1, если больше, то больше 1 и появляется DIF сектор.
030 SECT 4 _sectDirStart;   Номер первого сектора, в котором размещается Property Set Storage (еще называют FAT Directory или Root Directory Entry)
034 DFSIGNATURE 4 _signature; 00000000 Подпись для транзакций. Excel не использует транзакции. Должно быть равно 0.
038 ULONG 4 _ulMiniSectorCutoff; 00100000 Максимальный размер мини-потока. Обычно 4096
03С SECT 4 _sectMiniFatStart;   Первый сектор мини-FAT. Если 0 х FFFFFFFE (-2), то мини-поток отсутствует.
040 FSINDEX 4 _csectMiniFat;   Число секторов в цепочке мини-FAT. 0, если мини-потока нет
044 SECT 4 _sectDifStart;   Первый сектор в DIF цепочке. Если файл <7Мб, то DIF цепочка отсутствует и значение равно 0 x FFFFFFFE (-2)
048 FSINDEX 4 _csectDif;   число секторов в DIF цепочке.0, если файл <7Мб
04С SECT 436 _sectFat[109];   Номера первых 109 секторов, в которых располагается FAT. Если файл <7Мб, то сектор один, остальные значение заполняются 0 х FFFFFFFF (-1).

Пример простого файла Excel:

Размер сектора 512 байт. Размер мини-сектора 64 байта. FAT расположен в секторе 24 (18H, смещение 00003200) и он единственный. Root Directory Entry располагается в 25 (19H, смещение 00003400) секторе. Мини-потоки отсутствуют.

2.3. FAT

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

Каждый виртуальный поток файла представлен в FAT цепочкой из номеров блоков, в которых расположен виртуальный поток. Есть специальные значения:

В этом примере блоки идут подряд, но это не обязательно.

Как читать FAT:

FAT состоит из блоков по 4 байта, пронумерованных с 0. В блоке указан номер блока FAT или, что то же самое, номер следующего принадлежащего потоку блока. Итак, в блоке FAT0 значение 1. Это значит, что поток начинается с блока 0 и следующий блок 1 (блок файла и блок FAT). В блоке 1 значение 2, и т.д. до блока 7. В нем значение -2 (0 x FFFFFFFE), что означает конец цепочки. В результате, первый поток содержит блоки файла {0,1,2,3,4,5,6,7}. В нашем примере присутствуют еще 2 потока: {8,9,10,11,12,13,14,15}, {16,17,18,19,20,21,22,23}.

Следующее значение, расположенное в блоке 24, это -3 (0 x FFFFFFFD). Оно означает, что в блоке 24 файла находится сам FAT.

Осторожно, не пропустите следующее значение в блоке 25, это -2 (0 x FFFFFFFE). Оно означает, что в этом блоке что-то располагается. А из заголовка нам известно, что здесь располагается Property Set Storage. Property Set Storage - обычный виртуальный поток, который обычным образом отображается в FAT.

Остальные значение равны -1 (0 x FFFFFFFF). Это значит, что дальше пустые сектора и ими забит остаток FAT до 512 байт.

2.3.1. Мини FAT

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

Расположение мини-FAT можно узнать из заголовка. В переменной _sectMiniFatStart хранится номер сектора с мини-FAT, а в переменной _csectMiniFat - число занимаемых мини-FAT секторов. Мини-FAT - это такой же виртуальный поток, как и все остальные, поэтому он представлен в обычном FAT, и если он занимает несколько секторов, то не составит труда восстановить цепочку секторов мини-FAT по обычному FAT.

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

Как собрать воедино мини-поток будет показано ниже, так как для этого необходимо рассмотреть структуру Property Set Storage.

2.3.2. DIF сектора

DIF сектора (Double Indirect Fat) служат для организации целого FAT из нескольких FAT секторов. Для оптимизации, первые 109 секторов FAT представлены в заголовке и никакого DIF сектора в небольшом (<7 Мб) файле нет.

DIF сектор - это как бы виртуальное продолжение заголовка (перечисления секторов, в которых располагается FAT). Первые 109 секторов указываются в заголоовке, остальные - в DIF секторах.

Есть ли в файле DIF сектора и сколько их, известно из заголовка (_sectDifStart - первый сектор в DIF цепочке, _csectDif - число секторов DIF). Если DIF сектор не один, то последнне значение в секторе - указатель на блок, в котором расположен следующий DIF сектор.

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

2.4. Property Set Storage

Может возникнуть небольшая путаница с названиями Property Set Storage и Property Set. Вроде одно относится к другому, но Property Set Storage на самом деле гораздо шире, чем просто контейнер для Property Set. В некоторых источниках его называют Directory chain - цепь директорий или попросту дерево каталогов. Это более правильное название. Как и в FAT DOS системы, здесь есть файлы и каталоги.

Каждый виртуальный поток файла имеет запись в дереве каталогов. Первый сектор дерева каталогов - Root Directory Entry (корень). Корень может содержать как потоки (Stream), так и контейнеры (Storage). Имя Root Directory Entry обычно содержит строку "Root Entry" в Unicod'e, но в некоторых версиях содержится только первая буква "R". Эта строка всегда игнорируется, поскольку Root Entry известна по своей позиции и идентификатору SID 0 (он нигде не указан, а отсчитывается по подряд идущим записям в дереве каталогов, начиная с нуля) лучше, чем по своему имени. Новые разработки должны записывать имя "Root Entry", но поддерживать работу и со старыми версиями, в которых записано "R".

Каждый уровень иерархии (набор ветвей) представлен как красное/черное дерево. Родитель этого набора ветвей имеет указатель на вершину этого дерева. Дерево должно отвечать следующим требованиям, чтобы быть правильным:

С помощью красных узлов можно организовать как бы дерево в дереве.

Простейшая реализация, когда все узлы - черные. В этом случае получается бинарное дерево. Все сказанное про цвета относится к спецификации OLE2. Если Вас интересует только Excel, без дополнительных объектов типа MS Equation, то используйте бинарное дерево и не задумывайтесь о цвете. Он используется в основном для внешних объектов. Например, если вставляется в таблицу документ MS Word, то узел Word Document будет красным и содержать дерево относящееся только к внедренному документу Word.

Некорневые записи могут быть как потоковыми (STGTY_STREAM) - виртуальные файлы, так и контейнерными (STGTY_STORAGE) элементами - виртуальные директории. Контейнерные элементы имеют _clsid, _time[], и _sidChild значения, потоковые элементы могут и не иметь. Потоковые элементы имеют установленные _sectStart и _ulSize поля, в то время как эти поля в контейнерных элементах могут быть равны 0 (за исключением корня).

Чтобы определить физическое расположение потока (файла) для потокового элемента, необходимо сначало определить, какой FAT (обычный или мини) используется для потока. Поток, у которого значение поля _ulSize меньше, чем _ulMiniSectorCutoff располагается в мини-потоке и _startSect используется как индекс в MiniFat (который начинается с _sectMiniFatStart). Поток, чей _ulSize больше, чем _ulMiniSectorCutoff располагается в стандартном FAT, его _startSect используется как индекс в стандартном FAT.

typedef enum tagSTGTY {
  STGTY_INVALID = 0,
  STGTY_STORAGE = 1,
  STGTY_STREAM = 2,
  STGTY_LOCKBYTES = 3,
  STGTY_PROPERTY = 4,
  STGTY_ROOT = 5,
} STGTY ;
typedef enum tagDECOLOR {
  DE_RED = 0,
  DE_BLACK = 1,
} DECOLOR ;

Смещ.
HEX
Тип Размер
байт
Переменная Значение, как в файле Описание
000 BYTE 64 _ab[32*sizeof(WCHAR)];   Название элемента в Unicode, оканчивающегося нулем. Остаток места заполняются нулями
040 WORD 2 _cb;   Длина элемента в байтах, включая завершающий ноль
042 BYTE 1 _mse   Тип объекта. Значение берется из перечисления STGTY
043 BYTE 1 _bflags; 01 Значение берется из перечисления DECOLOR
044 SID 4 _sidLeftSib;   SID левой ветви этой записи в дереве каталогов. Т.е. ID предыдущего элемента
048 SID 4 _sidRightSib;   SID правой ветви этой записи в дереве каталогов. Т.е. ID следующего элемента
04C SID 4 _sidChild;   SID первого ребенка, выполняющего роль корня для всех детей этого элемента (если _mse = STGTY_STORAGE)
050 GUID 16 _clsId;   CLSID этого контейнера (если _mse = STGTY_STORAGE)
060 DWORD 4 _dwUserFlags;   Пользовательский флаг контейнера (если _mse = STGTY_STORAGE)
064 TIME 16 _T_time[2];   Время создания/изменения (Timestamp). Первые 8 байт - время создания, вторые 8 байт - время изменения
074 SECT 4 _sectStart;   Первый сектор потока (если _mse = STGTY_STREAM). Если _mse = STGTY_ROOT, здесь может быть значение первого сектора мини-потока
078 ULONG 4 _ulSize;   Размер потока в байтах (если _mse = STGTY_STREAM). Если _mse = STGTY_ROOT, здесь может быть размер мини-потока
07C DFPROPTYPE 2 _dptPropType;   Зарезервировано на будущее. Должно быть равно 0

Посмотрим на пример. Здесь 4 элемента в дереве каталогов. Нумеруются они подряд начиная с 0, т.е. Root Entry имеет SID 0, Book - SID 1, Summary Information - SID 2, Document Summary - SID 3.

_ab = "R o o t   E n t r y  "  
_cb = 16H (22) Включает завершающий 0. Каждый символ занимает 2 байта (и 0 тоже).
_mse = 05 STGTY_ROOT. Показывает, что данная запись - корневая
_bflsgs = 01 DE_BLACK. Ветвь черная.
_sidLeftSib = FFFFFFFF -1. Нет предыдущего
_sidRightSib = FFFFFFFF -1. Нет следующего
_sidChild = 02000000 Ребенок SID 2 (Summary Information)
_clsId = 1008 0200 0000 0000 
  С000 0000 0000 0046
Class ID. Если не знаете, заполняйте нулями
_dwUserFlags = 00000000 Для STGTY_ROOT недоступен
_T_time[2] = 0000 0000 0000 0000
  0000 0000 0000 0000
Первые 8 байт - время создания, вторые 8 байт - время изменения. Все нули - время не установлено
_sectStart = FFFFFFFF Здесь может быть значение первого сектора мини-потока. -1, значит мини-поток отсутствует.
_ulSize = 00000000 Здесь может быть размер мини-потока в байтах. 0 - мини-потока нет.
_dptPropType = 0000 Всегда 0

_ab = "B o o k  "  
_cb = 0AH (10) Включает завершающий 0. Каждый символ занимает 2 байта (и 0 тоже).
_mse = 02 STGTY_STREAM. Показывает, что данная запись - поток.
_bflsgs = 01 DE_BLACK. Ветвь черная.
_sidLeftSib = FFFFFFFF -1. Нет предыдущего
_sidRightSib = FFFFFFFF -1. Нет следующего (Почему?)
_sidChild = FFFFFFFF Нет детей. Для потока не доступно.
_clsId = 0000 0000 0000 0000 
  0000 0000 0000 0000
Class ID. Если не знаете, заполняйте нулями
_dwUserFlags = 00000000 Не установлен
_T_time[2] = 0000 0000 0000 0000
  0000 0000 0000 0000
Первые 8 байт - время создания, вторые 8 байт - время изменения. Все нули - время не установлено
_sectStart = 00000000 Может показаться, что ничего не установлено. На самом деле установлен 0, т.е. поток начинается с нулевого блока.
_ulSize = 00100000 4096 байт. Размер потока.
_dptPropType = 0000 Всегда 0

_ab

= 05H" S u m m a r y
I n f o r m a t i o n  "

 
Название начинается с 05H - это специальные имена, указывающие, что данный поток Property Set и, что он отвечает определенному формату, который будет описан ниже
_cb = 28H (40) Включает завершающий 0. Каждый символ занимает 2 байта (и 0 тоже).
_mse = 02 STGTY_STREAM. Показывает, что данная запись - поток.
_bflsgs = 01 DE_BLACK. Ветвь черная.
_sidLeftSib = 01000000 Предыдущий элемент с SID 1 - Book
_sidRightSib = 03000000 Следующий элемент с SID 3 - Document Summary
_sidChild = FFFFFFFF Нет детей. Для потока не доступно.
_clsId = 0000 0000 0000 0000 
  0000 0000 0000 0000
Class ID. Если не знаете, заполняйте нулями
_dwUserFlags = 00000000 Не установлен
_T_time[2] = 0000 0000 0000 0000
  0000 0000 0000 0000
Первые 8 байт - время создания, вторые 8 байт - время изменения. Все нули - время не установлено
_sectStart = 08000000 Поток начинается с блока 8.
_ulSize = 00100000 4096 байт. Размер потока.
_dptPropType = 0000 Всегда 0

_ab

= 05H" D o c u m e n t
S u m m a r y
I n f o r m a t i o n  "

 
Название начинается с 05H - это специальные имена, указывающие, что данный поток Property Set и, что он отвечает определенному формату, который будет описан ниже
_cb = 38H (56) Включает завершающий 0. Каждый символ занимает 2 байта (и 0 тоже).
_mse = 02 STGTY_STREAM. Показывает, что данная запись - поток.
_bflsgs = 01 DE_BLACK. Ветвь черная.
_sidLeftSib = FFFFFFFF -1. Нет предыдущего (Почему?)
_sidRightSib = FFFFFFFF -1. Нет следующего
_sidChild = FFFFFFFF Нет детей. Для потока не доступно.
_clsId = 0000 0000 0000 0000 
  0000 0000 0000 0000
Class ID. Если не знаете, заполняйте нулями
_dwUserFlags = 00000000 Не установлен
_T_time[2] = 0000 0000 0000 0000
  0000 0000 0000 0000
Первые 8 байт - время создания, вторые 8 байт - время изменения. Все нули - время не установлено
_sectStart = 08000000 Поток начинается с блока 8.
_ulSize = 00100000 4096 байт. Размер потока.
_dptPropType = 0000 Всегда 0

Все потоки должны принадлежать корню (поскольку каталогов нет), но судя по ссылкам (_sidLeftSib, _sidRightSib, _sidChild) - это дерево вида:

Root Entry
|
Book <--  Summary Information  --> Document Summary Information

Т.е. _sidChild показывает с какой ветвью связан каталог, а уже от этой ветви идут связи вправо и влево (_sidLeftSib, _sidRightSib).

Почему же только Summary Information имеет ссылки на соседние ветви? В принципе, Book имеет правого соседа, а Document Summary Information - левого, но эти ссылки для обхода дерева ничего не дают полезного, поэтому -1. На самом деле, если в этих местах будет ссылка, то она будет идти не на того соседа, с которого пришли, а совсем на другого. То есть, каждый элемент фактически может иметь 2 соседа справа и два соседа слева.

Часто в файле Excel можно встретить потоки (первые два обязательны):

и контейнеры:

Во всех потоках записи располагаются последовательно, они никогда не включаются в другие записи. Исключение составляет BIFF8: поток объекта Escher разделен и включается в несколько записей MSODRAWING.

2.5. Мини-поток

Как найти данные мини-потока? Для этого нужно обратиться к записи Root Entry в Property Set Storage. Со смещением 074H в переменной _sectStart указан номер первого сектора, в котором находятся данные мини-потока. Что бы определить весь мини-поток, необходимо проследить цепочку в основном FAT, начиная с сектора _sectStart.

Данные в мини-потоке разбиты на блоки размером _uMiniSectorShift и нумеруются с 0. Цепочки данных в мини-потоке описываются мини-FAT'ом, используя эту нумерацию.

2.6. Чтение файловой системы

Примерный алгоритм чтения файла следующий:

1. Читаем заголовок.

2. Читаем FAT

3. Если есть мини-FAT, читаем его

3. Читаем Property Set Storage

4. Читаем необходимые нам данные (на примере записи "Book")

2.7. Property Sets

Наборы свойств (Property Sets) необходимы для хранения небольшой информации (вроде авторства и т.п.). Они располагаются в файле как отдельная запись, поэтому очень удобны для передачи данных другим приложениям. Например, какой-нибудь информационной программе нужно быстро узнать авторов всех расположеных на диске файлов. Она читает каждый файл, обращается в Property Set Storage, находит необходимую запись, и читает только необходимую для нее информацию (я опустил, что еще надо прочитать заголовок, FAT и т.п., т.е. то, что в любом случае надо делать), не трогая остальное.

Все Наборы свойств представлены в Property Set Storage, причем их имена начинаются с символа 0 x 05, что говорит о том, что данные будут в определенном формате, описанном ниже.

Наборы свойств имеют заголовок, пару Format ID/смещение и секцию (теоретически спецификация OLE2 допускает более, чем одну секцию, но это не поддерживается). Секция также имеет некоторое деление. Лучше понять формат Набора свойств поможет таблица:

Property Set Header
Порядок байт Версия формата Версия ОС Класс ID Зарезервировано
Пара Format ID/ Смещение
FMTID Offset
Секция
Заголовок
Размер секции Колличество значений в Свойстве, m
Смещения
Property ID для Свойства 1
...
Property ID для Свойства m
Offset
...
Offset
Массив значений
Тип Свойства 1
...
Тип Свойства m
Значение Свойства 1
...
Значение Свойства m

Размер одного Набора свойств лимитирован 256 кб. Все данные находятся в Intel нотации. В заголовке может смутить часть "порядок байт", но она не используется и включена для будущих реализаций.

Property Set Header

typedef struct PROPERTYSETHEADER {
  WORD    wByteOrder;     // Всегда 0 х FFFE
  WORD    wFormat;          // 0 показывает, что формат правильный и принадлежит Property Set
  DWORD dwOSver;          // Старшее слово показывает операционную систему: 0 - Windows, 1 - Macintosh, 2 - Windows 32 bit, 3 - Unix. Младшее слово показывает номер версии операционной системы
  CLSID     clsid;                // Идентификатор класса, который показывает или дает доступ к Набору свойств
  DWORD reserved;          // Зарезервировано. Всегда 1.
PROPERTYSETHEADER;

Пара Format ID/ Смещение

typedef struct FORMATIDOFFSET {
  FMTID    fmtid;                // Имя секции
  DWORD dwOffset;           // Смещение в байтах от начала всего Набора свойств
} FORMATIDOFFSET;

Секция

Каждая секция состоит из

typedef struct PROPERTYSECTIONHEADER {
  DWORD                             cbSection;    // Размер секции в байтах
  DWORD                             cProreties;   // Колличество Свойств в наборе
  PROPERTYIDOFFSET rgprop[];         // Перечисление адресов Свойств
PROPERTYSECTIONHEADER;

typedef struct PROPERTYIDOFFSET {
  DWORD                            propid;          // Название Свойства
  DWORD                             dwOffset;       // Смещение в байтах от начала секции до Свойства
PROPERTYIDOFFSET;

typedef struct SERIALIZEDPROPERTYVALUE {
  DWORD                           dwType;           // Тип Свойства
  BYTE                               rgb[];               // Собственно само значение Свойства
SERIALIZEDPROPERTYVALUE;

Наиболее полная информация собрана в статье Charlie Kindel "OLE Property Set Exposed" из библиотеки MSDN.