Skip to content

Add reserved name handling and add user filename validation#42

Merged
Flamefire merged 10 commits into
Return-To-The-Roots:masterfrom
MichalLabuda:add-filename-validation
Jun 30, 2026
Merged

Add reserved name handling and add user filename validation#42
Flamefire merged 10 commits into
Return-To-The-Roots:masterfrom
MichalLabuda:add-filename-validation

Conversation

@MichalLabuda

Copy link
Copy Markdown
Contributor

Summary

Adds Windows reserved device name support to makePortableName and introduces two new predicates (isValidFileNameChar, isValidFileName) for validating user-provided filenames.

Motivation

These utilities are needed in s25client to validate user-provided filenames in save file dialogs and similar text input controls, where both real-time character filtering and save-time structural validation are required.

Note

makePortableFileName (and by extension makePortableName) is intended for programmatic filename creation - it silently and automatically replaces invalid characters without user involvement.

For user-provided filenames - such as save games and the incoming addon presets feature (s25client PR #1951) - the intended approach is different: invalid characters will be suppressed at the ctrlEdit input level using isValidFileNameChar, and isValidFileName will perform a final structural check on save. If validation fails, the user receives a general message asking them to correct the name before saving. There is no automatic character replacement and no per-reason error detail - both would complicate the implementation, and silent replacement in particular would create an inconsistent experience where the saved filename silently differs from what the user typed.

Changes

makePortableName

Windows reserved device names (CON, PRN, AUX, NUL, COM0COM9, LPT0LPT9) were previously passed through unchanged when they already satisfied bfs::portable_name. This caused a subtle bug where e.g. "nul" was returned as-is despite being unusable as a filename on Windows. The fix appends '_' to any reserved name.

isValidFileNameChar(char32_t)

New predicate for real-time filtering of user-typed filename characters. Rejects control characters and characters forbidden on Windows (< > : " / \ | ? *), which is the most restrictive set across all supported platforms.

isValidFileName(const std::string&)

New structural validator for complete user-provided filenames. Rejects:

  • Empty names
  • Windows reserved device names - On Windows 7 and earlier, the device name is determined by the part before the first dot - making "nul.ini" equivalent to NUL and therefore invalid. Windows 10/11 relaxed this so that only bare names like "nul" are restricted, while "nul.ini" is a valid regular file. For cross-version portability, fileName is checked against the segment before the first dot, so "nul.ini" is also rejected
  • Names starting or ending with a dot
  • Names with a trailing space (Windows silently strips it, causing a mismatch between the displayed and actual filename; trimming before calling is left to the caller)

Comment corrections

The existing doc comments on makePortableName, makePortableFileName and makePortableDirName incorrectly stated that invalid characters were removed, when they are in fact replaced with _. Comments have been corrected to accurately describe the sanitization behavior.

Tests

Added test cases covering reserved device name handling in makePortableName (bare names such as "con", "NUL", "com0") and the new isValidFileNameChar and isValidFileName functions.

@MichalLabuda

Copy link
Copy Markdown
Contributor Author

If you decide to accept and merge this, could you bump the submodule reference in s25client? There's a follow-up PR there that depends on this. Thanks!

@Flamefire Flamefire left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

For user-provided filenames - such as save games and the incoming addon presets feature (Return-To-The-Roots/s25client#1951) - the intended approach is different: invalid characters will be suppressed at the ctrlEdit input level using isValidFileNameChar, and isValidFileName will perform a final structural check on save. If validation fails, the user receives a general message asking them to correct the name before saving.

I'm not sure about the suppressing part.
I think the easiest way would be to simply run the users input through makePortableName and show an error if the result is different.
The current handling seems complicated: isValidFileName allows invalid characters and isValidFileNameChar seemingly allows at least non-portable characters.

On second thought I think it does make sense to allow users more freedom in their choice of file names, so fine by me.

Just fix isValidFileName to use isValidFileNameChar and add a single test for that (or 3: invalid char at start, middle, end, but I'd say one is enough)

I'm not a big fan of verbose tests, but having the valid and invalid chars in an array and using a loop might not be much clearer either. Just mentioning it for consideration where it might make sense in the future.

Comment thread libs/common/src/fileFuncs.cpp Outdated
Comment thread tests/testFilefuncs.cpp
Comment thread tests/testFilefuncs.cpp Outdated
Comment thread tests/testFilefuncs.cpp Outdated
@MichalLabuda MichalLabuda requested a review from Flamefire June 28, 2026 12:10

@MichalLabuda MichalLabuda left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I added isValidFileNameChar to isValidFileName, applied your other suggestions and trimmed the test cases. Will remember for the future to be a bit more conservative with them :-)
With the suppressing in ctrlEdit (similarly to numberOnly_ but for this it would be fileNameOnly_) I found it the easiest solution to implement without passing back some error codes and creating text messages for each case...
Would you like ctrlEdit modifications (literally 9 new lines including comments, and 4 removed for double-space bug fix) and save game fixes in a separate PR, single PR or added to addon-presets PR? Personally would prefer addon-presets, then I wouldn't have to wait for the ctrlEdit PR to be merged but that is impatient me ;-)

Comment thread libs/common/src/fileFuncs.cpp

@Flamefire Flamefire left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

trimmed the test cases. Will remember for the future to be a bit more conservative with them :-)

A bit too much, I might have been not clear enough. See suggestions. For each test simply think if this could potentially catch a (class of) mistake. I put my own reasoning on the reserved-with-extension suggestion. The others are similar: CON-NUL-nul-Lpt --> 2 examples, 3rd is same as 2 in lowercase, and a 4th mixed-case one.
For the special chars I was aiming for reasonably common ones.

Would you like ctrlEdit modifications (literally 9 new lines including comments, and 4 removed for double-space bug fix) and save game fixes in a separate PR, single PR or added to addon-presets PR? Personally would prefer addon-presets, then I wouldn't have to wait for the ctrlEdit PR to be merged but that is impatient me ;-)

Fine for me to add it where it's used, ie. the existing PR

Comment thread tests/testFilefuncs.cpp Outdated
Comment thread tests/testFilefuncs.cpp
Comment thread tests/testFilefuncs.cpp
Comment thread tests/testFilefuncs.cpp
Comment thread libs/common/src/fileFuncs.cpp
@MichalLabuda MichalLabuda requested a review from Flamefire June 29, 2026 09:36
Comment thread libs/common/src/fileFuncs.cpp Outdated
@MichalLabuda MichalLabuda requested a review from Flamefire June 29, 2026 19:53
@Flamefire Flamefire merged commit bb04587 into Return-To-The-Roots:master Jun 30, 2026
@MichalLabuda MichalLabuda deleted the add-filename-validation branch June 30, 2026 12:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants