分类:
2008-01-31 11:53:47
This series of articles discusses three typical requirements when working with file resources (by that I mean, files that have been compiled into your program as resources):
Part 1 | Text and binary files |
Zip files | |
Encrypted files |
One of the most frequent questions I see about resources is, 'How do I update a resource while the EXE is still running?' Short answer: you can't. Theoretically, you could write a second program that would update a resource of the first program when it shuts down, but this is a Really Bad Idea. It is very common for programs to be installed in directories that are set to read-only for ordinary users, and with Microsoft tightening security, it is unlikely that any would work for long.
So, no, the classes that I am presenting in these articles do not offer write access to a program's resources.
For this article's demo program, I have set up resources to include seven files - six text and one binary. Here are actual lines taken from XResFileTest.rc:
/////////////////////////////////////////////////////////////////////////////
//
// TEXT
//
IDU_FILE1 TEXT DISCARDABLE "test1.txt"
IDU_FILE2 TEXT DISCARDABLE "test2.txt"
IDU_FILE3 TEXT DISCARDABLE "test3.txt"
IDU_FILE4 TEXT DISCARDABLE "test4.txt"
IDU_FILE5 TEXT DISCARDABLE "test5.txt"
IDU_FILE6 TEXT DISCARDABLE "test6.txt"
/////////////////////////////////////////////////////////////////////////////
//
// BINARY
//
IDU_FILE7 BINARY DISCARDABLE "test7.bin"
There is nothing special about these lines at all - you will see similar lines for icons, bitmaps, etc., in your own *.rc files. There are a few things worth noting:
IDU_FILE1
, IDU_FILE2
, through IDU_FILE7
. This is clear enough. However, what isn't widely known is that resource ID can be a string. In other words, instead of defining IDU_FILE1
, etc., in resource.h, you can simply not define them. This has the effect of forcing Resource Compiler to treat them as strings. In the demo app, I will show you how to deal with resources both ways - using integer resource IDs, and using string resource IDs.
TEXT
and BINARY
that you see in the above snippet. In fact, you can name resources anything you want - BOFFORESOURCE
would be perfectly acceptable. But, you should be consistent, and (hopefully) the name you use should be indicative of what the resource is. The effect of including seven files as resources is what you would expect - the size of the EXE gets larger. This is the first thing to pay attention to - there is no kind of compression applied to these files. You can see this clearly with a hex editor such as excellent free :
If you are worried about people stealing your embedded resources, then I can set your mind at ease right now - you can stop worrying, because there is no doubt that it will happen. The situation is really much worse than simple hex editors. Take a look at freeware :
With this utility, anyone can inspect, extract, and copy your embedded resources. In Parts 2 and 3, I will discuss ways to defeat utilities such as hex editors and resource extractors, but for the remainder of this article, I will talk about how to access and read the files that are embedded in the demo app.
To give you an idea of what CResourceFile
and CResourceTextFile
classes are capable of, let me show you some screenshots of the demo app. The first one shows ANSI text being displayed from files test1.txt, test2.txt, and test3.txt:
This next one shows Unicode text being displayed from files test4.txt, test5.txt, and test6.txt. Notice that test5.txt has a :
The last one shows a binary file being displayed in hex from file test7.bin:
Each of the classes I will present in this and later articles is based on CResourceFile
. This class interfaces directly with resources via holy trinity of Win32 resource APIs: FindResource()
, LoadResource()
, and LockResource()
. The result of calling these three in succession is a pointer to a byte buffer. You can find out the size of the resource (in bytes) by calling SizeofResource()
. With a pointer to buffer and its size, you can copy resource to your own buffer, for later use. This is essentially all there is to CResourceFile::Open()
:
///////////////////////////////////////////////////////////////////////////////
//
// Open()
//
// Purpose: Open a file resource
//
// Parameters: hInstance - instance handle for the resource. A value of
// NULL specifies the exe (default).
// lpszResId - resource name or id
// (passed via MAKEINTRESOURCE)
// lpszResType - resource type string to look for
//
// Returns: BOOL - returns TRUE if resource opened ok,
// otherwise FALSE
//
BOOL CResourceFile::Open(HINSTANCE hInstance,
LPCTSTR lpszResId,
LPCTSTR lpszResType)
{
BOOL rc = FALSE;
Close();
_ASSERTE(lpszResId);
_ASSERTE(lpszResType);
if (lpszResId && lpszResType)
{
TCHAR *pszRes = NULL;
// is this a resource name string or an id?
if (HIWORD(lpszResId) == 0)
{
// id
pszRes = MAKEINTRESOURCE(LOWORD((UINT)(UINT_PTR)lpszResId));
}
else
{
// string
pszRes = (TCHAR *)lpszResId;
TRACE(_T("pszRes=%s\n"), pszRes);
}
HRSRC hrsrc = FindResource(hInstance, pszRes, lpszResType);
_ASSERTE(hrsrc);
if (hrsrc)
{
DWORD dwSize = SizeofResource(hInstance, hrsrc); // in bytes
TRACE(_T("dwSize=%d\n"), dwSize);
HGLOBAL hglob = LoadResource(hInstance, hrsrc);
_ASSERTE(hglob);
if (hglob)
{
LPVOID lplock = LockResource(hglob);
_ASSERTE(lplock);
if (lplock)
{
// save resource as byte buffer
m_pBytes = new BYTE [dwSize+16];
memset(m_pBytes, 0, dwSize+16);
m_nBufLen = (int) dwSize;
memcpy(m_pBytes, lplock, m_nBufLen);
m_nPosition = 0;
m_bIsOpen = TRUE;
m_bDoNotDeleteBuffer = FALSE; // ok to delete the buffer
rc = TRUE;
}
}
}
}
return rc;
}
The buffer that is allocated in Open()
is deleted in ~CResourceFile()
.
CResourceFile
makes it easy to deal with binary resources. Here is the code that opens and displays test7.bin in the demo app:
void CXResFileTestDlg::OnTestBinary()
{
m_List.ResetContent();
CFont *pFont = m_List.GetFont();
LOGFONT lf = { 0 };
pFont->GetLogFont(&lf);
if (m_strFaceName.IsEmpty())
{
m_strFaceName = lf.lfFaceName;
// change to Courier so everything will line up
_tcscpy(lf.lfFaceName, _T("Courier New"));
m_font.DeleteObject();
m_font.CreateFontIndirect(&lf);
}
m_List.SetFont(&m_font, TRUE);
BYTE alphabet[52] = { 0 };
BYTE b = 0x61;
for (int i = 0; i < 52; i++)
alphabet[i++] = b++;
// test5.bin is actually UTF-16
m_List.AddLine(CXListBox::Blue, CXListBox::White, _T
("=== Checking BINARY file test7.bin ==="));
m_List.AddLine(CXListBox::Blue, CXListBox::White, _T(""));
TestBinaryResource(_T("IDU_FILE7"), alphabet, 52);
}
And here is the function TestBinaryResource()
that does the work:
void CXResFileTestDlg::TestBinaryResource(LPCTSTR lpszResource,
BYTE * values, int nResSize)
{
CResourceFile rf;
UINT nResId = 0;
CString strRes = _T("");
if (HIWORD(lpszResource) == 0)
{
nResId = (UINT)(UINT_PTR)lpszResource;
strRes.Format(_T("%u"), nResId);
}
else
{
strRes = lpszResource;
}
if (rf.Open(NULL, lpszResource, _T("BINARY")))
{
int nSize = rf.Read(NULL, 0);
if (nSize == nResSize)
{
m_List.Printf(CXListBox::Green, CXListBox::White, 0,
_T("Binary resource '%s' size ok, %d bytes"), strRes, nSize);
BYTE * buf = rf.GetByteBuffer();
BOOL bContentsOk = TRUE;
for (int i = 0; i < nResSize; i++)
{
if (values[i] != buf[i])
{
bContentsOk = FALSE;
break;
}
}
if (bContentsOk)
{
m_List.Printf(CXListBox::Green, CXListBox::White, 0,
_T("Binary resource '%s' contents ok"), strRes);
}
else
{
m_List.Printf(CXListBox::Red, CXListBox::White, 0,
_T("Binary resource '%s' contents incorrect"), strRes);
}
m_List.AddLine(CXListBox::Blue, CXListBox::White, _T(""));
m_List.AddLine(CXListBox::Blue,
CXListBox::White, _T("Actual contents:"));
DisplayHex(buf, rf.GetLength(), _T(""));
m_List.AddLine(CXListBox::Blue, CXListBox::White, _T(""));
m_List.AddLine(CXListBox::Blue,
CXListBox::White, _T("Contents should be:"));
DisplayHex(values, rf.GetLength(), _T(""));
}
else
{
m_List.Printf(CXListBox::Red, CXListBox::White, 0,
_T("Binary resource '%s' size = %d, should be %d"),
strRes, nSize, nResSize);
}
rf.Close();
}
else
{
m_List.Printf(CXListBox::Red, CXListBox::White, 0,
_T("Failed to open resource '%'s"), strRes);
}
}
The highlighted lines show how the resource file is opened and its size retrieved, by calling Read()
with a NULL
buffer pointer. (Tip: GetLength()
will do the same thing.) To do something with the buffer, you call GetByteBuffer()
, which returns a pointer to CResourceFile
internal byte buffer.
The code presented above can be boiled down to:
CResourceFile rf;
if (rf.Open(NULL, _T("IDU_FILE7"), _T("BINARY"))) // open resource
{
int len = rf.GetLength(); // get length of byte buffer
BYTE *p = rf.GetByteBuffer(); // get pointer to internal buffer
if (p)
{
--- do something ---
}
.
.
.
// rf goes out of scope and resource file is closed
}
The above example shows how to open test7.bin using string resource ID - since IDU_FILE7
is not defined in resource.h, it is treated as a string by the Resource Compiler, and to use it you must enclose it in quotes as above.
What if IDU_FILE7
was defined in resource.h? In that case, the code above would become:
CResourceFile rf;
// open resource
if (rf.Open(NULL, MAKEINTRESOURCE(IDU_FILE7), _T("BINARY")))
{
--- rest of code is unchanged ---
The Win32 macro converts the int value IDU_FILE7
into a LPTSTR
value suitable for passing as a string pointer. When CResourceFile::Open()
sees this value, it can recognize it because resource ID is in low-order word, and there is zero in high-order word.
It is interesting to note difference in resource IDs, as seen by resource extractors:
The file test2.txt is associated with an integer resource ID, defined in resource.h:
#define IDU_FILE2 130
This is why resource ID is displayed in Resource Hacker as 130. By itself, using integer resource IDs will not prevent your embedded resources from being ripped, but it is one step you can take to reduce transparency of your app's inner workings - seeing IDU_PASSWORD_FILE
, for example, is much more helpful to a hacker than seeing the value 130.
One last point about CResourceFile::Open()
: in the demo app, you will see this line of code:
rf.Open(NULL, lpszResource, _T("BINARY"))
The first parameter is NULL
, which has a special meaning to FindResource()
- according to MSDN,
What this means is that you can use NULL
if the resource is in your EXE. But if the resource is in a DLL, you need to use the DLL's instance handle, which you can get when you call LoadLibrary()
:
hInstanceDll = LoadLibrary("MyDll.dll");
Here is complete list of functions available in CResourceFile
:
// Close() Close a file resource
// DetachByteBuffer() Return pointer to byte buffer and set flag so
// buffer will not be deleted when resource is closed
// DuplicateByteBuffer() Duplicate the buffer associated with the binary
// file resource
// GetByteBuffer() Get pointer to binary file resource buffer
// GetLength() Get length of file resource
// GetPosition() Get current file position of file resource
// IsAtEOF() Check if file pointer is at end of file
// IsOpen() Check if file resource is open
// Open() Open a file resource
// Read() Read bytes from the binary file resource
// Seek() Move pointer to specified position in resource
// buffer
// SeekToBegin() Position pointer to beginning of resource buffer
// SeekToEnd() Position pointer to end of resource buffer
// SetByteBuffer() Set a new buffer for the binary file resource
To integrate CResourceFile
class into your app, do the following:
CResourceFile
.
CResourceFile
. See above for sample code. The class for reading resources as text files is CResourceTextFile
, which is derived from CResourceFile
. Why a special class for text files? The main reason is Unicode/ANSI conversion, including dealing with markers. Another reason: the ability to read text resource file line-by-line. You have already seen how this works in the demo app screenshots above. When you compile VS6 project, the resulting ANSI EXE will convert Unicode text resource files to ANSI; and when you compile VS2005 project, the resulting Unicode EXE will convert ANSI text resource files to Unicode.
The primary interface to access text resource files is CResourceTextFile::Open()
. Its first three parameters are the same as the base class CResourceFile::Open()
. It has an additional two parameters, to specify ANSI/Unicode conversion action, and how to deal with markers:
///////////////////////////////////////////////////////////////////////////////
//
// Open()
//
// Purpose: Open a file resource
//
// Parameters: hInstance - instance handle for the resource. A value of
// NULL specifies the exe (default).
// lpszResId - resource name or id
// (passed via MAKEINTRESOURCE)
// lpszResType - resource type string to look for
// eConvertAction - convert action to take
// eBomAction - action to take with BOM
//
// Returns: BOOL - returns TRUE if resource opened ok,
// otherwise FALSE
//
BOOL CResourceTextFile::Open(HINSTANCE hInstance,
LPCTSTR lpszResId,
LPCTSTR lpszResType /*= _T("TEXT")*/,
ConvertAction eConvertAction /*= NoConvertAction*/,
BomAction eBomAction /*= NoBomAction*/)
The types for the two additional parameters are defined as:
enum ConvertAction
{
NoConvertAction = 0, ConvertToUnicode, ConvertToAnsi
};
enum BomAction
{
NoBomAction = 0, RemoveBom, AddBom
};
These two parameters allow you to control how text files are converted and read. Note that you have total control over this - if you do not specify any conversion action, then none will be taken. This includes processing of the markers - even if you specify a BomAction
, no ANSI/Unicode conversion will automatically be done.
The sample app shows how to handle different conversion scenarios. Here is the code that displays ANSI text file resources:
CResourceTextFile::ConvertAction eConvertAction =
CResourceTextFile::NoConvertAction;
#ifdef _UNICODE
eConvertAction = CResourceTextFile::ConvertToUnicode;
#endif
// test1 - 3 are ANSI
m_List.AddLine(CXListBox::Blue, CXListBox::White,
_T("=== Checking ANSI TEXT file test1.txt ==="));
TestTextResource(_T("IDU_FILE1"), 1, 15, eConvertAction,
CResourceTextFile::NoBomAction);
m_List.AddLine(CXListBox::Blue, CXListBox::White, _T(""));
m_List.AddLine(CXListBox::Blue, CXListBox::White,
_T("=== Checking ANSI TEXT file test2.txt ==="));
TestTextResource(MAKEINTRESOURCE(IDU_FILE2), 2, 15,
eConvertAction, CResourceTextFile::NoBomAction);
m_List.AddLine(CXListBox::Blue, CXListBox::White, _T(""));
m_List.AddLine(CXListBox::Blue, CXListBox::White,
_T("=== Checking ANSI TEXT file test3.txt ==="));
TestTextResource(_T("IDU_FILE3"), 3, 15, eConvertAction,
CResourceTextFile::NoBomAction);
And here is the code that displays Unicode text file resources:
CResourceTextFile::ConvertAction eConvertAction =
CResourceTextFile::NoConvertAction;
#ifndef _UNICODE
eConvertAction = CResourceTextFile::ConvertToAnsi;
#endif
// test4.txt is UTF-16 (no BOM)
m_List.AddLine(CXListBox::Blue, CXListBox::White,
_T("=== Checking UNICODE TEXT file test4.txt ==="));
TestTextResource(_T("IDU_FILE4"), 1, 15, eConvertAction,
CResourceTextFile::NoBomAction);
// test5.txt is UTF-16 (with BOM)
m_List.AddLine(CXListBox::Blue, CXListBox::White, _T(""));
m_List.AddLine(CXListBox::Blue, CXListBox::White,
_T("=== Checking UNICODE TEXT WITH BOM file test5.txt ==="));
TestTextResource(_T("IDU_FILE5"), 2, 15, eConvertAction,
CResourceTextFile::RemoveBom);
// test6.txt is UTF-16 (no BOM)
m_List.AddLine(CXListBox::Blue, CXListBox::White, _T(""));
m_List.AddLine(CXListBox::Blue, CXListBox::White,
_T("=== Checking UNICODE TEXT file test6.txt ==="));
TestTextResource(_T("IDU_FILE6"), 3, 15, eConvertAction,
CResourceTextFile::NoBomAction);
By knowing the code set of your EXE, and format of text file resource, you can set up your app to read both ANSI and Unicode text file resources.
Here is a stripped-down version of code presented in the demo app for reading text files:
CResourceTextFile rf;
if (rf.Open(NULL, lpszResource, _T("TEXT"), eConvertAction, eBomAction))
{
while (!rf.IsAtEOF())
{
TCHAR s[100] = { _T('\0') };
int nLen = rf.ReadLine(s, sizeof(s)/sizeof(TCHAR)-1);
if (nLen > 0)
{
--- do something ---
}
else
{
--- line was empty ---
}
}
rf.Close(); // optional - will close when rf goes out of scope
}
Here is a complete list of functions implemented in CResourceTextFile
:
// Close() Close a file resource
// DetachTextBuffer() Return pointer to text buffer and
// set flag so buffer
// will not be deleted when resource is closed
// DuplicateTextBuffer() Duplicate the buffer associated with the text file
// resource
// GetBomAction() Get action to take for BOM
// GetConvertAction() Get convert action
// GetTextBuffer() Get pointer to text file resource buffer
// Open() Open a file resource
// ReadLine() Read a line of text from the text file resource
// SetTextBuffer() Set a new buffer for the text file resource
To integrate CResourceTextFile
class into your app, do the following:
CResourceTextFile
.
CResourceTextFile
. See above for sample code. CResourceFile
and CResourceTextFile
have been implemented using C++, without any MFC or STL. They have been compiled under both VS6 and VS2005, and have been tested on XP and Vista. They should also work on Win2000 and Win98, but have not been tested on those platforms.
I have presented two classes that take care of mundane aspects of accessing text and binary files that have been included as resources in your app. In the next article, I will discuss the possibility of including zip file as resource.