-
Notifications
You must be signed in to change notification settings - Fork 10
Add reserved name handling and add user filename validation #42
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
dd36255
6383cef
c1773ad
2fc22bd
d37c01e
015b15e
70d869c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,17 +1,24 @@ | ||
| // Copyright (C) 2005 - 2021 Settlers Freaks (sf-team at siedler25.org) | ||
| // Copyright (C) 2005 - 2026 Settlers Freaks (sf-team at siedler25.org) | ||
| // | ||
| // SPDX-License-Identifier: GPL-2.0-or-later | ||
|
|
||
| #pragma once | ||
|
|
||
| #include <string> | ||
|
|
||
| /// Remove all invalid chars of a file or directory name. Result may be empty! | ||
| /// --> bfs::portable_name will return true | ||
| /// Sanitizes to a portable name. Appends '_' to Windows reserved device names. Result may be empty. | ||
| /// --> bfs::portable_name will return true for non-empty results | ||
| std::string makePortableName(const std::string& fileName); | ||
| /// Remove all invalid chars so the name can be used for a file. Result may be empty! | ||
| /// --> bfs::portable_file_name will return true | ||
| /// Sanitizes to a portable filename. Result may be empty. | ||
| /// --> bfs::portable_file_name will return true for non-empty results | ||
| std::string makePortableFileName(const std::string& fileName); | ||
| /// Remove all invalid chars so the name can be used for a directory. Result may be empty! | ||
| /// --> bfs::portable_directory_name will return true | ||
| /// Sanitizes to a portable directory name. Result may be empty. | ||
| /// --> bfs::portable_directory_name will return true for non-empty results | ||
| std::string makePortableDirName(const std::string& fileName); | ||
|
|
||
| /// Returns true if c is valid in a user-provided filename. | ||
| /// Rejects control characters and chars forbidden on Windows (< > : " / \ | ? *). | ||
| bool isValidFileNameChar(char32_t c); | ||
| /// Returns true if fileName is a valid user-provided filename. | ||
| /// Rejects reserved device names, empty names, leading/trailing dots, and trailing spaces. | ||
| bool isValidFileName(const std::string& fileName); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,16 +1,31 @@ | ||
| // Copyright (C) 2005 - 2021 Settlers Freaks (sf-team at siedler25.org) | ||
| // Copyright (C) 2005 - 2026 Settlers Freaks (sf-team at siedler25.org) | ||
| // | ||
| // SPDX-License-Identifier: GPL-2.0-or-later | ||
|
|
||
| #include "fileFuncs.h" | ||
| #include "s25util/strAlgos.h" | ||
| #include "s25util/utf8.h" | ||
| #include <boost/filesystem/path.hpp> | ||
| #include <algorithm> | ||
| #include <array> | ||
|
|
||
| namespace bfs = boost::filesystem; | ||
|
|
||
| // Windows reserved device names | ||
| static constexpr std::array reservedNames{"con", "prn", "aux", "nul", "com0", "com1", "com2", "com3", | ||
| "com4", "com5", "com6", "com7", "com8", "com9", "lpt0", "lpt1", | ||
| "lpt2", "lpt3", "lpt4", "lpt5", "lpt6", "lpt7", "lpt8", "lpt9"}; | ||
|
|
||
| static bool isReservedName(const std::string& name) | ||
| { | ||
| const std::string lower = s25util::toLower(name); | ||
| return std::find(reservedNames.begin(), reservedNames.end(), lower) != reservedNames.end(); | ||
| } | ||
|
|
||
| std::string makePortableName(const std::string& fileName) | ||
| { | ||
| if(fileName.empty() || bfs::portable_name(fileName)) | ||
| return fileName; | ||
| return isReservedName(fileName) ? fileName + '_' : fileName; | ||
| std::string result; | ||
| result.reserve(fileName.size()); | ||
| for(char c : fileName) | ||
|
|
@@ -30,6 +45,8 @@ std::string makePortableName(const std::string& fileName) | |
| while(!result.empty() && result.back() == '.') | ||
| result.erase(result.end() - 1); | ||
| } | ||
| if(isReservedName(result)) | ||
| result += '_'; | ||
| assert(result.empty() || bfs::portable_name(result)); | ||
| return result; | ||
| } | ||
|
|
@@ -74,3 +91,36 @@ std::string makePortableDirName(const std::string& fileName) | |
| assert(result.empty() || bfs::portable_directory_name(result)); | ||
| return result; | ||
| } | ||
|
|
||
| bool isValidFileNameChar(char32_t c) | ||
| { | ||
| // Reject control characters | ||
| if(c <= 0x1F || c == 0x7F) | ||
| return false; | ||
| // Reject characters forbidden on Windows (the most restrictive platform), | ||
| // which covers all restrictions on Linux, macOS, and Android as well. | ||
| if(c == '<' || c == '>' || c == ':' || c == '"' || c == '/' || c == '\\' || c == '|' || c == '?' || c == '*') | ||
| return false; | ||
| return true; | ||
| } | ||
|
|
||
| bool isValidFileName(const std::string& fileName) | ||
| { | ||
| if(fileName.empty()) | ||
| return false; | ||
| if(s25util::utf8to32(fileName).size() > 255) | ||
| return false; | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added the length check - using Tests cover both ASCII (255/256 'a') and a multibyte case (255/256 repetitions of é) to verify the byte-vs-code-point distinction explicitly. |
||
| if(fileName.front() == '.' || fileName.back() == '.') | ||
| return false; | ||
| // Windows silently strips trailing spaces, which would create a mismatch between | ||
| // the name the user typed and the file actually created on disk. | ||
| if(fileName.back() == ' ') | ||
| return false; | ||
| for(char c : fileName) | ||
| { | ||
| if(!isValidFileNameChar(static_cast<unsigned char>(c))) | ||
| return false; | ||
| } | ||
| // On Windows 7 and earlier the device name is the part before the first dot — "nul.ini" is NUL thus forbidden. | ||
| return !isReservedName(fileName.substr(0, fileName.find('.'))); | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
its completely overengineered.
Use
PathIsValidFileNameon windows, and on linux/macos theoretically (since its posix...): only '/' is forbidden.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What/where is
PathIsValidFileName? Other questions suggest there isn't some WinAPI function for that. Can you add a link?Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm shit the blogpost I found listed it in shlwapi, but its not in there.
But its still overengineered. maybe something simple like
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The goal is to have consistent restrictions across platforms and to avoid potential issues when files are transferred in multiplayer from Linux to Windows, which is the most restrictive one.
isReservedNameis extracted so it can be re-used inmakePortableNameisValidFileNameCharis extracted because it serves a different purpose than isValidFileName - it's planned to be used for real-time character filtering inctrlEdit:As for the proposed alternative: it does the same things as the current implementation just reorganized into a single function using wstring. The logic is equivalent,
wstringwould be inconsistent with the rest of the codebase which uses UTF-8 +char32_t.One subtle difference: the proposed code checks the full name against the reserved list, so
NUL.txtwould pass. The current code checksfileName.substr(0, fileName.find('.'))which correctly rejects it - this is the Windows 7 and earlier behavior mentioned in the comment.One thing that is genuinely missing is a length check - I'll add that.