Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 37 additions & 2 deletions libobs/util/platform.c
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,41 @@ static void erase_ch(struct dstr *str, size_t pos)
*str = new_str;
}

size_t os_strftime_utf8(char *dst, size_t dst_size, const char *format, const struct tm *tm)

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.

As this function's implementation is platform-specific, it should be implemented that way in platform-nix.c and platform-windows.c.

{
if (!dst || !dst_size || !format || !tm)
return 0;

#ifdef _WIN32
/*
* On Windows, strftime() returns strings encoded in the system's
* ANSI codepage, not UTF-8. This causes issues when locale-specific
* format specifiers (like %Z for timezone) contain non-ASCII
* characters (e.g., German "Mitteleuropäische Zeit").
*
* Use wcsftime() to get proper wide characters, then convert to UTF-8.
*/
wchar_t wformat[64];

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.

It is good practice to use os_utf8_to_wcs once with a NULL pointer to have it return the buffer size required for a successful conversion and then call it again with an appropriately sized buffer.

As the format string is UTF-8 encoded by convention, you cannot make any simple assumptions about the required size for the UTF-16 string (as both are variable length). Users are free to combine the format string with any text that is valid UTF-8 with the actual format modifiers, so this has to be reactive to that.

wchar_t wdst[256];

@PatTheMav PatTheMav Feb 3, 2026

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.

The maximum size of the intermediary buffer should depend on the provided size of the destination buffer in dst_size.

The constant 256 just happens to work here because the function is only called by os_generate_formatted_filename in this PR, but any other caller in the future might provide a different buffer size.

Assuming "2 bytes for every byte available in the output" seems reasonable to me: 256 bytes of UTF-16 code points of the ASCII range can take as little as 128 bytes of UTF-8, any characters beyond that will take 2-3 bytes, and characters beyond U+10000 take 4 bytes in both.

So a 2x intermediary buffer will be able to hold at most as much ASCII data as the destination buffer can hold and will easily have enough capacity for any code points that might not even fit into the output buffer after conversion.


size_t format_len = os_utf8_to_wcs(format, 0, wformat, sizeof(wformat) / sizeof(wchar_t));
if (!format_len) {
dst[0] = '\0';
return 0;
}

size_t wlen = wcsftime(wdst, sizeof(wdst) / sizeof(wchar_t), wformat, tm);
if (!wlen) {
dst[0] = '\0';
return 0;
}

return os_wcs_to_utf8(wdst, wlen, dst, dst_size);

@PatTheMav PatTheMav Feb 3, 2026

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.

Both strftime and wcsftime return 0 when the output buffer is not large enough to hold the converted result string (and the state of the output buffer is indeterminate).

When called on POSIX, os_strftime_utf8 will mimic that behaviour, but it won't on Windows, because os_wcs_to_utf8 will not abort conversion when the output buffer is too small, which introduces a non-obvious difference in behaviour between platforms.

And os_wcs_to_utf8 should be called once with a NULL pointer to figure out the size of the converted data and abort if that exceeds the size of the output buffer.

That's because there's not a single "2-to-1" ratio between UTF-16 and UTF-8:

All characters in the Basic Multilingual Plane (BMP) are encoded in 2 bytes in UTF-16, but can take anywhere between 1 to 3 bytes in UTF-8. Characters beyond that always take up 4 bytes in UTF-16 and UTF-8.

So while a 256-byte UTF-16 buffer can potentially hold just 128 bytes worth of ASCII data (see above), even a 128-byte UTF-16 buffer can potentially require more than 128 bytes to properly encode all code points in UTF-8 and would thus exceed the output buffer size provided by os_generate_formatted_filename and the entire function should abort the conversion.

#else
return strftime(dst, dst_size, format, tm);
#endif
}

char *os_generate_formatted_filename(const char *extension, bool space, const char *format)
{
time_t now = time(0);
Expand Down Expand Up @@ -727,9 +762,9 @@ char *os_generate_formatted_filename(const char *extension, bool space, const ch

if (astrcmp_n(cmp, spec[i][0], len) == 0) {
if (strlen(spec[i][1]))
strftime(convert, sizeof(convert), spec[i][1], cur_time);
os_strftime_utf8(convert, sizeof(convert), spec[i][1], cur_time);
else
strftime(convert, sizeof(convert), spec[i][0], cur_time);
os_strftime_utf8(convert, sizeof(convert), spec[i][0], cur_time);

dstr_copy(&c, convert);
if (c.len && valid_string(c.array))
Expand Down
2 changes: 2 additions & 0 deletions libobs/util/platform.h
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ EXPORT int os_safe_replace(const char *target_path, const char *from_path, const

EXPORT char *os_generate_formatted_filename(const char *extension, bool space, const char *format);

EXPORT size_t os_strftime_utf8(char *dst, size_t dst_size, const char *format, const struct tm *tm);

struct os_inhibit_info;
typedef struct os_inhibit_info os_inhibit_t;

Expand Down