Introduction
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 |
Part 2 | Zip files |
Part 3 | Encrypted files |
Please Read This
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 kludge 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.
Embedding a File as a Resource
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:
- Column 1 contains resource ID -
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. - Column 2 contains resource type - MSDN lumps all non-standard resource types into what it calls User Defined Resources. This includes types such as
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.
So at last we get to the question, What is resource type used for? Something important, it turns out. The two pieces of information that Win32 API FindResource() needs in order to locate resource are the resource ID and the resource type. - Column 3 contains resource attribute. This is an anachronism that I have gotten in the habit of using. According to this article on MSDN, it is ignored.
- Column 4 contains the name of the file that you want to include. Nothing new here.
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 XVI32 Hex Editor:
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 Resource Hacker:
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.
Working with Text and Binary Files
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 BOM:
The last one shows a binary file being displayed in hex from file test7.bin:
Reading Binary Files
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()
:
BOOL CResourceFile::Open(HINSTANCE hInstance,
LPCTSTR lpszResId,
LPCTSTR lpszResType)
{
BOOL rc = FALSE;
Close();
_ASSERTE(lpszResId);
_ASSERTE(lpszResType);
if (lpszResId && lpszResType)
{
TCHAR *pszRes = NULL;
if (HIWORD(lpszResId) == 0)
{
pszRes = MAKEINTRESOURCE(LOWORD((UINT)(UINT_PTR)lpszResId));
}
else
{
pszRes = (TCHAR *)lpszResId;
TRACE(_T("pszRes=%s\n"), pszRes);
}
HRSRC hrsrc = FindResource(hInstance, pszRes, lpszResType);
_ASSERTE(hrsrc);
if (hrsrc)
{
DWORD dwSize = SizeofResource(hInstance, hrsrc); TRACE(_T("dwSize=%d\n"), dwSize);
HGLOBAL hglob = LoadResource(hInstance, hrsrc);
_ASSERTE(hglob);
if (hglob)
{
LPVOID lplock = LockResource(hglob);
_ASSERTE(lplock);
if (lplock)
{
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; 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;
_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++;
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.
Summary: Reading Binary Files
The code presented above can be boiled down to:
CResourceFile rf;
if (rf.Open(NULL, _T("IDU_FILE7"), _T("BINARY"))) {
int len = rf.GetLength(); BYTE *p = rf.GetByteBuffer(); if (p)
{
--- do something ---
}
.
.
.
}
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;
if (rf.Open(NULL, MAKEINTRESOURCE(IDU_FILE7), _T("BINARY")))
{
--- rest of code is unchanged ---
The Win32 MAKEINTRESOURCE() 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,
A value of NULL specifies the module handle associated with the image file that the operating system used to create the current process.
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");
CResourceFile Quick Reference
Here is complete list of functions available in CResourceFile
:
How to Add CResourceFile to Your Project
To integrate CResourceFile
class into your app, do the following:
- You first need to add the following files to your project:
- ResourceFile.cpp
- ResourceFile.h
- In Visual Studio settings, select Not using pre-compiled header for ResourceFile.cpp.
- Next, include header file ResourceFile.h in the source file where you want to use
CResourceFile
. - Now you are ready to start using
CResourceFile
. See above for sample code.
Reading Text Files
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 BOM 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 BOM markers:
BOOL CResourceTextFile::Open(HINSTANCE hInstance,
LPCTSTR lpszResId,
LPCTSTR lpszResType ,
ConvertAction eConvertAction ,
BomAction eBomAction )
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 BOM 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
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
m_List.AddLine(CXListBox::Blue, CXListBox::White,
_T("=== Checking UNICODE TEXT file test4.txt ==="));
TestTextResource(_T("IDU_FILE4"), 1, 15, eConvertAction,
CResourceTextFile::NoBomAction);
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);
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.
Summary: Reading Text Files
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(); }
CResourceTextFile Quick Reference
Here is a complete list of functions implemented in CResourceTextFile
:
How to Add CResourceTextFile To Your Project
To integrate CResourceTextFile
class into your app, do the following:
- You first need to add following files to your project:
- ResourceFile.cpp
- ResourceFile.h
- ResourceTextFile.cpp
- ResourceTextFile.h
- In Visual Studio settings, select Not using pre-compiled header for ResourceFile.cpp and ResourceTextFile.cpp.
- Next, include header file ResourceTextFile.h in the source file where you want to use
CResourceTextFile
. - Now you are ready to start using
CResourceTextFile
. See above for sample code.
Implementation Notes
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.
Summary
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.
Revision History
Version 1.0 — 2007 July 7
Usage
This software is released into the public domain. You are free to use it in any way you like, except that you may not sell this source code. If you modify it or extend it, please consider posting new code here for everyone to share. This software is provided "as is" with no expressed or implied warranty. I accept no liability for any damage or loss of business that this software may cause.