- PVSM.RU - https://www.pvsm.ru -
Некоторое время назад мне потребовалось сжимать данные прямо в памяти, причём не использовать для этого ничего стороннего — т.е. пользоваться встроенными в систему возможностями. Выбор пал на Cabinet.dll в качестве средства для сжатия данных и на интерфейс IStream для работы с данными в памяти. Ничего подобного в интернете я не нашёл, поэтому решил поделиться наработками.
Использовать сторонние решения не хотелось, ибо пришлось бы таскать с собой библиотеки или включать исходники в проект. Windows предоставляет не такой уж и большой набор средств для сжатия/распаковки данных: это Cabinet.dll, ZipFldr.dll (сжатые Zip-папки) и RtlCompressBuffer [1]/RtlDecompressBuffer [2]. Внятной документации по сжатым Zip-папкам я не нашёл, RtlCompressBuffer/RtlDecompressBuffer в версиях по Windows 7 включительно поддерживает только сжатие LZ, а вот Cabinet.dll присутствует в системе аж с Windows 95 [3] и до наших дней.
В качестве функций для работы с файлами и памятью документация предлагает использовать функции стандартной библиотеки C или функции Windows API, такие как CreateFile/CloseHandle/ReadFile/WriteFile. Так как все операции над файлами выполнялись в памяти, то для этих целей было решено использовать IStream [4].
Библиотека функционально делится на 2 части: FCI (file compression interface) и FDI (file decompression interface). Почитать об этом можно тут [5]. Оба интерфейса используют, по сути, одни и те же функции для работы с файлами и памятью, но Microsoft почему-то решила сделать разные прототипы для FCI и FDI. Впрочем, ничто не мешает описать одни через другие. Как это сделать, смотрите ниже.
Для использования библиотеки надо подключить файлы FCI.h и/или FDI.h соответственно и указать линкеру на Cabinet.lib. Все эти файлы входят в состав Windows SDK.
Простейший код, реализующий сжатие, выглядит так:
/*
Входные данные:
IStream* pIStreamFile — поток с данными файла, который надо добавить в архив
char* szFileName — имя файла в архиве. Если файлов несколько, то и имена их должны быть разными
*/
ERF erf;
CCAB ccab = {MAXINT, MAXINT};
*(IStream**)ccab.szCabPath = SHCreateMemStream(0, 0); //Поток для выходного файла
HFCI hFCI = FCICreate(&erf, fPlaced, fAlloc, fFree, fOpen, fRead, fWrite, fClose, fSeek, fDelete, fTemp, &ccab, 0);
if(hFCI){
FCIAddFile(hFCI, (PSZ)pIStreamFile, szFileName, 0, fGetNext, fStatus, fInfo, tcompTYPE_MSZIP);
FCIFlushFolder(hFCI, fGetNext, fStatus);
FCIFlushCabinet(hFCI, 0, fGetNext, fStatus);
FCIDestroy(hFCI);
}
/*
Выходные данные:
(IStream*)ccab.szCabPath — поток, содержащий cab-архив. Не забудьте сделать ему Release() по окончании использования!
*/
Т.е. сам код довольно прост. Вся соль заключается в функциях, передаваемых при создании контекста FCI и далее по ходу выполнения. Об их параметрах и возвращаемых значениях можно почитать здесь [6], поэтому далее будет указана только основная информация. Ниже приведён разбор каждой функции.
Тут следует добавить, что файловые дескрипторы у нас будут нестандартными в этом плане — это указатели на IStream. В силу этой особенности нужно быть аккуратным с передачей этого «дескриптора». Например, в структуре CCAB есть 2 поля: szCabPath и szCab, и казалось бы логичным передать адрес во 2-й параметр, но нет. FCI выполняет конкатенацию строк (вернее, он-то думает, что конкатенирует строки, но мы-то знаем…), поэтому в результате «именем» файла будет являться szCabPath, и он же будет являться дескриптором.
Вызывается каждый раз при добавлении нового файла в архив.
FNFCIFILEPLACED(fPlaced){
return 0;
}
Возврат -1 означает ошибку, остальные значения определяются приложением. Можно использовать для индикации добавления файлов, например.
Вызывается перед созданием нового тома архива.
FNFCIGETNEXTCABINET(fGetNext){
return 1;
}
В случае успеха возвращает TRUE, в противном случае — FALSE. Ничего примечательного.
Вызывается на нескольких этапах обработки файла: сжатие блока, добавление сжатого блока и запись архива.
FNFCISTATUS(fStatus){
return typeStatus == statusCabinet ? cb2 : 0;
}
В случае ошибки надо вернуть -1, в противном случае — любое значение (за исключением typeStatus == statusCabinet — тогда надо вернуть размер архива, который передаётся через параметр cb2).
Устанавливает атрибуты файла.
FNFCIGETOPENINFO(fInfo){
*pattribs = 0;
return (INT_PTR)pszName;
}
IStream не поддерживает атрибуты даты, да и вообще файловые атрибуты, поэтому значение по адресу pattribs надо установить в 0, иначе вы рискуете получить файлы в архиве со странными атрибутами (а то и не получить архив вовсе).
Возврат -1 означает ошибку, в противном случае надо вернуть дескриптор открытого файла.
Создание временного файла.
FNFCIGETTEMPFILE(fTemp){
*(IStream**)pszTempName = SHCreateMemStream(0, 0);
return 1;
}
В случае успеха возвращает TRUE, иначе — FALSE. Имя файла (указатель на IStream в данном случае) передаётся через параметр pszTempName.
Удаление файла.
FNFCIDELETE(fDelete){
(*(IStream**)pszFile)->Release();
return 0;
}
При успехе возвращает 0, при неудаче — -1. Удаление файла в данном случае — это освобождение занимаемых потоком ресурсов, поэтому просто делаем Release().
Выделение/освобождение памяти.
FNFCIALLOC(fAlloc){
return new char[cb];
}
FNFCIFREE(fFree){
delete memory;
}
Тут всё очень просто, поэтому я даже объединил эти функции в одном разделе.
Открытие файла (потока).
FNFCIOPEN(fOpen){
return *(INT_PTR*)pszFile;
}
Т.к. имя файла в нашем случае эквивалентно дескриптору этого файла, поэтому мы и возвращаем имя в качестве дескриптора (ну или -1, если вдруг произошла какая-то ошибка).
Закрытие дескриптора файла.
FNFCICLOSE(fClose){
LARGE_INTEGER li = {};
((IStream*)hf)->Seek(li, 0, 0);
return 0;
}
При успехе возвращает 0, при неудаче — -1. Почему не Release()? Потому что он «удаляет файл», т.е. уничтожает поток, в то время как нужно лишь его закрытие. Поэтому просто сбрасываем указатель на начало.
Чтение/запись данных из файла/в файл.
FNFCIREAD(fRead){
ULONG ul;
HRESULT hr = ((IStream*)hf)->Read(memory, cb, &ul);
return (hr && hr != S_FALSE) ? -1 : ul;
}
FNFCIWRITE(fWrite){
ULONG ul;
HRESULT hr = ((IStream*)hf)->Write(memory, cb, &ul);
return (hr && hr != S_FALSE) ? -1 : ul;
}
Возвращает количество прочтённых/записанных байт или -1 в случае ошибки (0 — достигнут конец файла).
Позиционирование указателя в файле.
FNFCISEEK(fSeek){
LARGE_INTEGER liDist = {dist};
HRESULT h r =((IStream*)hf)->Seek(liDist, seektype, (ULARGE_INTEGER*)&liDist);
return hr ? -1 : liDist.LowPart;
}
Возвращает -1 при ошибке, иначе — новую позицию указателя.
Код распаковки выглядит следующим образом:
/*
Входные данные:
IStream* pIStrCab — поток с архивом
*/
ERF erf;
HFDI hFDI = FDICreate(fAlloc, fFree, fnOpen, fnRead, fnWrite, fnClose, fnSeek, cpuUNKNOWN, &erf);
if(hFDI){
IStream *pIStrSrc = SHCreateMemStream(0, 0);
if(FDICopy(hFDI, (PSZ)&pIStrCab, (PSZ)&pIStrCab, 0, fnNotify, 0, &pIStrSrc)){
//Использование данных из потока pIStrSrc
}
pIStrSrc->Release();
FDIDestroy(hFDI);
}
pIStrCab->Release();
/*
Выходные данные:
IStream* pIStrSrc — поток с распакованными данными
*/
Здесь уже не всё так просто. Дело в том, что извлечение всех файлов из архива инициируется единственной функцией FDICopy, которая в процессе своей работы вызывает fnNotify, где и происходит вся магия. Но об этом — чуть позже.
В целом процесс аналогичен: создаём контекст FDI, поток для выходных данных, извлекаем файл из архива в этот поток (в моём примере надо было извлечь единственный файл) и уничтожаем контекст. (PSZ)&pIStrCab надо указать дважды, потому что в процессе своей работы функция конкатенирует оба параметра, и если опустить один из них, то будет ошибка (да, и на такие грабли я тоже натыкался).
Теперь немного о функциях. В целом они аналогичны функциям FCI, кроме того, что у них нет 2-х параметров; функции выделения/освобождения памяти вообще идентичны, поэтому повторно их описывать не имеет смысла. Для уменьшения количества кода можно переписать функции FCI через функции FDI, чтобы не указывать лишние нулевые параметры.
Открытие/закрытие файла (потока).
FNOPEN(fnOpen){
return *(INT_PTR*)pszFile;
}
FNCLOSE(fnClose){
return fClose(hf, 0, 0);
}
fnOpen проще продублировать, чем вызывать fOpen, а в fnClose вызывается функция FCI fClose с 2-мя нулевыми последними параметрами, ибо они не используются в этой реализации.
Чтение/запись данных и позиционирование указателя.
FNREAD(fnRead){
return fRead(hf, pv, cb, 0, 0);
}
FNWRITE(fnWrite){
return fWrite(hf, pv, cb, 0, 0);
}
FNSEEK(fnSeek){
return fSeek(hf, dist, seektype, 0, 0);
}
Возвращаемые значения аналогичны значениям для FCI.
Самая главная функция.
FNFDINOTIFY(fnNotify){
if(fdint == fdintCOPY_FILE)
if(!lstrcmp(pfdin->psz1, "Data")) //Если это тот файл, который надо извлечь
return (INT_PTR)*(int*)pfdin->pv;
return fdint == fdintCLOSE_FILE_INFO;
}
Всю информацию по функции можно прочитать тут [7]. Здесь же нужно несколько пояснений.
В большинстве случаев функция возвращает 0 как показатель успеха (кроме fdintCLOSE_FILE_INFO, тогда надо вернуть TRUE). При fdint == fdintCOPY_FILE поведение следующее: 0 означает пропуск файла, -1 — ошибка (завершение FDICopy), другое значение — дескриптор потока, в который надо извлечь данные.
Теперь начинается самое интересное, потому что если мы будем создавать потоки в этой функции, снаружи мы не получим к ним доступ. Поэтому есть минимум 2 пути решения, и оба они затрагивают доселе незадействованный и потому неприметный последний параметр pvUser функции FDICopy. Через него можно передавать пользовательские данные, и именно он возвращается в pfdin->pv. Первый путь — если у вас есть фиксированный список имён файлов, которые надо извлечь из архива, то его можно передать в виде массива структур, содержащих требуемое имя файла и указатель на IStream для извлечения в него. Второй путь — когда число файлов неизвестно, и вам надо извлечь их все; в таком случае через pvUser можно передать адрес контейнера (например, std::vector), в котором будут сохраняться имена и дескрипторы извлечённых файлов).
Этот способ подходит для случаев, когда результирующий размер данных у вас не особо большой — порядка сотни мегабайт. Разумеется, при наличии 8+ Гб памяти это не такие уж и большие затраты, но помните, что операция перевыделения памяти — не самая быстрая операция, которая к тому же ведёт к фрагментации памяти, вследствие чего может внезапно случиться такая оказия, что достаточно длинного непрерывного блока памяти у вас не будет.
В качестве некоторой альтернативы можно использовать structured storage [8] (там тот же самый IStream) или файловые потоки, созданные с помощью SHCreateStreamOnFile [9]/SHCreateStreamOnFileEx [10]. Таким образом, можно совместить операции ввода/вывода в памяти с аналогичными операциями в файлах, т.к. интерфейс IStream может использоваться в обоих случаях без каких-либо дополнительных манипуляций.
Если у вас есть какие-то вопросы по реализации, готов ответить на них в комментариях.
Автор: apixosoft
Источник [11]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/api/208024
Ссылки в тексте:
[1] RtlCompressBuffer: http://msdn.microsoft.com/en-us/library/windows/hardware/ff552127
[2] RtlDecompressBuffer: http://msdn.microsoft.com/en-us/library/windows/hardware/ff552191
[3] Windows 95: http://msdn.microsoft.com/en-us/library/ms974336
[4] IStream: http://msdn.microsoft.com/en-us/library/windows/desktop/aa380034
[5] тут: http://msdn.microsoft.com/en-us/library/bb417343
[6] здесь: http://msdn.microsoft.com/en-us/library/ff797922
[7] тут: http://msdn.microsoft.com/en-us/library/ff797944
[8] structured storage: http://msdn.microsoft.com/en-us/library/windows/desktop/aa380369
[9] SHCreateStreamOnFile: http://msdn.microsoft.com/en-us/library/windows/desktop/bb759864
[10] SHCreateStreamOnFileEx: http://msdn.microsoft.com/en-us/library/windows/desktop/bb759866
[11] Источник: https://habrahabr.ru/post/314832/?utm_source=habrahabr&utm_medium=rss&utm_campaign=sandbox
Нажмите здесь для печати.