Skip to content

feat(string): Implement UTF-8 string conversion and validation functions - #2528

Merged
xezon merged 26 commits into
TheSuperHackers:mainfrom
bobtista:bobtista/feat/utf8-string-functions
Aug 18, 2026
Merged

feat(string): Implement UTF-8 string conversion and validation functions#2528
xezon merged 26 commits into
TheSuperHackers:mainfrom
bobtista:bobtista/feat/utf8-string-functions

Conversation

@bobtista

@bobtista bobtista commented Apr 3, 2026

Copy link
Copy Markdown

Adds portable UTF-8/wide-character conversion helpers to WWLib and uses them for engine string translation and the existing GameSpy conversion paths.

This picks up the work from #2045 with API and behavior changes based on review feedback.

WWLib/utf8.h / utf8.cpp

Adds four conversion functions:

  • Wide_To_Utf8_Len
  • Utf8_To_Wide_Len
  • Wide_To_Utf8
  • Utf8_To_Wide

The _Len functions return the required destination size without counting a null terminator. The conversion functions return the number of units written and write a null terminator when the destination has spare capacity.

Malformed UTF-8 is reported as UTF8_INVALID, keeping it distinct from a successful empty conversion, which returns 0.

The decoder validates UTF-8 according to RFC 3629, rejecting:

  • malformed and truncated sequences;
  • overlong encodings;
  • surrogate codepoints;
  • codepoints above U+10FFFF.

The encoder substitutes U+FFFD for unpaired UTF-16 surrogates and wide values that have no valid UTF-8 representation. Its output therefore always decodes successfully through Utf8_To_Wide.

The implementation uses no platform text APIs. It adapts to the platform width of wchar_t:

  • 16-bit wchar_t is handled as UTF-16, including surrogate pairs;
  • 32-bit wchar_t is handled as UTF-32 codepoints.

AsciiString::translate

Replaces the original 7-bit-only wide-to-narrow translation with UTF-8 encoding.

UnicodeString::translate

Decodes valid UTF-8 into the platform wide-character representation.

When the input is not valid UTF-8, translation falls back to the previous unsigned-byte-to-wide-character behavior. This preserves legacy byte values instead of rejecting or replacing the input. It is a compatibility fallback, not a complete code-page-specific decoder.

Because AsciiString does not carry encoding metadata, valid UTF-8 is preferred over the legacy fallback.

ThreadUtils.cpp

Replaces the GameSpy-specific Win32 conversion calls with the shared WWLib functions.

MultiByteToWideCharSingleLine uses the same invalid-UTF-8 fallback as UnicodeString::translate. Newline and carriage-return replacement remains unchanged.

Non-goals

This change does not make narrow filesystem APIs Unicode-aware. Paths are still passed to the existing narrow platform APIs; updating those APIs belongs in a separate change.

Verification

  • Tested ASCII and 2-, 3-, and 4-byte UTF-8 round trips.
  • Tested both 16-bit and 32-bit wchar_t paths.
  • Tested rejection of malformed, truncated, overlong, surrogate, and out-of-range UTF-8 sequences.
  • Tested replacement of unpaired wide surrogates and out-of-range wide values.
  • All Generals and GeneralsMD VC6 and Win32 CI configurations pass.
  • GeneralsMD replay checks pass.

@greptile-apps

greptile-apps Bot commented Apr 3, 2026

Copy link
Copy Markdown

Greptile Summary

Adds a portable UTF-8/wide-character transcoder and integrates it into engine string and GameSpy conversion paths.

  • Validates UTF-8 by rejecting malformed, overlong, surrogate, and out-of-range encodings.
  • Replaces the previous ASCII-only AsciiString and UnicodeString translations with UTF-8 conversion and a legacy byte-cast fallback.
  • Replaces GameSpy’s Win32 conversion calls with shared WWLib helpers.
  • Registers the new implementation in the WWLib build.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
Core/Libraries/Source/WWVegas/WWLib/utf8.cpp Implements width-aware UTF-8 transcoding with checks for malformed, overlong, surrogate, and out-of-range sequences.
Core/Libraries/Source/WWVegas/WWLib/utf8.h Defines the portable conversion API, invalid-input sentinel, sizing contracts, and destination-buffer behavior.
Core/GameEngine/Source/Common/System/AsciiString.cpp Replaces lossy wide-to-byte casting with correctly sized UTF-8 encoding.
Core/GameEngine/Source/Common/System/UnicodeString.cpp Adds validated UTF-8 decoding while preserving legacy non-UTF-8 data through byte-wise fallback.
Core/GameEngine/Source/GameNetwork/GameSpy/Thread/ThreadUtils.cpp Migrates GameSpy string conversion from Win32 APIs to WWLib and handles invalid UTF-8 through the documented fallback.
Core/Libraries/Source/WWVegas/WWLib/CMakeLists.txt Adds the portable UTF-8 source and header to the WWLib target.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Engine or GameSpy text] --> B{Conversion direction}
    B -->|UTF-8 to wide| C[Utf8_To_Wide_Len]
    C --> D{Valid UTF-8?}
    D -->|Yes| E[Utf8_To_Wide]
    D -->|No| F[Legacy one-to-one byte fallback]
    B -->|Wide to UTF-8| G[Wide_To_Utf8_Len]
    G --> H[Wide_To_Utf8]
    E --> I[UnicodeString or std::wstring]
    F --> I
    H --> J[AsciiString or std::string]
Loading

Reviews (17): Last reviewed commit: "build: Use qualified include path for WW..." | Re-trigger Greptile

Comment thread Core/Libraries/Source/WWVegas/WWLib/utf8.cpp Outdated
@bobtista

bobtista commented Apr 3, 2026

Copy link
Copy Markdown
Author
  • Fixed the if formatting
  • added RFC 3629 overlong and out-of-range checks
  • RE the theoretical memory leak, can that even happen here? set() allocates via the engine's custom memory allocator which crashes on failure rather than throwing, so the leak path can't really be reached right?

Comment thread Core/Libraries/Source/WWVegas/WWLib/utf8.h Outdated
Comment thread Core/Libraries/Source/WWVegas/WWLib/utf8.cpp Outdated
Comment thread Core/GameEngine/Source/GameNetwork/GameInfo.cpp Outdated
Comment thread Core/GameEngine/Source/GameNetwork/GameSpy/Thread/ThreadUtils.cpp Outdated
Comment thread Core/GameEngine/Source/GameNetwork/GameSpy/Thread/ThreadUtils.cpp Outdated
Comment thread Core/GameEngine/Source/Common/System/UnicodeString.cpp Outdated
Comment thread Core/GameEngine/Source/Common/System/AsciiString.cpp Outdated
Comment thread Core/GameEngine/Source/GameNetwork/GameInfo.cpp Outdated
Comment thread Core/GameEngine/Source/Common/System/AsciiString.cpp Outdated
@xezon xezon added Enhancement Is new feature or request Minor Severity: Minor < Major < Critical < Blocker labels Apr 3, 2026
Comment thread Core/GameEngine/Source/GameNetwork/GameInfo.cpp Outdated

@xezon xezon left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Get_Utf8_Size should not include the null terminator in its size.

Comment thread Core/GameEngine/Source/Common/System/AsciiString.cpp Outdated
Comment thread Core/GameEngine/Source/Common/System/AsciiString.cpp Outdated
Comment thread Core/Libraries/Source/WWVegas/WWLib/utf8.h Outdated
Comment thread Core/Libraries/Source/WWVegas/WWLib/utf8.h Outdated
Comment thread Core/Libraries/Source/WWVegas/WWLib/utf8.cpp Outdated
Comment thread Core/Libraries/Source/WWVegas/WWLib/utf8.cpp Outdated
Comment thread Core/Libraries/Source/WWVegas/WWLib/utf8.cpp Outdated
Comment thread Core/Libraries/Source/WWVegas/WWLib/utf8.cpp Outdated
Comment thread Core/GameEngine/Source/GameNetwork/GameSpy/Thread/ThreadUtils.cpp Outdated
Comment thread Core/GameEngine/Source/GameNetwork/GameSpy/Thread/ThreadUtils.cpp Outdated
Comment thread Core/GameEngine/Source/GameNetwork/GameSpy/Thread/ThreadUtils.cpp Outdated
Comment thread Core/Libraries/Source/WWVegas/WWLib/utf8.cpp Outdated
Comment thread Core/GameEngine/Source/Common/System/UnicodeString.cpp Outdated
Comment thread Core/GameEngine/Source/Common/System/AsciiString.cpp Outdated
Comment thread Core/GameEngine/Source/Common/System/AsciiString.cpp Outdated
Comment thread Core/GameEngine/Source/Common/System/AsciiString.cpp Outdated
Comment thread Core/GameEngine/Source/GameNetwork/GameSpy/Thread/ThreadUtils.cpp Outdated
Comment thread Core/Libraries/Source/WWVegas/WWLib/utf8.cpp Outdated
Comment thread Core/Libraries/Source/WWVegas/WWLib/utf8.cpp Outdated
@xezon

xezon commented Apr 6, 2026

Copy link
Copy Markdown

The diff now shows unrelated changes.

@bobtista
bobtista force-pushed the bobtista/feat/utf8-string-functions branch from 39d7229 to 40393b8 Compare April 6, 2026 20:02
@bobtista

bobtista commented Apr 6, 2026

Copy link
Copy Markdown
Author

The diff now shows unrelated changes.

Try again cleaned up the commits and force pushed

}
ensureUniqueBufferOfSize((Int)size + 1, false, nullptr, nullptr);
char* buf = peek();
if (!Unicode_To_Utf8(buf, src, srcLen, size))

@Mauller Mauller Apr 7, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

So is this translating UTF16LE from windows into UTF8 that is then stored within AsciiString?

If so this may help with the paths issue with usernames and paths not using Latin characters, but file handling functions will need updating to use unicode variants instead of Ascii.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yes. It does not fix path handling on its own though, the file APIs still take the narrow string. Should that be a separate change?

@Mauller

Mauller commented Apr 7, 2026

Copy link
Copy Markdown

I wonder if we should also add a flag to state that the Ascii string is holding a UTF8 string?

I guess all normal ascii characters will display properly, it's just extended character sets that will look garbled.

Comment thread Core/Libraries/Source/WWVegas/WWLib/CMakeLists.txt
@OmniBlade

Copy link
Copy Markdown

Slight nitpick on function naming, shouldn't it be Utf16 rather than Wchar as Wchar is only Utf16 on windows yet we will likely want the conversion functions on other platforms as well for at least csf parsing if nothing else.

@bobtista

bobtista commented Apr 8, 2026

Copy link
Copy Markdown
Author

Slight nitpick on function naming, shouldn't it be Utf16 rather than Wchar as Wchar is only Utf16 on windows yet we will likely want the conversion functions on other platforms as well for at least csf parsing if nothing else.

Yeah, but it's consistent with the other naming as it is for now. How about we keep the naming and using a uint16_t/char16_t type internally rather than wchar_t when we make non windows paths? Or would you rather we rename to something like Utf16_To_Utf8?

@Mauller

Mauller commented Apr 13, 2026

Copy link
Copy Markdown

Slight nitpick on function naming, shouldn't it be Utf16 rather than Wchar as Wchar is only Utf16 on windows yet we will likely want the conversion functions on other platforms as well for at least csf parsing if nothing else.

Yeah, but it's consistent with the other naming as it is for now. How about we keep the naming and using a uint16_t/char16_t type internally rather than wchar_t when we make non windows paths? Or would you rather we rename to something like Utf16_To_Utf8?

If anything it would be Utf16Le_To_Utf8, windows uses the little endian utf16 format. Not sure if Utf16Be is used much anywhere but worth being concise with it.

Comment thread Core/Libraries/Source/WWVegas/WWLib/utf8.cpp Outdated
Comment thread Core/Libraries/Source/WWVegas/WWLib/utf8.cpp Outdated
Comment thread Core/Libraries/Source/WWVegas/WWLib/utf8.cpp Outdated
Comment thread Core/Libraries/Source/WWVegas/WWLib/utf8.cpp Outdated
Comment thread Core/Libraries/Source/WWVegas/WWLib/utf8.cpp Outdated
Comment thread Core/Libraries/Source/WWVegas/WWLib/utf8.cpp Outdated
Comment thread Core/Libraries/Source/WWVegas/WWLib/utf8.cpp Outdated
Comment thread Core/Libraries/Source/WWVegas/WWLib/utf8.cpp Outdated
Comment thread Core/GameEngine/Source/GameNetwork/GameSpy/Thread/ThreadUtils.cpp Outdated
Comment thread Core/Libraries/Source/WWVegas/WWLib/utf8.cpp Outdated
Comment thread Core/Libraries/Source/WWVegas/WWLib/utf8.cpp Outdated
Comment thread Core/Libraries/Source/WWVegas/WWLib/utf8.cpp Outdated
Comment thread Core/Libraries/Source/WWVegas/WWLib/utf8.cpp Outdated
Comment thread Core/Libraries/Source/WWVegas/WWLib/utf8.cpp Outdated
@bobtista

Copy link
Copy Markdown
Author

Pushed updates addressing the open comments:

  • Portable transcoder, dropped the Win32 WideCharToMultiByte/MultiByteToWideChar wrappers and the #else #error for a hand-rolled RFC 3629 encode/decode in utf8.cpp. It compiles on all platforms now, and the wide side adapts to wchar_t width at compile time (UTF-16 with surrogate pairs on Windows, UTF-32 elsewhere), so it's reusable for CSF parsing off-Windows. @OmniBlade — I kept the Utf16Le_* naming for now per the earlier thread; happy to rename if you'd prefer.
  • CP1252 fallback, UnicodeString::translate now validates first. Valid UTF-8 decodes as UTF-8; anything else (legacy CP1252) falls back to the old 1:1 byte cast instead of producing replacement characters. Bonus: data written by the old lossy path reads back byte-identical.
  • Scope, reverted the ensureUniqueBufferOfSize null-terminator change, so the string files now carry only the translate rewrite.

Tested the codec in isolation: ASCII / 2- / 3- / 4-byte round-trips on both wide widths, plus RFC 3629 rejection of overlong, surrogate, out-of-range, and truncated sequences.

Ready for another pass

@bobtista

Copy link
Copy Markdown
Author

Reworked the conversion interface:

Utf8_To_Wide_Len / Utf8_To_Wide return UTF8_INVALID on bad input. 0 now means empty only, so callers can tell the two apart.
The encoder substitutes U+FFFD for unpaired surrogates and out of range wide values. Before this, a lone surrogate encoded to ED A0 80, which the decoder then rejected.
Renamed to Wide_To_Utf8 / Utf8_To_Wide. The wide side is wchar_t, which is UTF-32 off Windows, so the Utf16Le name was wrong.
MultiByteToWideCharSingleLine falls back to a byte cast on non-UTF-8, matching UnicodeString::translate. It returned an empty string before.
Dropped Utf8_Validate, Utf8_To_Wide_Len covers it and nothing else called it.

@xezon xezon left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I think it is a good idea to make this custom implementation and be independent of Windows API.

Comment thread Core/GameEngine/Source/Common/System/AsciiString.cpp Outdated
Comment thread Core/Libraries/Source/WWVegas/WWLib/utf8.h Outdated
Comment thread Core/Libraries/Source/WWVegas/WWLib/utf8.cpp Outdated
Comment thread Core/Libraries/Source/WWVegas/WWLib/utf8.h Outdated
Comment thread Core/Libraries/Source/WWVegas/WWLib/utf8.cpp Outdated
@CryoTheRenegade

Copy link
Copy Markdown

Have we thought about implementing ICU4C instead of a custom implementation?
Also for posix compatible utf-8, we could probably reference this

@OmniBlade OmniBlade left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Not a fan of having different encodings on different platforms personally and went with an ICU4C approach as suggested by CryoTheRenegade for OpenW3D which keeps the representation as utf-16 for Wide strings on all platforms. The refactor to support that is larger though and it adds a dependency.

@bobtista

bobtista commented Aug 5, 2026

Copy link
Copy Markdown
Author

Have we thought about implementing ICU4C instead of a custom implementation? Also for posix compatible utf-8, we could probably reference this

Yes we've talked about ICU, so far the play has been hold off until VC6 is dropped

@xezon

xezon commented Aug 5, 2026

Copy link
Copy Markdown

Can we use ICU for non VC6 and whatever else for VC6 ?

@bobtista

bobtista commented Aug 5, 2026

Copy link
Copy Markdown
Author

Can we use ICU for non VC6 and whatever else for VC6 ?

Kind of, but we'd want to replace wchar_t with UTF-16 first. I think we merge this, then use OmniBlade’s OpenW3D approach to refactor WideChar/UnicodeString to fixed 16-bit storage. Then we can use ICU because both VC6 and non VC6 builds would share the same internal representation and public contract, only the backend would differ.

@xezon xezon left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Looks good. A few things can be polished.

Comment thread Core/Libraries/Source/WWVegas/WWLib/utf8.cpp Outdated
Comment thread Core/GameEngine/Source/Common/System/UnicodeString.cpp Outdated
do
{
c = wcschr(dest, L'\n');
c = wcschr(&ret[0], L'\n');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Any idea why this is removing \r\n ? Probably should have been a different function doing that.

do
{
c = wcschr(dest, L'\r');
c = wcschr(&ret[0], L'\r');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

While at it, maybe optimize these loops. It is searching for the character from the beginning of the string on every iteration. Or better in a follow up change.

@xezon xezon changed the title feat(string): add UTF-8 string conversion and validation functions feat(string): Add UTF-8 string conversion and validation functions Aug 18, 2026
@xezon xezon changed the title feat(string): Add UTF-8 string conversion and validation functions feat(string): Implement UTF-8 string conversion and validation functions Aug 18, 2026
@xezon
xezon merged commit c98fcaf into TheSuperHackers:main Aug 18, 2026
16 checks passed
mahdip72 pushed a commit to mahdip72/GeneralsGameCode that referenced this pull request Aug 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Enhancement Is new feature or request Minor Severity: Minor < Major < Critical < Blocker

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants