Skip to content

Commit

Permalink
A lot of FONT/FONTDIR changes
Browse files Browse the repository at this point in the history
A lengthy deep dive just to realize that FONTDIR doesn't really do anything at all. At least now I'm confident in the FONT/FONTDIR behavior of resinator, though. I believe the resinator behavior is now more 'correct' than the 32-bit rc.exe has ever been.
  • Loading branch information
squeek502 committed Jun 6, 2023
1 parent 234ed8d commit cba3b36
Show file tree
Hide file tree
Showing 10 changed files with 635 additions and 71 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ The plan is to use fuzz testing with the `rc` tool as an oracle to ensure that `
- `resinator` will allow parameters that are limited to a `u16` to contain numbers with `L` suffixes, but will truncate the value to a `u16` and emit a warning about the Win32 behavior.
+ The Win32 RC compiler will error with something like `version WORDs separated by commas expected` or `PRIMARY LANGUAGE ID too large` if any number literal within such a parameter is evaluated to have an `l`/`L` suffix (since in theory that tells the compiler to use a `u32` for that number). Note, though, that it doesn't complain about numbers that overflow a `u16` and uses wrapping overflow as is common everywhere else.
+ Statements that this affects all parameters of: `PRODUCTVERSION`, `FILEVERSION`, `LANGUAGE`
- `resinator` will write (what seems to be) a more correct `FONTDIR` resource when `FONT` resources are present in the `.rc` file
+ See [the resinator `FONT` documentation for a full explanation](https://squeek502.github.io/resinator/resources/font)

#### Resource data and `.res` filesize limits

Expand Down
1 change: 1 addition & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ pub fn build(b: *std.build.Builder) void {
_ = addFuzzyTest(b, "icons", mode, target, resinator, all_fuzzy_tests_step, test_options);
_ = addFuzzyTest(b, "bitmaps", mode, target, resinator, all_fuzzy_tests_step, test_options);
_ = addFuzzyTest(b, "stringtable", mode, target, resinator, all_fuzzy_tests_step, test_options);
_ = addFuzzyTest(b, "fonts", mode, target, resinator, all_fuzzy_tests_step, test_options);

_ = addFuzzer(b, "fuzz_rc", &.{}, resinator, target);

Expand Down
20 changes: 0 additions & 20 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,26 +66,6 @@ Currently a dumping ground for various pieces of information related to `.rc` an
| `IMPURE` | `flags & ~(PURE | DISCARDABLE)` |
| `DISCARDABLE` | `flags | (DISCARDABLE | MOVEABLE | SHARED)` |

## `FONT` resource

- The `<id>` in the `<id> FONT` definition **must** be an ordinal, not a string.
- The `<id>` of each `FONT` must be unique, but this does not fail the compilation, only emits an error. The first `FONT` defined with the `<id>` takes precedence, all others with the same `<id>` are ignored.
- Each `FONT` is stored with type `RT_FONT`. The entire binary contents of the specified file are the data. No validation/parsing is done of the data.

At the end of the .res, a single `RT_FONTDIR` resource with the name `FONTDIR` is written with the data:

| Size/Type | Description |
|-----------|--------|
| `u16` | Number of total font resources |
| - | *Below is repeated for each `RT_FONT` in the `.res`* |
| `u16` | ID of the `RT_FONT` |
| 150 bytes | The first 148 bytes of the `FONT`'s file, followed by two `0x00` bytes. If the file is smaller than 148 bytes, all missing bytes are filled with `0x00`. |

{: .note }
> There appears to be some bugs in the MSVC++ `rc` tool for files smaller than 148 bytes
> - If the file is 75 bytes or smaller with no null bytes, the `FONTDIR` data for it will be 149 bytes (the first `n` being the bytes from the file, then the rest are `0x00` padding bytes). After that, there will be `n` bytes from the file again, and then a final `0x00`.
> - If the file is between 76 and 140 bytes long with no null bytes, the MSVC++ `rc` tool will crash.
## `ACCELERATORS` resource

- Warning on `SHIFT` or `CONTROL` without `VIRTKEY`
Expand Down
86 changes: 86 additions & 0 deletions docs/resources/font.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
---
layout: default
title: FONT
parent: Resources
---

# `FONT` resource

- The `<id>` in the `<id> FONT` definition **must** be an ordinal, not a string.
- The `<id>` of each `FONT` must be unique, but this does not fail the compilation, only emits an error. The first `FONT` defined with the `<id>` takes precedence, all others with the same `<id>` are ignored.
- Each `FONT` is stored with type `RT_FONT`. The entire binary contents of the specified file are the data. No validation/parsing is done of the data.

At the end of the .res, a single `RT_FONTDIR` resource with the name `FONTDIR` is written. The `FONTDIR` resource doesn't seem to matter at all; it can even be entirely omitted seemingly without any practical consequences.

What follows is a more complete explanation of the `FONTDIR` resource:

## What is `FONT` used for anyway?

The `FONT` resource is basically obsolete--the only purpose of it is to bundle `.fnt` bitmap fonts into `.fon` files (which are resource-only DLLs renamed to have a `.fon` extension; they are recognized as fonts and can be installed like any other font). Although modern Windows versions still *use* `.fon` files (e.g. quite a few `.fon` files are distributed with the default Windows 10 installation in `C:\Windows\Fonts`), it seems that `rc.exe` is no longer really equipped to *generate* `.fon` files in the same way as the 16-bit version did.

From [TN318C.txt: How to create Windows .FON font libraries for use](https://community.embarcadero.com/article/technical-articles/149-tools/14454-how-to-create-windows-fon-font-libraries-for-use):

> To complete the .FON file, you must append the font resources onto the font library. This is done with: RC xxxxxxxx.rc xxxxxxxx.fon
This usage of `rc.exe` is only possible in the 16-bit version of `rc.exe`, where it would do the linking of the resources itself (and the `.res` file was an optional intermediate output). The modern equivalent would be [Creating a resource-only DLL](https://learn.microsoft.com/en-us/cpp/build/creating-a-resource-only-dll?view=msvc-170), where `rc.exe` is used to generate a `.res` which then gets linked with a DLL that has no entry point.

## So what should go in the `FONTDIR`?

Practically, it doesn't seem to really matter what's in the `FONTDIR`--it's likely not even read/parsed since it's possible to just iterate over the `FONT` resources directly instead. A `.res` without a `FONTDIR` is seemingly treated the same at link-time, and a resource-only DLL without a `FONTDIR` is seen the same by the Windows font system as one with a `FONTDIR`.

If we *do* want to write a `FONTDIR` resource, though, the format that the 32-bit `rc.exe` writes seems to be incorrect on a fundamental level.

First, let's look at some of the relevant documentation:
- [`FONTGROUPHDR`](https://learn.microsoft.com/en-us/windows/win32/menurc/fontgrouphdr)
- [`DIRENTRY`](https://learn.microsoft.com/en-us/windows/win32/menurc/direntry)
- [`FONTDIRENTRY`](https://learn.microsoft.com/en-us/windows/win32/menurc/fontdirentry)

So, the `FONTDIR`'s data should be a `FONTGROUPHDR` with the total number of fonts, followed by a `DIRENTRY` and a `FONTDIRENTRY` for each font. The `FONTDIRENTRY` is not an actual struct--all fields are contiguous with no padding--so each `FONTDIRENTRY` is at least `115` bytes: `113` for the statically sized fields, and then `szDeviceName` and `szFaceName` are both variable-length `NUL`-terimated strings (so at the very least they will have a `NUL` byte).

If we run the 16-bit version of `rc.exe`, this is exactly what you see for each font. However, for the 32-bit version of `rc.exe`, it will output at least `150` bytes for each `FONTDIRENTRY`. This difference has an explanation: the Windows 3.0 `.FNT` specification has the header size as `148`, so `rc.exe` is writing the full v3.0 header and then the `szDeviceName` and `szFaceName` as `NUL`-terminated strings. However, this seems incorrect for two reasons:

- There does not seem to be any updated `FONTDIRENTRY` docs that accommodate this new size, so anyone trying to enumerate the `FONTDIRENTRY`s in a `FONTDIR` will end up reading garbage after the first entry (since the offset to the next `FONTDIRENTRY` won't match the `FONTDIRENTRY` size as specified in the documentation)
- The `szDeviceName` and `szFaceName` are wrong. The Win32 compiler seems to be doing subtraction from end of the `.FNT` header to get the `dfDevice` and `dfFace` fields (which themselves contain offsets from the beginning of the file to where the associated `NUL`-terminated string is located). Since the 32-bit version is using a header size of 148, this means it is grabbing the fields from within a reserved section of the header instead of where the fields actually are (that is, instead of doing `113 - 12 = 101` for `dfDevice` it's doing `148 - 12 = 136` and reading from there instead). Subsequently, the 32-bit version of `rc.exe` basically never writes the `szDeviceName` or `szFaceName` to the `FONTDIRENTRY` since it's looking in the wrong place for the `dfDevice` and `dfFace` fields. This can be confirmed by modifying byte offset `136` and `140` to contain an offset of a `NUL-terminated` string in the file and seeing that `rc.exe` will use them as `szDeviceName`/`szFaceName`.

{: .note }
> The 148 byte size corresponds to the format specified [here](https://web.archive.org/web/20080115184921/http://support.microsoft.com/kb/65123) up to the end of `dfReserved1`. Those last 16 reserved bytes are the section in which the 32-bit `rc.exe` is erroneously interpreting as the `dfDevice`/`dfFace` fields.
## So really, what should go in the `FONTDIR`?

There are a few possibilities:

The first possibility is that the current `rc.exe` behavior should be emulated, even if it seems wrong. If that's the case, then the format would look like this:

| Size/Type | Description |
|-----------|--------|
| `u16` | Number of total font resources |
| - | *Below is repeated for each `RT_FONT` in the `.res`* |
| `u16` | ID of the `RT_FONT` |
| 148 bytes | The first 148 bytes of the `FONT`'s file |
| At least 1 byte | `NUL`-terminated string containing the 'device name'. To match the 32-bit `rc.exe`, the device name is the string located at the offset dictated by the `u32` at offset `140` of the file |
| At least 1 byte | `NUL`-terminated string containing the 'face name'. To match the 32-bit `rc.exe`, the face name is the string located at the offset dictated by the `u32` at the offset `144` of the file |

The second possibility is that the current `rc.exe` is miscompiling the `FONTDIR` and the 16-bit `rc.exe` behavior is correct. If that's the case, then the format would look like this:

| Size/Type | Description |
|-----------|--------|
| `u16` | Number of total font resources |
| - | *Below is repeated for each `RT_FONT` in the `.res`* |
| `u16` | ID of the `RT_FONT` |
| 113 bytes | The first 113 bytes of the `FONT`'s file (aka all of the fields of [`FONTDIRENTRY`](https://learn.microsoft.com/en-us/windows/win32/menurc/fontdirentry) without `szDeviceName` and `szFaceName` and no padding between any of the fields) |
| At least 1 byte | `NUL`-terminated string containing the 'device name'. The device name is the string located at the offset dictated by the [`dfDevice` field of `FONTDIRENTRY`](https://learn.microsoft.com/en-us/windows/win32/menurc/fontdirentry) (101 bytes into the file) |
| At least 1 byte | `NUL`-terminated string containing the 'face name'. The face name is the string located at the offset dictated by the [`dfFace` field of `FONTDIRENTRY`](https://learn.microsoft.com/en-us/windows/win32/menurc/fontdirentry) (105 bytes into the file) |

There is a third possibility that the Win32 behavior of writing the full 148-byte long `.FNT` header is intended, but the 'device name'/'face name' bug should be fixed. This would be a combination of the two formats above.

{: .note }
> If the font file is 140 bytes or fewer, the 32-bit `rc.exe` seems to default to a `dfFace` of `0` (as the [incorrect] location of the `dfFace` field is past the end of the file).
> - If the file is 75 bytes or smaller with no null bytes, the `FONTDIR` data for it will be 149 bytes (the first `n` being the bytes from the file, then the rest are `0x00` padding bytes). After that, there will be `n` bytes from the file again, and then a final `0x00`.
> - If the file is between 76 and 140 bytes long with no `0x00` bytes, the MSVC++ `rc` tool will crash.
{: .note }
> `rc.exe` actually seems to interpret the [incorrectly located] `dfFace` and `dfName` values as signed integers. If a value in either field that is interpreted as negative it will lead to a `fatal error RW1023: I/O error seeking in file` error.
## What does `resinator` do?

`resinator` has gone with the second possibility: write each `FONTDIRENTRY` as specified in the available documentation (so 113 bytes + the device/type name as trailing `NUL`-terminated strings).
9 changes: 9 additions & 0 deletions docs/resources/resources.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
layout: default
title: Resources
nav_order: 2
has_children: true
permalink: /docs/resources
---

# Resources
Loading

0 comments on commit cba3b36

Please sign in to comment.