From e8ef8f9a02b5a152f4684f7351ca4479e8f96246 Mon Sep 17 00:00:00 2001 From: Henri Kulotie Date: Mon, 29 Sep 2025 23:38:27 +0300 Subject: [PATCH 1/4] frontend,libobs,plugins: Ensure ctype functions use unsigned chars Cast ctype function char parameters to unsigned char to ensure they are in correct range (0 to 255 vs -128 to 127) when used with utf-8 encoding (or extended ascii). Fixes dstr astrcmp* functions when used with utf-8 (or extended ascii) characters, so now they are treated greater than the base ascii and thus sorted after them, not before. --- frontend/OBSApp_Themes.cpp | 6 +++--- libobs/util/cf-lexer.c | 2 +- libobs/util/dstr.c | 12 ++++++------ libobs/util/lexer.c | 20 ++++++++++++-------- plugins/text-freetype2/find-font.c | 4 ++-- 5 files changed, 24 insertions(+), 20 deletions(-) diff --git a/frontend/OBSApp_Themes.cpp b/frontend/OBSApp_Themes.cpp index 7b67a45933aaae..d0c4f72bd39524 100644 --- a/frontend/OBSApp_Themes.cpp +++ b/frontend/OBSApp_Themes.cpp @@ -384,7 +384,7 @@ static vector ParseThemeVariables(const char *themeData) /* Look for a suffix and mark variable as size if it exists */ while (ch < end) { - if (!isdigit(*ch) && !isspace(*ch) && *ch != '.') { + if (!isdigit((unsigned char)*ch) && !isspace((unsigned char)*ch) && *ch != '.') { var.suffix = QString::fromUtf8(ch, end - ch); var.type = OBSThemeVariable::Size; break; @@ -581,14 +581,14 @@ static OBSThemeVariable ParseMathVariable(const QHash const QByteArray utf8 = value.toUtf8(); const char *data = utf8.constData(); - if (isdigit(*data)) { + if (isdigit((unsigned char)*data)) { double f = os_strtod(data); var.type = OBSThemeVariable::Number; var.value = f; const char *dataEnd = data + utf8.size(); while (data < dataEnd) { - if (*data && !isdigit(*data) && *data != '.') { + if (*data && !isdigit((unsigned char)*data) && *data != '.') { var.suffix = QString::fromUtf8(data, dataEnd - data); var.type = OBSThemeVariable::Size; break; diff --git a/libobs/util/cf-lexer.c b/libobs/util/cf-lexer.c index 77ccc361d4a3ef..014e4b57851167 100644 --- a/libobs/util/cf-lexer.c +++ b/libobs/util/cf-lexer.c @@ -71,7 +71,7 @@ static inline void cf_convert_from_escape_literal(char **p_dst, const char **p_s /* oct */ default: - if (isdigit(*src)) { + if (isdigit((unsigned char)*src)) { *(dst++) = (char)strtoul(src, NULL, 8); src += 3; } diff --git a/libobs/util/dstr.c b/libobs/util/dstr.c index 4dc415cb356841..c07b12dbe5b182 100644 --- a/libobs/util/dstr.c +++ b/libobs/util/dstr.c @@ -43,8 +43,8 @@ int astrcmpi(const char *str1, const char *str2) str2 = astrblank; do { - char ch1 = (char)toupper(*str1); - char ch2 = (char)toupper(*str2); + unsigned char ch1 = (unsigned char)toupper((unsigned char)*str1); + unsigned char ch2 = (unsigned char)toupper((unsigned char)*str2); if (ch1 < ch2) return -1; @@ -85,8 +85,8 @@ int astrcmp_n(const char *str1, const char *str2, size_t n) str2 = astrblank; do { - char ch1 = *str1; - char ch2 = *str2; + unsigned char ch1 = (unsigned char)*str1; + unsigned char ch2 = (unsigned char)*str2; if (ch1 < ch2) return -1; @@ -129,8 +129,8 @@ int astrcmpi_n(const char *str1, const char *str2, size_t n) str2 = astrblank; do { - char ch1 = (char)toupper(*str1); - char ch2 = (char)toupper(*str2); + unsigned char ch1 = (unsigned char)toupper((unsigned char)*str1); + unsigned char ch2 = (unsigned char)toupper((unsigned char)*str2); if (ch1 < ch2) return -1; diff --git a/libobs/util/lexer.c b/libobs/util/lexer.c index a69634c154fd0a..cada3ba758959d 100644 --- a/libobs/util/lexer.c +++ b/libobs/util/lexer.c @@ -29,7 +29,8 @@ int strref_cmp(const struct strref *str1, const char *str2) str2 = astrblank; do { - char ch1, ch2; + unsigned char ch1; + unsigned char ch2; ch1 = (i < str1->len) ? str1->array[i] : 0; ch2 = *str2; @@ -53,10 +54,11 @@ int strref_cmpi(const struct strref *str1, const char *str2) str2 = astrblank; do { - char ch1, ch2; + unsigned char ch1; + unsigned char ch2; - ch1 = (i < str1->len) ? (char)toupper(str1->array[i]) : 0; - ch2 = (char)toupper(*str2); + ch1 = (i < str1->len) ? (unsigned char)toupper((unsigned char)str1->array[i]) : 0; + ch2 = (unsigned char)toupper((unsigned char)*str2); if (ch1 < ch2) return -1; @@ -77,7 +79,8 @@ int strref_cmp_strref(const struct strref *str1, const struct strref *str2) return -1; do { - char ch1, ch2; + unsigned char ch1; + unsigned char ch2; ch1 = (i < str1->len) ? str1->array[i] : 0; ch2 = (i < str2->len) ? str2->array[i] : 0; @@ -103,10 +106,11 @@ int strref_cmpi_strref(const struct strref *str1, const struct strref *str2) return -1; do { - char ch1, ch2; + unsigned char ch1; + unsigned char ch2; - ch1 = (i < str1->len) ? (char)toupper(str1->array[i]) : 0; - ch2 = (i < str2->len) ? (char)toupper(str2->array[i]) : 0; + ch1 = (i < str1->len) ? (unsigned char)toupper((unsigned char)str1->array[i]) : 0; + ch2 = (i < str2->len) ? (unsigned char)toupper((unsigned char)str2->array[i]) : 0; if (ch1 < ch2) return -1; diff --git a/plugins/text-freetype2/find-font.c b/plugins/text-freetype2/find-font.c index 79312528ad1d9a..217e2f0165abd8 100644 --- a/plugins/text-freetype2/find-font.c +++ b/plugins/text-freetype2/find-font.c @@ -333,8 +333,8 @@ static inline size_t get_rating(struct font_path_info *info, struct dstr *cmp) size_t num = 0; do { - char ch1 = (char)toupper(*src); - char ch2 = (char)toupper(*dst); + unsigned char ch1 = (unsigned char)toupper((unsigned char)*src); + unsigned char ch2 = (unsigned char)toupper((unsigned char)*dst); if (ch1 != ch2) break; From 4db8b5a07b261f9ffeed9cf73c543739938ac336 Mon Sep 17 00:00:00 2001 From: Henri Kulotie Date: Mon, 29 Sep 2025 23:39:04 +0300 Subject: [PATCH 2/4] logging: Use ISO 8601 time format for logging Switch locale-aware timestamping for logging / crash handling to %H:%M:%S Update frontend/OBSApp.cpp Co-authored-by: Patrick Heyer Update libobs/obs-win-crash-handler.c Co-authored-by: Patrick Heyer --- frontend/OBSApp.cpp | 2 +- libobs/obs-win-crash-handler.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/OBSApp.cpp b/frontend/OBSApp.cpp index 0d6d0e28e35fbd..86fd164a9a4902 100644 --- a/frontend/OBSApp.cpp +++ b/frontend/OBSApp.cpp @@ -282,7 +282,7 @@ string CurrentDateTimeString() struct tm tstruct; char buf[80]; tstruct = *localtime(&now); - strftime(buf, sizeof(buf), "%Y-%m-%d, %X", &tstruct); + strftime(buf, sizeof(buf), "%Y-%m-%d, %H:%M:%S", &tstruct); return buf; } diff --git a/libobs/obs-win-crash-handler.c b/libobs/obs-win-crash-handler.c index 664a9c6eb12305..befc3843a8f391 100644 --- a/libobs/obs-win-crash-handler.c +++ b/libobs/obs-win-crash-handler.c @@ -245,7 +245,7 @@ static inline void write_header(struct exception_handler_data *data) time_t now = time(0); struct tm ts; ts = *localtime(&now); - strftime(date_time, sizeof(date_time), "%Y-%m-%d, %X", &ts); + strftime(date_time, sizeof(date_time), "%Y-%m-%d, %H:%M:%S", &ts); const char *obs_bitness; if (sizeof(void *) == 8) From dc3d449159d9db6e88581ff779c079695d8fe288 Mon Sep 17 00:00:00 2001 From: Henri Kulotie Date: Sun, 8 Feb 2026 19:22:31 +0200 Subject: [PATCH 3/4] frontend: Add utf-8 active code page manifest Declaring Utf-8 as active code page in manifest makes Win32 API use utf-8 instead of ANSI codepages when using the "A" versions of functions. Manifest declaration also encodes command line arguments as utf8. This allows for example --profile to load profiles with special characters. --- frontend/cmake/windows/obs.manifest | 5 +++++ libobs/obs.h | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/frontend/cmake/windows/obs.manifest b/frontend/cmake/windows/obs.manifest index c5d4297d945860..4e309f7b7778ea 100644 --- a/frontend/cmake/windows/obs.manifest +++ b/frontend/cmake/windows/obs.manifest @@ -11,6 +11,11 @@ + + + UTF-8 + + diff --git a/libobs/obs.h b/libobs/obs.h index 4e51eb4488a0ca..a7b5319fbe6560 100644 --- a/libobs/obs.h +++ b/libobs/obs.h @@ -382,8 +382,11 @@ EXPORT const char *obs_get_version_string(void); * and safely copies argv/argc from main(). Subsequent calls do nothing. * * @param argc The count of command line arguments, from main() - * @param argv An array of command line arguments, copied from main() and ends + * @param argv An array of command line arguments in UTF-8, copied from main() and ends * with NULL. + * + * On Windows this requires that active code page is UTF-8. + * For example as a declaration in obs.manifest. */ EXPORT void obs_set_cmdline_args(int argc, const char *const *argv); From ddc3398c5077c1abdcf53c5be24296b1b8df58be Mon Sep 17 00:00:00 2001 From: Henri Kulotie Date: Sat, 13 Sep 2025 20:59:13 +0300 Subject: [PATCH 4/4] frontend: Use system locale instead of 'C' Sets runtime locale to system locale with UTF-8 codepage. This is already default behavior on unix, but Windows defaults to minimal 'C' locale. Use CRT locale for C++ std::locale default OBS Studio language settings no longer change QLocale default locale, instead system locale is used for conformity. It is likely this is what user wants as well. Ie. sorting and formatting functions should follow OS locale instead of OBS Studio language (which also lacks country information). --- frontend/OBSApp.cpp | 17 -------------- frontend/obs-main.cpp | 54 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 17 deletions(-) diff --git a/frontend/OBSApp.cpp b/frontend/OBSApp.cpp index 86fd164a9a4902..a3a6d480c17563 100644 --- a/frontend/OBSApp.cpp +++ b/frontend/OBSApp.cpp @@ -724,11 +724,6 @@ bool OBSApp::InitLocale() locale = lang; - // set basic default application locale - if (!locale.empty()) { - QLocale::setDefault(QLocale(QString::fromStdString(locale).replace('-', '_'))); - } - string englishPath; if (!GetDataFilePath("locale/" DEFAULT_LANG ".ini", englishPath)) { OBSErrorBox(NULL, "Failed to find locale/" DEFAULT_LANG ".ini"); @@ -768,11 +763,6 @@ bool OBSApp::InitLocale() blog(LOG_INFO, "Using preferred locale '%s'", locale_.c_str()); locale = locale_; - // set application default locale to the new chosen one - if (!locale.empty()) { - QLocale::setDefault(QLocale(QString::fromStdString(locale).replace('-', '_'))); - } - return true; } @@ -914,13 +904,6 @@ OBSApp::OBSApp(int &argc, char **argv, profiler_name_store_t *store) { installNativeEventFilter(new OBS::NativeEventFilter); - /* fix float handling */ -#if defined(Q_OS_UNIX) - if (!setlocale(LC_NUMERIC, "C")) { - blog(LOG_WARNING, "Failed to set LC_NUMERIC to C locale"); - } -#endif - #ifndef _WIN32 // Add POSIX signal handlers: // * SIGINT diff --git a/frontend/obs-main.cpp b/frontend/obs-main.cpp index c2532416bfbecc..dfbd52c98d7dca 100644 --- a/frontend/obs-main.cpp +++ b/frontend/obs-main.cpp @@ -45,6 +45,7 @@ #include #define WIN32_LEAN_AND_MEAN #include +#include #else #include #endif @@ -77,6 +78,7 @@ bool opt_disable_missing_files_check = false; string opt_starting_collection; string opt_starting_profile; string opt_starting_scene; +bool using_utf8 = false; bool restart = false; bool restart_safe = false; @@ -416,6 +418,42 @@ static void create_log_file(fstream &logFile) } } +static bool set_utf8_locale(void) +{ + // Use system locale with UTF-8 codepage (available from Windows 10 version 1803) + bool usingUTF8 = !!setlocale(LC_ALL, ".UTF-8"); + + /* + Fallback to minimal C locale + Could use "" for system defaults, but the Windows default ANSI codepages (such as .125x or .9xx) + mismatch with UTF-8 that is assumed by many parts of the codebase. "C" only covers ASCII, so no mismatches. + */ + usingUTF8 = usingUTF8 || !!setlocale(LC_ALL, "C.UTF-8"); + +#ifdef _WIN32 + usingUTF8 = usingUTF8 && (_setmbcp(CP_UTF8) == 0); +#endif + if (!usingUTF8) { + setlocale(LC_ALL, "C"); + } + + // fix float handling + setlocale(LC_NUMERIC, "C"); + + // Copy C runtime locale for C++ + std::locale defaultLocale(setlocale(LC_ALL, nullptr)); + std::locale::global(defaultLocale); + + /* + system() is already the QLocale default, but just to be explicit about the intention. + Unlike CRT and C++ locales above, QLocale doesn't support customization of locale categories and codepages. + Ie. We can't enforce decimal point to be a dot and at the same time use user's preferred locale. + */ + QLocale::setDefault(QLocale::system()); + + return usingUTF8; +} + static auto ProfilerNameStoreRelease = [](profiler_name_store_t *store) { profiler_name_store_free(store); }; @@ -533,6 +571,12 @@ static int run_program(fstream &logFile, int argc, char *argv[]) qputenv("QT_NO_SUBTRACTOPAQUESIBLINGS", "1"); OBSApp program(argc, argv, profilerNameStore.get()); + +#if defined(Q_OS_UNIX) + // OBSApp (QApplication) constructor overwrote the locale for unix, so set it again + set_utf8_locale(); +#endif + try { QAccessible::installFactory(accessibleFactory); QFontDatabase::addApplicationFont(":/fonts/OpenSans-Regular.ttf"); @@ -683,6 +727,15 @@ static int run_program(fstream &logFile, int argc, char *argv[]) blog(LOG_INFO, "Command Line Arguments: %s", stor.str().c_str()); } + if (!using_utf8) { + blog(LOG_DEBUG, "UTF-8 locale is not available on this OS version. " + "Skipping regional formatting and sorting."); + } + + // Use collation (sorting rules) as indicator of regional settings + blog(LOG_DEBUG, "Locale: %s", setlocale(LC_COLLATE, nullptr)); + blog(LOG_DEBUG, "App language: %s", program.GetLocale()); + if (!program.OBSInit()) { return 0; } @@ -903,6 +956,7 @@ static void set_process_mitigation_policies() int main(int argc, char *argv[]) { + using_utf8 = set_utf8_locale(); #ifndef _WIN32 using SignalHandlerCallback = decltype(OBSApp::sigIntSignalHandler);