Здравствуйте, читатели Хабрахабр!
Это завершение поста начатого вот здесь.
В принципе, мы почти все уже сделали, осталось небольшое окончание. Напомню, мы взяли EDB базу, открыли ее, воспользовавшись технологией ESE, перечислили имеющиеся таблицы и начали перечислять колонки внутри этих таблиц. Нам осталось дополучить колонки, получить значения столбцов и вуаля, база прочитана.
В предыдущем посте я не дописал функцию отвечающую за переход на следующую колонку в таблице, а также описание структуры SColumnInfo, вот они:
typedef struct tagColumnInfo
{
DWORD dwId;
std::wstring sName;
DWORD dwType;
DWORD dwMaxSize;
}SColumnInfo;
bool CJetDBReaderCore::TableEnd ( std::wstring sTableName )
{
std::map <std::wstring, JET_TABLEID>::const_iterator iter = m_tables.find ( sTableName );
if ( iter != m_tables.end() )
{
if ( _JetMove ( m_sesid, iter->second, JET_MoveNext, 0 ) )
{
return true;
}
else return false;
}
return true;
}
Соответственно когда перейти на следующий элемент не удается, то это признак, что таблица закончилась.
Мы получили список колонок и занесли их в наши структуры, осталось получить значения.
Получаем значения ячеек в колонках
Пишем следующую функцию:
JET_ERR CJetDBReaderCore::EnumColumnsValues (
SDBTableInfo &sTableInfo,
std::list<SColumnInfo> &sColumnsInfo )
{
typedef std::basic_string<byte> CByteArray;
JET_ERR jRes = OpenTable ( sTableInfo.sTableName );
if ( jRes == JET_errSuccess )
{
JET_COLUMNLIST sColumnInfo;
jRes = GetTableColumnInfo ( sTableInfo.sTableName, &sColumnInfo, FALSE );
if ( jRes != JET_errSuccess ) return jRes;
if ( !MoveToFirst ( sTableInfo.sTableName ) )
{
std::vector<CByteArray> Holders(sColumnsInfo.size());
std::vector<CByteArray>::iterator HolderIt = Holders.begin();
std::vector<JET_RETRIEVECOLUMN> Row ( sTableInfo.sColumnInfo.size() );
std::vector<JET_RETRIEVECOLUMN>::iterator It = Row.begin();
std::list<SColumnInfo>::const_iterator ColumnInfoIt
= sColumnsInfo.begin();
for ( int nColNum = 0; ColumnInfoIt != sColumnsInfo.end();
++ColumnInfoIt, ++It, ++HolderIt, ++nColNum )
{
DWORD dwMaxSize = ColumnInfoIt->dwMaxSize ? ColumnInfoIt->dwMaxSize :
MAX_BUFFER_SIZE;
It->columnid = ColumnInfoIt->dwId;
It->cbData = dwMaxSize;
HolderIt->assign(dwMaxSize, '\0');
It->pvData = const_cast<byte*>(HolderIt->data());
It->itagSequence = 1;
}
do
{
GetColumns (
sTableInfo.sTableName, &Row[0],
static_cast <INT> ( Row.size() ) );
ColumnInfoIt = sColumnsInfo.begin();
int iSubItem = 0;
for ( It = Row.begin(); It != Row.end(); ++It,
++ColumnInfoIt, ++iSubItem )
{
if((*It).cbActual)
{
std::wstring s;
std::string tmp;
wchar_t buff[16];
switch(ColumnInfoIt->dwType)
{
case JET_coltypBit:
s = *reinterpret_cast<byte*>((*It).pvData) ? L"True" : L"False";
break;
case JET_coltypText:
tmp.assign(reinterpret_cast<char*>((*It).pvData), (*It).cbActual);
s.assign(tmp.begin(), tmp.end());
break;
case JET_coltypLongText:
s.assign(reinterpret_cast<wchar_t*>((*It).pvData),
(*It).cbActual / sizeof ( wchar_t ) );
break;
case JET_coltypUnsignedByte:
s = _ltow(*static_cast<byte*>((*It).pvData), buff, 10);
break;
case JET_coltypShort:
s = _ltow(*static_cast<short*>((*It).pvData), buff, 10);
break;
case JET_coltypLong:
s = _ltow(*static_cast<long*>((*It).pvData), buff, 10);
break;
case JET_coltypGUID:
wchar_t wszGuid[64];
::StringFromGUID2(*reinterpret_cast<const GUID*>((*It).pvData),
wszGuid, sizeof(wszGuid));
USES_CONVERSION;
s = wszGuid;
break;
}
sTableInfo.sColumnInfo[iSubItem].sColumnValues.push_back ( s );
}
else sTableInfo.sColumnInfo[iSubItem].sColumnValues.push_back ( L"<empty>" );
}
}
while ( !TableEnd ( sTableInfo.sTableName ) );
}
jRes = CloseTable ( sTableInfo.sTableName );
}
return jRes;
}
Мы пройдемся по всем строчкам и для каждой вычитаем значение ячеек. И начнем с того, что переместимся на первый элемент, установив курсор в соответствующую позицию (функция MoveToFirst, см. предыдущий пост).
Все очень похоже на то, как мы перечисляли имена таблиц из MSysObjects, за одним исключением: тогда мы знали, что данные имеют строковый тип, а здесь данные могут быть произвольными. Поэтому мы создадим специальные byte’вые контейнеры Holders куда и сохраним будущую информацию, а так как это обычный вектор, то нам не придется думать об очистки памяти, она сама уничтожится когда мы выйдем из зоны видимости этой функции.
Т.е.:
- Для краткости объявим:
typedef std::basic_string<byte> CByteArray
- Создадим Holder'ы для данных, в них и попадет информация из базы:
std::vector<CByteArray> Holders(sColumnsInfo.size());
- А далее в цикле увяжем куски памяти куда будут возвращаться данные и наши холдеры, а для этого сначала выделим под них память, достаточную для удержания всей информации (максимальный размер данных мы получили при перечислении колонок):
HolderIt->assign(dwMaxSize, '\0');
It->pvData = const_cast<byte*>(HolderIt->data()); - А теперь в цикле вернем информацию для каждой колонки в наш вектор JET_RETRIEVECOLUMN, который использует память выделенную для холдеров:
GetColumns(sTableInfo.sTableName, &Row[0],static_cast <INT> ( Row.size() ) );
В итоге мы вычитаем одну строчку из таблицы. Но т.к. данные это массив байт, его нужно преобразовать во что-то более читаемое, поэтому мы попытаемся распарсим ячейки в зависимости от их типов. В данном примере разбираются следующие типы (как наиболее очевидные):
- JET_coltypBit
- JET_coltypText
- JET_coltypLongText
- JET_coltypUnsignedByte
- JET_coltypShort
- JET_coltypLong
- JET_coltypGUID
Описание имеющихся в ESE типов можно найти здесь.
Заключение
Вот мы и написали viewer почтовой базы! Если начать копаться в этих бесконечных таблицах можно найти много интересного, например, что существенного изменилось в Exchange 2010 по сравнению со всеми предыдущими версиями и многое-многое другое.
У того, что мы написали есть существенный недостаток: наглядность. Мы заполнили кучу структур, которые никак не отобразили. Т.е. нужно написать GUI программу, которая в наглядном виде покажет содержимое всех этих структур. Написать ее должно быть не сложно, т.к. представлять она из себя будет набор простых гридов, благо структуры мы уже подготовили и заполнили, т.е., грубо говоря, осталось только распечатать. Пару примеров таких таблиц я приведу ниже.
Что нам это дало практически? С одной стороны, немногое, т.к. большая часть информации находится в бинарном виде JET_coltypBinary и описание этого форматы вы нигде не найдете. Здесь без реверсинженеринга работы Exchange уже не обойтись. Но с другой стороны мы можем лучше понять «А как оно работает на самом деле?», а это может оказаться бесценной информацией. Плюс это полезный материал для «старта», если придется чем-то подобным заниматься в будущем.
Где это может пригодиться? Это может пригодится только в тех случаях когда другое API не работает или же не удовлетворяемый каким-то требованиям, например, требованию производительности. Примерами таких задач могут быть: антивирусы, системы аудита, миграции и восстановления.
Напоследок два небольших примера полученных данных.
Кусочек списка таблиц в базе:
Список папок и их типов:
Спасибо, что прочитали и уделили ваше время!
P.S. Данный код является адаптированной и уменьшенной версией для поста. Поэтому в коде есть некоторые недоработки, а точнее затычки на местах урезанного функционала. Пожалуйста, не обращайте на них внимания это не production, а я хотел показать рабочие примеры. Код полностью рабочий и написан так, чтобы его можно было разместить в интернете и при этом не «съесть» все место на странице. Спасибо за понимание.
P.P.S. Я понимаю, что ввиду специфики данная информация вряд ли окажется полезной для широкого круга лиц, но если она поможет, даже одному человеку, я буду рад, и время, потраченное на этот пост, окупится.
Ссылки по теме
- Extensible Storage Engine на MSDN
- Статья на русском языке (чуть ли не единственная в рунете статья про использование ESE API)
- Примеры использования функций ESE