From 1cdde2cab6ecb78f4b47e8f2879d571f6abdd2d7 Mon Sep 17 00:00:00 2001 From: Stenzek Date: Fri, 12 Sep 2025 23:27:54 +1000 Subject: [PATCH] Path: Add IsFileNameValid() --- src/common-tests/path_tests.cpp | 30 ++++++++++++++++++++++++++++++ src/common/file_system.cpp | 24 ++++++++++++++++++++++++ src/common/path.h | 3 +++ 3 files changed, 57 insertions(+) diff --git a/src/common-tests/path_tests.cpp b/src/common-tests/path_tests.cpp index 45c23196b..693b4a8f6 100644 --- a/src/common-tests/path_tests.cpp +++ b/src/common-tests/path_tests.cpp @@ -6,6 +6,10 @@ #include +#include + +using namespace std::string_view_literals; + TEST(Path, ToNativePath) { ASSERT_EQ(Path::ToNativePath(""), ""); @@ -234,6 +238,7 @@ TEST(Path, SanitizeFileName) ASSERT_EQ(Path::SanitizeFileName("abcdefghijlkmnopqrstuvwxyz-0123456789+&=_[]{}"), "abcdefghijlkmnopqrstuvwxyz-0123456789+&=_[]{}"); ASSERT_EQ(Path::SanitizeFileName("some*path**with*asterisks"), "some_path__with_asterisks"); + ASSERT_EQ(Path::SanitizeFileName("foo\0bar"sv), "foo_bar"); #ifdef _WIN32 ASSERT_EQ(Path::SanitizeFileName("foo:"), "foo_"); ASSERT_EQ(Path::SanitizeFileName("foo:bar."), "foo_bar_"); @@ -244,6 +249,31 @@ TEST(Path, SanitizeFileName) ASSERT_EQ(Path::SanitizeFileName("foo/bar", false), "foo/bar"); } +TEST(Path, IsFileNameValid) +{ + ASSERT_TRUE(Path::IsFileNameValid("foo"sv)); + ASSERT_TRUE(Path::IsFileNameValid("foo_bar-0123456789+&=_[]{}"sv)); + ASSERT_TRUE(Path::IsFileNameValid("f🙃o"sv)); + ASSERT_TRUE(Path::IsFileNameValid("ŻąłóРстуぬねのはen🍪⟑η∏☉ⴤℹ︎∩₲ ₱⟑♰⫳🐱"sv)); + ASSERT_TRUE(Path::IsFileNameValid("foo/bar"sv, true)); + ASSERT_TRUE(Path::IsFileNameValid("foo\\bar"sv, true)); + ASSERT_FALSE(Path::IsFileNameValid("foo/bar"sv)); + ASSERT_FALSE(Path::IsFileNameValid("foo\0bar"sv)); + ASSERT_FALSE(Path::IsFileNameValid("foo\nbar"sv)); +#ifdef _WIN32 + ASSERT_FALSE(Path::IsFileNameValid("foo\\bar"sv)); + ASSERT_FALSE(Path::IsFileNameValid("foo:bar"sv)); + ASSERT_FALSE(Path::IsFileNameValid("foo*bar"sv)); + ASSERT_FALSE(Path::IsFileNameValid("foo?bar"sv)); + ASSERT_FALSE(Path::IsFileNameValid("foo\"bar"sv)); + ASSERT_FALSE(Path::IsFileNameValid("foobar"sv)); + ASSERT_FALSE(Path::IsFileNameValid("foo|bar"sv)); + ASSERT_FALSE(Path::IsFileNameValid("foobar.txt."sv)); + ASSERT_FALSE(Path::IsFileNameValid("foobar."sv)); +#endif +} + TEST(Path, RemoveLengthLimits) { #ifdef _WIN32 diff --git a/src/common/file_system.cpp b/src/common/file_system.cpp index e8735d2bb..4d4ed95c9 100644 --- a/src/common/file_system.cpp +++ b/src/common/file_system.cpp @@ -67,6 +67,10 @@ static bool IsUNCPath(const T& path) static inline bool FileSystemCharacterIsSane(char32_t c, bool strip_slashes) { + // no null bytes + if (c == 0) + return false; + #ifdef _WIN32 // https://docs.microsoft.com/en-gb/windows/win32/fileio/naming-a-file?redirectedfrom=MSDN#naming-conventions if ((c == U'/' || c == U'\\') && strip_slashes) @@ -149,6 +153,26 @@ void Path::SanitizeFileName(std::string* str, bool strip_slashes /* = true */) #endif } +bool Path::IsFileNameValid(std::string_view str, bool allow_slashes) +{ + size_t pos = 0; + while (pos < str.length()) + { + char32_t ch; + pos += StringUtil::DecodeUTF8(str, pos, &ch); + if (!FileSystemCharacterIsSane(ch, !allow_slashes)) + return false; + } + +#ifdef _WIN32 + // Windows: Can't end filename with a period. + if (str.length() > 0 && str.back() == '.') + return false; +#endif + + return true; +} + std::string Path::RemoveLengthLimits(std::string_view str) { std::string ret; diff --git a/src/common/path.h b/src/common/path.h index 9511a7a1c..cf781476c 100644 --- a/src/common/path.h +++ b/src/common/path.h @@ -36,6 +36,9 @@ void Canonicalize(std::string* path); std::string SanitizeFileName(std::string_view str, bool strip_slashes = true); void SanitizeFileName(std::string* str, bool strip_slashes = true); +/// Returns true if the given filename contains any invalid characters. +bool IsFileNameValid(std::string_view str, bool allow_slashes = false); + /// Mutates the path to remove any MAX_PATH limits (for Windows). std::string RemoveLengthLimits(std::string_view str); void RemoveLengthLimits(std::string* path);