Skip to content

Conversation

ptorr-msft
Copy link
Contributor

Added description of string-table resources and a snippet of code to help enumerate actual strings. Thanks to @oldnewthing's blog for the details.

Added info on string tables and how to enumerate them
Polished it a bit
Copy link
Contributor

@ptorr-msft : Thanks for your contribution! The author(s) and reviewer(s) have been notified to review your proposed change.

@ptorr-msft
Copy link
Contributor Author

@oldnewthing FYI, this is based on info from your blog. This had a (now-archived) KB article written about it and you wrote a nice blog post about it, but the actual Win32 API docs are quite useless. This is my attempt to fix it. (Assuming it is approved, I will submit another PR for the actual Enum* APIs to refer to this article for info about enumerating strings.)

### String Table Resources

When enumerating String Table resources (type **RT_STRING**) with functions such as **EnumResourceNamesW**, the system doesn't
enumerate each individual string resource ID; instead it enumerates blocks of resources. You must inpsect these blocks to
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
enumerate each individual string resource ID; instead it enumerates blocks of resources. You must inpsect these blocks to
enumerate each individual string resource ID; instead it enumerates blocks of resources. You must inspect these blocks to


When enumerating String Table resources (type **RT_STRING**) with functions such as **EnumResourceNamesW**, the system doesn't
enumerate each individual string resource ID; instead it enumerates blocks of resources. You must inpsect these blocks to
determine which string resource ID(s) they actually contain.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This phrasing makes it sound like EnumResourceNamesW contains a special case for strings. But really, there is no special case. EnumResourceNamesW enumerates the resources. It's just that strings do not map to resources 1:1.

I think we can delete this paragraph and instead augment the second paragraph.
'

Strings provided in the resource string table are packaged into blocks of 16 strings per block. Each string in the block takes the form of a 16-bit integer representing the length of the string, followed by that number of 16-bit Unicode (UTF-16LE) code units. Resource ID 1 is a block that holds strings 0 to 15, resource ID 2 is a block that holds strings 16 to 31, and in general, resource ID N is a block that holds strings (N-1) × 16 to (N-1)×16 + 15. Missing string table IDs are treated as empty strings, and any block that consists entirely of empty strings is omitted. The EnumResourceNamesW function enumerates the resource blocks, not the individual strings.

For example, the following string table

STRINGTABLE BEGIN
1 "One"
5 "Five"
100 "Hundred"
STRINGTABLE END

is encoded as two string blocks.

Block 1:

0000 | Length of string 0 (empty string)
0003 | Length of string 1 (three characters)
004F | "O"
006E | "n"
0065 | "e"
0003 | Length of string 2 (three characters)
0054 | "T"
0077 | "w"
006F | "o"
0000 | Length of string 3 (empty string)
0000 | Length of string 4 (empty string)
0004 | Length of string 5 (four characters)
0046 | "F"
0069 | "i"
0076 | "v"
0065 | "e"
0000 | Length of string 6 (empty string)
0000 | Length of string 7 (empty string)
0000 | Length of string 8 (empty string)
0000 | Length of string 9 (empty string)
0000 | Length of string 10 (empty string)
0000 | Length of string 11 (empty string)
0000 | Length of string 12 (empty string)
0000 | Length of string 13 (empty string)
0000 | Length of string 14 (empty string)
0000 | Length of string 15 (empty string)

Block 7:

0000 | Length of string 96 (empty string)
0000 | Length of string 97 (empty string)
0000 | Length of string 98 (empty string)
0000 | Length of string 99 (empty string)
0007 | Length of string 100 (7 characters)
0048 | "H"
0075 | "u"
006E | "n"
0064 | "d"
0072 | "r"
0065 | "e"
0064 | "d"
0000 | Length of string 101 (empty string)
0000 | Length of string 102 (empty string)
0000 | Length of string 103 (empty string)
0000 | Length of string 104 (empty string)
0000 | Length of string 105 (empty string)
0000 | Length of string 106 (empty string)
0000 | Length of string 107 (empty string)
0000 | Length of string 108 (empty string)
0000 | Length of string 109 (empty string)
0000 | Length of string 110 (empty string)
0000 | Length of string 111 (empty string)

Note that the strings are not null-terminated.

std::wcout << L"Explorer.exe contains " << nStrings << L" strings (in " << nStringBlocks
<< L" blocks)." << std::endl;
}
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would accept a path as a parameter, rather than hard-coding Explorer.

Maybe

BOOL PrintStringsInBlock(HMODULE module, PCWSTR type, PCWSTR name, LONG_PTR data)
{
    // Load the block and obtain a pointer to it.
    auto ptr = (wchar_t*)LockResource(LoadResource(hModule, FindResource(module, name, type)));
    if (ptr)
    {
        // Print all the non-empty strings in the block.
        unsigned int baseId = (PtrToInt(name) - 1) * 16;
        for (unsigned int stringIndex = 0; stringIndex < 16; ++stringIndex)
        {
            wchar_t size = *ptr;
            if (size > 0)
            {
                printf("ID %u: ", baseId + stringIndex);
                for (unsigned int charIndex = 0; charIndex < size; ++charIndex)
                {
                    fputwc(ptr[charIndex + 1], stdout); // requires stdout to be set to Unicode
                }
                printf("\n");
            }
            // Skip to next potential string in the block
            ptr += size + 1;
        }
    }
    return TRUE;
}

void PrintStringsInModule(PCWSTR modulePath)
{
    HMODULE module = LoadLibraryExW(modulePath, nullptr, LOAD_LIBRARY_AS_DATAFILE);
    if (!module) return;

    EnumResourceNamesW(module, RT_STRING, PrintStringsInBlock, 0);
    FreeLibrary(module);
]


## Remarks

### String Table Resources
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this content belongs in a separate page entirely, since it applies to multiple APIs (FindResourceW, EnumResourceNamesW, UpdateResourceW, plus the A versions).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants