2014. 3. 10. 16:08

.cab 파일 압축 해제 코드

파일의 압축/해제를 구현할때 가장 먼저 선택해야 하는 것이 압축의 종류입니다. 보통 .zip, .bzip 혹은 .7z 등이 사용되기도 합니다. 이들 모두 어느정도 license에서 자유로운 장점이 있지만, 더 중요한 건 "많은 사람들이 사용한다"라는 안정성과 대략 높은 압축률, 그리고 성능을 보여주기 때문입니다.


Windows 에서는 .cab이라는 형식의 압축을 매우 오래전부터 사용하고 있는데도 불구하고 윈도우 개발자들은 이를 지나쳐가기도 합니다. 우선, 웬만한 Windows 시스템에는 기본적으로 cabinet.dll이 모두 포함되어 있습니다. 그리고, 압축률도 준수하며, 더군다나 압축 파일에 디지털 서명도 할 수 있습니다.


Windows에서는 Cabinet(즉, .cab)에 대한 SDK가 있었지만, 현재는 Windows Platform SDK등을 설치해야지만 사용할 수 있습니다. fdi.h, fci.h 두개의 파일이 필요합니다. 만일, 해당 파일을 공수했다면, 굳이 Platform SDK를 설치할 필요는 없어 보이네요(확인되지 않음).


.cab 파일 압축 해제를 위해, Cabinet.h와 Cabinet.cpp 두개의 파일만 있으면 됩니다.

그리고 해당 class에는 아래의 단 하나의 함수만 노출되어 있습니다.

static DWORD ExtractCab

IN LPCTSTR lpszCabPath

IN OPTIONAL LPCTSTR lpszDestPath

IN pfnCbExtractCabBeforeCopy pfnCbBeforeCopy

IN pfnCbExtractCabAfterCopy pfnCbAfterCopy

IN LPVOID pContext

즉, 주어진 .cab 파일의 경로와 압축을 풀 경로를 전달합니다. 압축을 풀 경로는 NULL 전달 가능합니다(압축 파일 경로 전부 %temp%와 같이 환경변수로 이뤄져 있는 경우). 그리고 개별 파일의 압축을 풀기 전, 그리고 후의 Callback 함수를 전달합니다. Callback 함수에 함께 전달될 Context도 포함합니다. 마지막으로 리턴값은 Win32 Error Code입니다.

  • lpszCabPath : .cab 파일의 Full 경로
  • lpszDestPath : 압축을 풀 경로. NULL 가능. 만일 그때 내부 경로가 %로 시작하지 않는 절대 경로인 경우, 해당 항목의 압축 해제는 실패하게 된다.
  • pfnCbBeforeCopy : 단일 항목 파일을 압축 해제 직전 호출하는 Callback 함수 등록
  • pfnCbAfterCopy : 단일 항목 파일 압축 해제 후 호출하는 Callback 함수 등록
  • pContext : Callback 함수에 전달될 Context

그럼, callback 함수의 prototype은 다음과 같습니다.

BOOL __cdecl* CbExtractCabBeforeCopy

IN LPCTSTR lpszArchivePath

IN LPCTSTR lpszDestPath

IN LPCTSTR lpszCompressedPath

OUT PBOOL pbSkipThis

IN LPVOID pContext

단일 항목 파일 압축 해제 직전에 호출
  • lpszArchivePath : 압축하는 파일 경로
  • lpszDestPath : 압축이 풀릴 Output 경로
  • lpszCompressedPath : 압축 파일의 내부 경로
  • pbSkipThis : *pbSkipThis=TRUE로 하면 해당 파일을 건너뛴다.
  • pContext : ExtractCab의 pContext 값
  • 리턴 : TRUE (계속) / 이외 (중단)

BOOL __cdecl* CbExtractCabAfterCopy

IN LPCTSTR lpszDestPath

IN DWORD dwErrorCode

IN LPVOID pContext

단일 항목 파일 압축 해제 직후에 호출
  • lpszDestPath : 압축이 풀린 Output 경로
  • dwErrorCode : Win32 Error Code. ERROR_SUCCESS면 성공
  • pContext : ExtractCab의 pContext 값

예제 & Source Code


Cabinet.cpp


Cabinet.h


[주의 : 위 코드의 License는 by-nc-nd로, 상업적 용도는 불허합니다. 단, 관련 필요시, 해당 프로젝트의 성격과 정보를 알려주시면(comment) by-nd와 같이 상업적 용도 가능토록 사용하실 수 있으니, 참고 바랍니다.)


CabinetTool.zip


아래는 CabinetTool.zip의 예제 코드 입니다.

#include "stdafx.h"
#include "Cabinet.h"
#include >strsafe.h>

class CCallback
{
public:
	CCallback() {}
	~CCallback() {}

public:
	typedef struct tagST_CONTEXT
	{
		BOOL bContinue;
		BOOL bSkipThis;
	} ST_CONTEXT, *LPST_CONTEXT;

public:
	static BOOL __cdecl CbExtractCabBeforeCopy(IN LPCTSTR lpszArchivePath, IN LPCTSTR lpszDestPath, IN LPCTSTR lpszCompressedPath, OUT PBOOL pbSkipThis, IN LPVOID pContext);
	static BOOL __cdecl CbExtractCabAfterCopy(IN LPCTSTR lpszDestPath, IN DWORD dwErrorCode, IN LPVOID pContext);
};

BOOL CCallback::CbExtractCabBeforeCopy(IN LPCTSTR lpszArchivePath, IN LPCTSTR lpszDestPath, IN LPCTSTR lpszCompressedPath, OUT PBOOL pbSkipThis, IN LPVOID pContext)
{
	_tprintf(TEXT("[BEFORE]\r\n\tarchive : %s\r\n\tdest : %s\r\n\tcompressed : %s\r\n"), lpszArchivePath, lpszDestPath, lpszCompressedPath);
	*pbSkipThis = ((LPST_CONTEXT)pContext)->bSkipThis;
	return ((LPST_CONTEXT)pContext)->bContinue;
}

BOOL CCallback::CbExtractCabAfterCopy(IN LPCTSTR lpszDestPath, IN DWORD dwErrorCode, IN LPVOID pContext)
{
	_tprintf(TEXT("[AFTER]\r\n\tdest : %s\r\n\terr : %d\r\n"), lpszDestPath, dwErrorCode);
	return ((LPST_CONTEXT)pContext)->bContinue;
}

int _tmain(int argc, _TCHAR* argv[])
{
	WCHAR			szPath[MAX_PATH]	= {0,};
	PWCHAR			lpszFind		= NULL;
	DWORD			dwRtnValue		= 0;
	CCallback::ST_CONTEXT	stContext		= {0,};
	CString			strDest;

	//////////////////////////////////////////////////////////////////////////
	// test.cab은 .exe 경로에 있다.
	::GetModuleFileNameW(::GetModuleHandle(NULL), szPath, MAX_PATH);
	lpszFind = wcsrchr(szPath, L'\\');
	if (NULL == lpszFind)
	{
		goto FINAL;
	}
	lpszFind[0] = '\0';
	StringCchCatW(szPath, MAX_PATH, L"\\test.cab");

	//////////////////////////////////////////////////////////////////////////
	// Callback context를 설정한다.
	stContext.bContinue = TRUE;
	stContext.bSkipThis = FALSE;

	//////////////////////////////////////////////////////////////////////////
	// 압축을 풀 경로는 %temp%\cabtest로 한다.
	if (FALSE == CCabinet::ExpandEnvironmentForCabinet(L"%temp%\\cabtest", strDest))
	{
		goto FINAL;
	}

	//////////////////////////////////////////////////////////////////////////
	// Cab 압축을 해제한다.
	_tprintf(TEXT("Extract : %s -> %s\r\n"), szPath, strDest);
	_tprintf(TEXT("--------------------------------------------------------------\r\n"));
	dwRtnValue = CCabinet::ExtractCab(szPath, 
					  strDest, 
					  CCallback::CbExtractCabBeforeCopy, 
					  CCallback::CbExtractCabAfterCopy, 
									  &stContext);
	_tprintf(L"Extrace Cab Finished [%d]\r\n", dwRtnValue);
	_tprintf(TEXT("--------------------------------------------------------------\r\n"));

	//////////////////////////////////////////////////////////////////////////
	// Cab 압축을 해제한다. (continue 하지 않는다)
	stContext.bContinue = FALSE;
	stContext.bSkipThis = FALSE;
	_tprintf(TEXT("\r\n\r\nStop by Callback\r\n"));
	_tprintf(TEXT("--------------------------------------------------------------\r\n"));
	dwRtnValue = CCabinet::ExtractCab(szPath, 
					  strDest, 
					  CCallback::CbExtractCabBeforeCopy, 
					  CCallback::CbExtractCabAfterCopy, 
					  &stContext);
	_tprintf(L"Extrace Cab Finished [%d]\r\n", dwRtnValue);
	_tprintf(TEXT("--------------------------------------------------------------\r\n"));

	//////////////////////////////////////////////////////////////////////////
	// Cab 압축을 해제한다. (continue 하지 않는다)
	stContext.bContinue = TRUE;
	stContext.bSkipThis = TRUE;
	_tprintf(TEXT("\r\n\r\nSkip file by Callback\r\n"));
	_tprintf(TEXT("--------------------------------------------------------------\r\n"));
	dwRtnValue = CCabinet::ExtractCab(szPath, 
					  strDest, 
					  CCallback::CbExtractCabBeforeCopy, 
					  CCallback::CbExtractCabAfterCopy, 
					  &stContext);
	_tprintf(L"Extrace Cab Finished [%d]\r\n", dwRtnValue);
	_tprintf(TEXT("--------------------------------------------------------------\r\n"));

FINAL:
	return 0;
}

즉, CabinetTool.exe는 .exe 경로에 있는 test.cab의 압축을 해제하도록 합니다.

Sample인 test.cab은 다음과 같이 되어 있습니다.

이를 내부 파일로 도식화 하면 다음과 같습니다.

├─%temp%

│  └─_cab_temp_

│          delete_me.txt

├─001

│      hello.txt

│      img7.jpg

└─002

        win.ini


즉, 특별한건 %temp%인데, 이것이 실제 압축이 풀리면, 환경 변수값으로 치환되어 압축이 풀리도록 하였습니다. 즉, ExtractCab 함수의 Destination을 C:\Test로 해도 %temp% 하부의 파일인 delete_me.txt는 C:\Test\%temp%\_cab_temp_\delete_me.txt가 아닌 %temp%\_cab_temp_\delete_me.txt로 저장합니다. 물론, %temp%는 일반적으로 C:\Users\greenfish\AppData\Local\Temp와 같은 형태겠지요.

그리고 hello.txt는 C:\Test\001\hello.txt로 전환됩니다.


CabinetTool\Release\CabinetTool.exe를 실행하면 다음과 같습니다.

Microsoft Windows XP [Version 5.1.2600]

(C) Copyright 1985-2001 Microsoft Corp.


C:\Documents and Settings\Administrator>cd C:\CabinetTool\Release


C:\CabinetTool\Release>CabinetTool.exe

Extract : C:\CabinetTool\Release\test.cab -> C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\

cabtest

--------------------------------------------------------------

[BEFORE]

        archive : C:\CabinetTool\Release\test.cab

        dest : C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\cabtest\001\hello.txt

        compressed : 001\hello.txt

[AFTER]

        dest : C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\cabtest\001\hello.txt

        err : 0

[BEFORE]

        archive : C:\CabinetTool\Release\test.cab

        dest : C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\cabtest\001\img7.jpg

        compressed : 001\img7.jpg

[AFTER]

        dest : C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\cabtest\001\img7.jpg

        err : 0

[BEFORE]

        archive : C:\CabinetTool\Release\test.cab

        dest : C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\cabtest\002\win.ini

        compressed : 002\win.ini

[AFTER]

        dest : C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\cabtest\002\win.ini

        err : 0

[BEFORE]

        archive : C:\CabinetTool\Release\test.cab

        dest : C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\_cab_temp_\delete_me.txt

        compressed : %temp%\_cab_temp_\delete_me.txt

[AFTER]

        dest : C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\_cab_temp_\delete_me.txt

        err : 0

Extrace Cab Finished [0]

--------------------------------------------------------------



Stop by Callback

--------------------------------------------------------------

[BEFORE]

        archive : C:\CabinetTool\Release\test.cab

        dest : C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\cabtest\001\hello.txt

        compressed : 001\hello.txt

[INFO] Callback Canceled

[ERROR] Fail to FDICopy, 995, 11, 0, 1

Extrace Cab Finished [995]

--------------------------------------------------------------



Skip file by Callback

--------------------------------------------------------------

[BEFORE]

        archive : C:\CabinetTool\Release\test.cab

        dest : C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\cabtest\001\hello.txt

        compressed : 001\hello.txt

[INFO] Skip File, C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\cabtest\001\hello.txt

[BEFORE]

        archive : C:\CabinetTool\Release\test.cab

        dest : C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\cabtest\001\img7.jpg

        compressed : 001\img7.jpg

[INFO] Skip File, C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\cabtest\001\img7.jpg

[BEFORE]

        archive : C:\CabinetTool\Release\test.cab

        dest : C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\cabtest\002\win.ini

        compressed : 002\win.ini

[INFO] Skip File, C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\cabtest\002\win.ini

[BEFORE]

        archive : C:\CabinetTool\Release\test.cab

        dest : C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\_cab_temp_\delete_me.txt

        compressed : %temp%\_cab_temp_\delete_me.txt

[INFO] Skip File, C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\_cab_temp_\delete_me.txt

Extrace Cab Finished [0]

--------------------------------------------------------------


C:\CabinetTool\Release>

Extract : C:\CabinetTool\Release\test.cab -> C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\

cabtest

와 같이 test.cab을 %temp%\cabtest, 즉 C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\

cabtest로 압축을 푼다고 표시를 해주고 있습니다.


[BEFORE]

        archive : C:\CabinetTool\Release\test.cab

        dest : C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\cabtest\001\hello.txt

        compressed : 001\hello.txt

[AFTER]

        dest : C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\cabtest\001\hello.txt

        err : 0


와 같이 test.cab에 hello.txt를 C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\cabtest\001\hello.txt로 압축을 풀기전에 호출을 해 줬습니다. 그리고 복사뒤, C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\cabtest\001\hello.txt 파일이 안정적으로 복사가 성공(0=ERROR_SUCCESS)하였음이 확인되었습니다.


[BEFORE]

        archive : C:\CabinetTool\Release\test.cab

        dest : C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\_cab_temp_\delete_me.txt

        compressed : %temp%\_cab_temp_\delete_me.txt

[AFTER]

        dest : C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\_cab_temp_\delete_me.txt

        err : 0


를 보면, .cab에 %temp%\_cab_temp_\delete_me.txt를 %temp%의 환경 변수를 확장하여, C:\DOCUME~1\ADMINI~1\LOCALS~1\Temp\_cab_temp_\delete_me.txt로 저장함이 확인됩니다.


여기에서 의문이, .cab으로 압축을 할 때 %temp%와 같은 이름을 지정할 수 있냐는 문제인데,

http://www.codeproject.com/Articles/15397/Cabinet-File-CAB-Compression-and-Extraction

를 참고하시기 바랍니다. 일반적은 MakeCab이나 CabArc등으로는 잘 되지 않습니다.


Stop by Callback에서, Callback 함수 리턴값으로 FALSE를 선택한다면, 압축 해제 Process는 완전 중단됩니다. 리턴값은 995(=ERROR_OPERATION_ABORTED)가 전달됩니다.


Skip file by Callback는, Before callback 함수에서 SkipThis를 True로 전달하는 경우인데, 압축을 푸는 과정에서 몇몇 파일들은 압축 해제를 Skip할 필요가 있을 것인데, 이때 사용하면 됩니다. 혹은, 압축을 풀기 전, 압축 파일 내부의 파일들의 List를 하는데도 도움을 줍니다.