From c0f8e5f482a1feb1243d53f1ebb587b6876d3ab4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Wed, 8 Apr 2026 05:00:00 +0000 Subject: [PATCH 01/24] SetConsoleOutputCP --- nob.c | 9 --------- nob.h | 1 + 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/nob.c b/nob.c index 58c0500..f8925c4 100644 --- a/nob.c +++ b/nob.c @@ -162,15 +162,6 @@ void print_available_commands(Commands commands) } } -#if defined(_WIN32) && defined(_MSC_VER) && defined(__cplusplus) -// TODO: I don't know why, but when you compile nob.c with -// cl.exe /std:c++20 /TP nob.c -// It just can't find the declaration of SetConsoleOutputCP(). -// This is probably something about how we include windows.h in nob.h -extern "C" { - WINBASEAPI BOOL WINAPI SetConsoleOutputCP(_In_ UINT wCodePageID); -} -#endif int main(int argc, char **argv) { diff --git a/nob.h b/nob.h index 2735a93..4eb3110 100644 --- a/nob.h +++ b/nob.h @@ -164,6 +164,7 @@ # include # include # include +# include #else # ifdef __APPLE__ # include From 3613693311eaa9c222f81036a3e5605a6bcf1e7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Wed, 8 Apr 2026 05:01:00 +0000 Subject: [PATCH 02/24] nob__unicode_utf8_to_unicode_utf16_temp --- nob.h | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/nob.h b/nob.h index 4eb3110..fd27ff4 100644 --- a/nob.h +++ b/nob.h @@ -977,6 +977,40 @@ NOBDEF void nob__cmd_append(Nob_Cmd *cmd, size_t n, const char **args) } } +#ifdef _WIN32 + + +wchar_t* nob__unicode_utf8_to_unicode_utf16_temp(const char* narrow_str) +{ + int wide_len_a; + DWORD err; + int wide_len_b; + wchar_t* wide_str; + + NOB_ASSERT(narrow_str); + wide_len_a = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, narrow_str, -1, NULL, 0); + if (wide_len_a == 0) { + err = GetLastError(); + (void)err; + return NULL; + } + NOB_ASSERT(wide_len_a >= 1); + wide_str = (wchar_t*)nob_temp_alloc(wide_len_a * sizeof(wchar_t)); + NOB_ASSERT(wide_str); + wide_len_b = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, narrow_str, -1, wide_str, wide_len_a); + if (wide_len_b == 0) { + err = GetLastError(); + (void)err; + return NULL; + } + NOB_ASSERT(wide_len_b == wide_len_a); + return wide_str; +} + + +#endif // _WIN32 + + #ifdef _WIN32 // Base on https://stackoverflow.com/a/75644008 From 1f246f7e861f4339d8c935531863fff495593b00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Wed, 8 Apr 2026 05:02:00 +0000 Subject: [PATCH 03/24] nob__unicode_utf16_to_unicode_utf8 --- nob.h | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/nob.h b/nob.h index fd27ff4..02d6299 100644 --- a/nob.h +++ b/nob.h @@ -1007,6 +1007,24 @@ wchar_t* nob__unicode_utf8_to_unicode_utf16_temp(const char* narrow_str) return wide_str; } +int nob__unicode_utf16_to_unicode_utf8(const wchar_t* wide_str, int wide_len, char* narrow_str, int narrow_capacity) +{ + int narrow_len; + DWORD err; + + NOB_ASSERT(wide_str); + NOB_ASSERT(wide_len >= 1); + NOB_ASSERT(narrow_str); + NOB_ASSERT(narrow_capacity >= 1); + + narrow_len = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, wide_str, wide_len, narrow_str, narrow_capacity, NULL, NULL); + if (narrow_len == 0) { + err = GetLastError(); + (void)err; + } + return narrow_len; +} + #endif // _WIN32 From 5abeea3aa10d8127c7d4270683e8b5620ccb1c14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Wed, 8 Apr 2026 05:03:00 +0000 Subject: [PATCH 04/24] nob__worst_case_utf16_to_utf8 --- nob.h | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/nob.h b/nob.h index 02d6299..fe6e098 100644 --- a/nob.h +++ b/nob.h @@ -308,6 +308,17 @@ NOBDEF bool nob_walk_dir_opt(const char *root, Nob_Walk_Func func, Nob_Walk_Dir_ #define nob_walk_dir(root, func, ...) nob_walk_dir_opt((root), (func), NOB_CLIT(Nob_Walk_Dir_Opt){__VA_ARGS__}) +#ifdef _WIN32 +/* +UTF-16 is variable length encoding. Every code point could be encoded by 1 or 2 UTF-16 code units. Every UTF-16 code units is two bytes. +UTF-8 is variable length encoding. Every code point could be encoded by 1, 2, 3 or 4 UTF-8 code units. Every UTF-8 code units is one byte. +In the worst case, single UTF-16 code unit code point could be encoded by 3 UTF-8 code units. Code points from U+0800 to U+FFFF. Meaning from 1 wchar_t to 3 char. +In the worst case, doble UTF-16 code unit code point could be encoded by 4 UTF-8 code units. Code points from U+010000 to U+10FFFF. Meaning from 2 wchar_t to 4 char. +Given the rules above, we need to allcoate 3 char for each 1 wchar_t in order to be able to represent any sequecne of any code points. +*/ +#define nob__worst_case_utf16_to_utf8(count) (3 * (count)) +#endif // _WIN32 + typedef struct { char *name; bool error; From e37e640d7e1c61df89587302d067327dea758af5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Wed, 8 Apr 2026 05:04:00 +0000 Subject: [PATCH 05/24] CopyFileW --- nob.h | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/nob.h b/nob.h index fe6e098..ed0fcd1 100644 --- a/nob.h +++ b/nob.h @@ -1167,11 +1167,21 @@ NOBDEF bool nob_copy_file(const char *src_path, const char *dst_path) nob_log(NOB_INFO, "copying %s -> %s", src_path, dst_path); #endif // NOB_NO_ECHO #ifdef _WIN32 - if (!CopyFile(src_path, dst_path, FALSE)) { + bool ret; + size_t mark; + wchar_t* wide_src_path; + wchar_t* wide_dst_path; + + ret = true; + mark = nob_temp_save(); + wide_src_path = nob__unicode_utf8_to_unicode_utf16_temp(src_path); + wide_dst_path = nob__unicode_utf8_to_unicode_utf16_temp(dst_path); + if (!CopyFileW(wide_src_path, wide_dst_path, FALSE)) { nob_log(NOB_ERROR, "Could not copy file: %s", nob_win32_error_message(GetLastError())); - return false; + ret = false; } - return true; + nob_temp_rewind(mark); + return ret; #else int src_fd = -1; int dst_fd = -1; From f5924eb749ec5d4f03e8879d1c9014bbc61201ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Wed, 8 Apr 2026 05:05:00 +0000 Subject: [PATCH 06/24] CreateProcessW --- nob.h | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/nob.h b/nob.h index ed0fcd1..0939f9d 100644 --- a/nob.h +++ b/nob.h @@ -1547,9 +1547,9 @@ static Nob_Proc nob__cmd_start_process(Nob_Cmd cmd, Nob_Fd *fdin, Nob_Fd *fdout, #ifdef _WIN32 // https://docs.microsoft.com/en-us/windows/win32/procthread/creating-a-child-process-with-redirected-input-and-output - STARTUPINFO siStartInfo; + STARTUPINFOW siStartInfo; ZeroMemory(&siStartInfo, sizeof(siStartInfo)); - siStartInfo.cb = sizeof(STARTUPINFO); + siStartInfo.cb = sizeof(siStartInfo); // NOTE: theoretically setting NULL to std handles should not be a problem // https://docs.microsoft.com/en-us/windows/console/getstdhandle?redirectedfrom=MSDN#attachdetach-behavior // TODO: check for errors in GetStdHandle @@ -1564,7 +1564,10 @@ static Nob_Proc nob__cmd_start_process(Nob_Cmd cmd, Nob_Fd *fdin, Nob_Fd *fdout, Nob_String_Builder quoted = {0}; nob__win32_cmd_quote(cmd, "ed); nob_sb_append_null("ed); - BOOL bSuccess = CreateProcessA(NULL, quoted.items, NULL, NULL, TRUE, 0, NULL, NULL, &siStartInfo, &piProcInfo); + size_t mark = nob_temp_save(); + wchar_t* wide_command_line = nob__unicode_utf8_to_unicode_utf16_temp(quoted.items); + BOOL bSuccess = CreateProcessW(NULL, wide_command_line, NULL, NULL, TRUE, 0, NULL, NULL, &siStartInfo, &piProcInfo); + nob_temp_rewind(mark); nob_sb_free(quoted); if (!bSuccess) { From 4956754818a498bdbb288349ea6a230cb2c0f5cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Wed, 8 Apr 2026 05:06:00 +0000 Subject: [PATCH 07/24] FindFirstFileW+FindNextFileW --- nob.h | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/nob.h b/nob.h index 0939f9d..6ddd6e5 100644 --- a/nob.h +++ b/nob.h @@ -319,13 +319,20 @@ Given the rules above, we need to allcoate 3 char for each 1 wchar_t in order to #define nob__worst_case_utf16_to_utf8(count) (3 * (count)) #endif // _WIN32 +#ifdef _WIN32 +typedef struct { + WIN32_FIND_DATAW win_find_data; + char utf8_file_name[nob__worst_case_utf16_to_utf8(MAX_PATH)]; +} nob__win32_find_dataw; +#endif // _WIN32 + typedef struct { char *name; bool error; struct { #ifdef _WIN32 - WIN32_FIND_DATA win32_data; + nob__win32_find_dataw find_data; HANDLE win32_hFind; bool win32_init; #else @@ -2036,8 +2043,9 @@ NOBDEF bool nob_dir_entry_open(const char *dir_path, Nob_Dir_Entry *dir) memset(dir, 0, sizeof(*dir)); #ifdef _WIN32 size_t temp_mark = nob_temp_save(); - char *buffer = nob_temp_sprintf("%s\\*", dir_path); - dir->nob__private.win32_hFind = FindFirstFile(buffer, &dir->nob__private.win32_data); + char *narrow_path = nob_temp_sprintf("%s\\*", dir_path); + wchar_t *wide_path = nob__unicode_utf8_to_unicode_utf16_temp(narrow_path); + dir->nob__private.win32_hFind = FindFirstFileW(wide_path, &dir->nob__private.find_data.win_find_data); nob_temp_rewind(temp_mark); if (dir->nob__private.win32_hFind == INVALID_HANDLE_VALUE) { @@ -2045,6 +2053,12 @@ NOBDEF bool nob_dir_entry_open(const char *dir_path, Nob_Dir_Entry *dir) dir->error = true; return false; } + + const wchar_t *wide_name = dir->nob__private.find_data.win_find_data.cFileName; + char *narrow_name = dir->nob__private.find_data.utf8_file_name; + int narrow_cap = NOB_ARRAY_LEN(dir->nob__private.find_data.utf8_file_name); + int narrow_len = nob__unicode_utf16_to_unicode_utf8(wide_name, (int)wcslen(wide_name) + 1, narrow_name, narrow_cap) - 1; + (void)narrow_len; #else dir->nob__private.posix_dir = opendir(dir_path); if (dir->nob__private.posix_dir == NULL) { @@ -2061,17 +2075,22 @@ NOBDEF bool nob_dir_entry_next(Nob_Dir_Entry *dir) #ifdef _WIN32 if (!dir->nob__private.win32_init) { dir->nob__private.win32_init = true; - dir->name = dir->nob__private.win32_data.cFileName; + dir->name = dir->nob__private.find_data.utf8_file_name; return true; } - if (!FindNextFile(dir->nob__private.win32_hFind, &dir->nob__private.win32_data)) { + if (!FindNextFileW(dir->nob__private.win32_hFind, &dir->nob__private.find_data.win_find_data)) { if (GetLastError() == ERROR_NO_MORE_FILES) return false; nob_log(NOB_ERROR, "Could not read next directory entry: %s", nob_win32_error_message(GetLastError())); dir->error = true; return false; } - dir->name = dir->nob__private.win32_data.cFileName; + const wchar_t *wide_name = dir->nob__private.find_data.win_find_data.cFileName; + char *narrow_name = dir->nob__private.find_data.utf8_file_name; + int narrow_cap = NOB_ARRAY_LEN(dir->nob__private.find_data.utf8_file_name); + int narrow_len = nob__unicode_utf16_to_unicode_utf8(wide_name, (int)wcslen(wide_name) + 1, narrow_name, narrow_cap); + (void)narrow_len; + dir->name = narrow_name; #else errno = 0; dir->nob__private.posix_ent = readdir(dir->nob__private.posix_dir); From cec6a1dc28c306e9fd1d8490ff021b1058be9f0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Wed, 8 Apr 2026 05:07:00 +0000 Subject: [PATCH 08/24] CreateFileW --- nob.h | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/nob.h b/nob.h index 6ddd6e5..3240de7 100644 --- a/nob.h +++ b/nob.h @@ -1677,14 +1677,17 @@ NOBDEF Nob_Fd nob_fd_open_for_read(const char *path) saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); saAttr.bInheritHandle = TRUE; - Nob_Fd result = CreateFile( - path, + size_t mark = nob_temp_save(); + wchar_t *wide_path = nob__unicode_utf8_to_unicode_utf16_temp(path); + Nob_Fd result = CreateFileW( + wide_path, GENERIC_READ, 0, &saAttr, OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, NULL); + nob_temp_rewind(mark); if (result == INVALID_HANDLE_VALUE) { nob_log(NOB_ERROR, "Could not open file %s: %s", path, nob_win32_error_message(GetLastError())); @@ -1711,8 +1714,10 @@ NOBDEF Nob_Fd nob_fd_open_for_write(const char *path) saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); saAttr.bInheritHandle = TRUE; - Nob_Fd result = CreateFile( - path, // name of the write + size_t mark = nob_temp_save(); + wchar_t *wide_path = nob__unicode_utf8_to_unicode_utf16_temp(path); + Nob_Fd result = CreateFileW( + wide_path, // name of the write GENERIC_WRITE, // open for writing 0, // do not share &saAttr, // default security @@ -1720,6 +1725,7 @@ NOBDEF Nob_Fd nob_fd_open_for_write(const char *path) FILE_ATTRIBUTE_NORMAL, // normal file NULL // no attr. template ); + nob_temp_rewind(mark); if (result == INVALID_HANDLE_VALUE) { nob_log(NOB_ERROR, "Could not open file %s: %s", path, nob_win32_error_message(GetLastError())); From 70f08f4debe391dc52091e8da4fff2fe1eeef11e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Wed, 8 Apr 2026 05:08:00 +0000 Subject: [PATCH 09/24] CreateDirectoryW --- nob.h | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/nob.h b/nob.h index 3240de7..3735192 100644 --- a/nob.h +++ b/nob.h @@ -1147,10 +1147,42 @@ static char nob_temp[NOB_TEMP_CAPACITY] = {0}; NOBDEF bool nob_mkdir_if_not_exists(const char *path) { #ifdef _WIN32 - int result = _mkdir(path); + size_t mark; + wchar_t *wide_path; + BOOL b; + DWORD err; + + mark = nob_temp_save(); + wide_path = nob__unicode_utf8_to_unicode_utf16_temp(path); + b = CreateDirectoryW(wide_path, NULL); + err = GetLastError(); + nob_temp_rewind(mark); + if(b != 0) + { + #ifndef NOB_NO_ECHO + nob_log(NOB_INFO, "created directory `%s`", path); + #endif // NOB_NO_ECHO + return true; + } + else if(b == 0 && err == ERROR_ALREADY_EXISTS) + { + #ifndef NOB_NO_ECHO + nob_log(NOB_INFO, "directory `%s` already exists", path); + #endif // NOB_NO_ECHO + return true; + } + else if(b == 0 && err == ERROR_PATH_NOT_FOUND) + { + NOB_TODO("One or more intermediate directories do not exist; this function will only create the final directory in the path."); + return false; + } + else + { + nob_log(NOB_ERROR, "Could not create directory: %s", nob_win32_error_message(err)); + return false; + } #else int result = mkdir(path, 0755); -#endif if (result < 0) { if (errno == EEXIST) { #ifndef NOB_NO_ECHO @@ -1166,6 +1198,7 @@ NOBDEF bool nob_mkdir_if_not_exists(const char *path) nob_log(NOB_INFO, "created directory `%s`", path); #endif // NOB_NO_ECHO return true; +#endif // _WIN32 } NOBDEF bool nob_copy_file(const char *src_path, const char *dst_path) From 2066e42d4a518ab718324b236426d3513d9962ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Wed, 8 Apr 2026 05:09:00 +0000 Subject: [PATCH 10/24] RemoveDirectoryW+DeleteFileW --- nob.h | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/nob.h b/nob.h index 3735192..893131a 100644 --- a/nob.h +++ b/nob.h @@ -2335,21 +2335,34 @@ NOBDEF bool nob_delete_file(const char *path) nob_log(NOB_INFO, "deleting %s", path); #endif // NOB_NO_ECHO #ifdef _WIN32 + bool ret; + size_t mark; + wchar_t *wide_path; Nob_File_Type type = nob_get_file_type(path); switch (type) { case NOB_FILE_DIRECTORY: - if (!RemoveDirectoryA(path)) { + ret = true; + mark = nob_temp_save(); + wide_path = nob__unicode_utf8_to_unicode_utf16_temp(path); + if (!RemoveDirectoryW(wide_path)) { nob_log(NOB_ERROR, "Could not delete directory %s: %s", path, nob_win32_error_message(GetLastError())); - return false; + ret = false; } + nob_temp_rewind(mark); + return ret; break; case NOB_FILE_REGULAR: case NOB_FILE_SYMLINK: case NOB_FILE_OTHER: - if (!DeleteFileA(path)) { + ret = true; + mark = nob_temp_save(); + wide_path = nob__unicode_utf8_to_unicode_utf16_temp(path); + if (!DeleteFileW(wide_path)) { nob_log(NOB_ERROR, "Could not delete file %s: %s", path, nob_win32_error_message(GetLastError())); - return false; + ret = false; } + nob_temp_rewind(mark); + return ret; break; default: NOB_UNREACHABLE("Nob_File_Type"); } From 6506327b1ffa7383c4e41cd6e499f7b6f35380f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Wed, 8 Apr 2026 05:10:00 +0000 Subject: [PATCH 11/24] nob_needs_rebuild --- nob.h | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/nob.h b/nob.h index 893131a..9b1187b 100644 --- a/nob.h +++ b/nob.h @@ -2519,9 +2519,10 @@ NOBDEF const char *nob_temp_sv_to_cstr(Nob_String_View sv) NOBDEF int nob_needs_rebuild(const char *output_path, const char **input_paths, size_t input_paths_count) { #ifdef _WIN32 - BOOL bSuccess; - - HANDLE output_path_fd = CreateFile(output_path, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, NULL); + size_t mark = nob_temp_save(); + wchar_t *wide_output_path = nob__unicode_utf8_to_unicode_utf16_temp(output_path); + HANDLE output_path_fd = CreateFileW(wide_output_path, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, NULL); + nob_temp_rewind(mark); if (output_path_fd == INVALID_HANDLE_VALUE) { // NOTE: if output does not exist it 100% must be rebuilt if (GetLastError() == ERROR_FILE_NOT_FOUND) return 1; @@ -2529,7 +2530,7 @@ NOBDEF int nob_needs_rebuild(const char *output_path, const char **input_paths, return -1; } FILETIME output_path_time; - bSuccess = GetFileTime(output_path_fd, NULL, NULL, &output_path_time); + BOOL bSuccess = GetFileTime(output_path_fd, NULL, NULL, &output_path_time); CloseHandle(output_path_fd); if (!bSuccess) { nob_log(NOB_ERROR, "Could not get time of %s: %s", output_path, nob_win32_error_message(GetLastError())); @@ -2538,7 +2539,10 @@ NOBDEF int nob_needs_rebuild(const char *output_path, const char **input_paths, for (size_t i = 0; i < input_paths_count; ++i) { const char *input_path = input_paths[i]; - HANDLE input_path_fd = CreateFile(input_path, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, NULL); + mark = nob_temp_save(); + wchar_t *wide_input_path = nob__unicode_utf8_to_unicode_utf16_temp(input_path); + HANDLE input_path_fd = CreateFileW(wide_input_path, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, NULL); + nob_temp_rewind(mark); if (input_path_fd == INVALID_HANDLE_VALUE) { // NOTE: non-existing input is an error cause it is needed for building in the first place nob_log(NOB_ERROR, "Could not open file %s: %s", input_path, nob_win32_error_message(GetLastError())); From 7f5350e8da1759ff8ce212e60bd153f22747c399 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Wed, 8 Apr 2026 05:11:00 +0000 Subject: [PATCH 12/24] MoveFileExW --- nob.h | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/nob.h b/nob.h index 9b1187b..8d82b8d 100644 --- a/nob.h +++ b/nob.h @@ -2612,17 +2612,28 @@ NOBDEF bool nob_rename(const char *old_path, const char *new_path) nob_log(NOB_INFO, "renaming %s -> %s", old_path, new_path); #endif // NOB_NO_ECHO #ifdef _WIN32 - if (!MoveFileEx(old_path, new_path, MOVEFILE_REPLACE_EXISTING)) { + bool ret; + size_t mark; + wchar_t *wide_old_path; + wchar_t *wide_new_path; + + ret = true; + mark = nob_temp_save(); + wide_old_path = nob__unicode_utf8_to_unicode_utf16_temp(old_path); + wide_new_path = nob__unicode_utf8_to_unicode_utf16_temp(new_path); + if (!MoveFileExW(wide_old_path, wide_new_path, MOVEFILE_REPLACE_EXISTING)) { nob_log(NOB_ERROR, "could not rename %s to %s: %s", old_path, new_path, nob_win32_error_message(GetLastError())); - return false; + ret = false; } + nob_temp_rewind(mark); + return ret; #else if (rename(old_path, new_path) < 0) { nob_log(NOB_ERROR, "could not rename %s to %s: %s", old_path, new_path, strerror(errno)); return false; } -#endif // _WIN32 return true; +#endif // _WIN32 } NOBDEF bool nob_read_entire_file(const char *path, Nob_String_Builder *sb) From e5f2a8c27a44f03aff187bc2feb774e26c113f5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Wed, 8 Apr 2026 05:12:00 +0000 Subject: [PATCH 13/24] GetCurrentDirectoryW --- nob.h | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/nob.h b/nob.h index 8d82b8d..07c1fec 100644 --- a/nob.h +++ b/nob.h @@ -2877,19 +2877,30 @@ NOBDEF int nob_file_exists(const char *file_path) NOBDEF const char *nob_get_current_dir_temp(void) { #ifdef _WIN32 - DWORD nBufferLength = GetCurrentDirectory(0, NULL); - if (nBufferLength == 0) { + DWORD nBufferLengthA; + wchar_t *wide_buffer; + DWORD nBufferLengthB; + char *narrow_buffer; + int narrow_len; + + nBufferLengthA = GetCurrentDirectoryW(0, NULL); + if (nBufferLengthA == 0) { nob_log(NOB_ERROR, "could not get current directory: %s", nob_win32_error_message(GetLastError())); return NULL; } - char *buffer = (char*) nob_temp_alloc(nBufferLength); - if (GetCurrentDirectory(nBufferLength, buffer) == 0) { + wide_buffer = (wchar_t*)nob_temp_alloc(nBufferLengthA * sizeof(wchar_t)); + nBufferLengthB = GetCurrentDirectoryW(nBufferLengthA, wide_buffer); + if (nBufferLengthB == 0) { nob_log(NOB_ERROR, "could not get current directory: %s", nob_win32_error_message(GetLastError())); return NULL; } + NOB_ASSERT(nBufferLengthB == nBufferLengthA - 1); - return buffer; + narrow_buffer = (char*)nob_temp_alloc(nob__worst_case_utf16_to_utf8(nBufferLengthA)); + narrow_len = nob__unicode_utf16_to_unicode_utf8(wide_buffer, nBufferLengthA, narrow_buffer, nob__worst_case_utf16_to_utf8(nBufferLengthA)) - 1; + (void)narrow_len; + return narrow_buffer; #else char *buffer = (char*) nob_temp_alloc(PATH_MAX); if (getcwd(buffer, PATH_MAX) == NULL) { From 4bad81a40856f3f3073a099093f169506b16ea43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Wed, 8 Apr 2026 05:13:00 +0000 Subject: [PATCH 14/24] SetCurrentDirectoryW --- nob.h | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/nob.h b/nob.h index 07c1fec..4d03807 100644 --- a/nob.h +++ b/nob.h @@ -2915,11 +2915,19 @@ NOBDEF const char *nob_get_current_dir_temp(void) NOBDEF bool nob_set_current_dir(const char *path) { #ifdef _WIN32 - if (!SetCurrentDirectory(path)) { + bool ret; + size_t mark; + wchar_t *wide_path; + + ret = true; + mark = nob_temp_save(); + wide_path = nob__unicode_utf8_to_unicode_utf16_temp(path); + if (!SetCurrentDirectoryW(wide_path)) { nob_log(NOB_ERROR, "could not set current directory to %s: %s", path, nob_win32_error_message(GetLastError())); - return false; + ret = false; } - return true; + nob_temp_rewind(mark); + return ret; #else if (chdir(path) < 0) { nob_log(NOB_ERROR, "could not set current directory to %s: %s", path, strerror(errno)); From 01079948ad04f9194710f399247a7c7bb89dc1a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Wed, 8 Apr 2026 05:14:00 +0000 Subject: [PATCH 15/24] UNICODE tests. --- tests/unicode.c | 103 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 tests/unicode.c diff --git a/tests/unicode.c b/tests/unicode.c new file mode 100644 index 0000000..9b1686d --- /dev/null +++ b/tests/unicode.c @@ -0,0 +1,103 @@ +#define NOB_IMPLEMENTATION +#include "nob.h" + + +#define stringify_impl(x) #x +#define stringify(x) stringify_impl(x) +#define test(x) do{ if(!(x)){ nob_log(NOB_ERROR, "TEST FAILED in file " __FILE__ " on line " stringify(__LINE__) " with expression `" stringify(x) "'."); } }while(false) + + +static char const* const k_strings[] = +{ + "Здравствуйте", /* Russian */ + "Γεια σας", /* Greek */ + "안녕", /* Korean */ + "こんにちは", /* Japanese */ + "您好", /* Chinese */ +}; + + +static void test_unicode_utf8_printf(void) +{ + int n; + int i; + + nob_log(NOB_INFO, "%s", "Testing log..."); + n = NOB_ARRAY_LEN(k_strings); + for(i = 0; i != n; ++i) + { + nob_log(NOB_INFO, "%s", k_strings[i]); + } +} + +static void test_unicode_utf8_dir(void) +{ + int n; + int i; + bool b; + size_t mark; + char const* curr_dir; + + nob_log(NOB_INFO, "%s", "Testing mkdir..."); + n = NOB_ARRAY_LEN(k_strings); + for(i = 0; i != n; ++i) + { + b = nob_mkdir_if_not_exists(k_strings[i]); + test(b); + + b = nob_set_current_dir(k_strings[i]); + test(b); + + mark = nob_temp_save(); + curr_dir = nob_get_current_dir_temp(); + test(memcmp(curr_dir + strlen(curr_dir) - strlen(k_strings[i]), k_strings[i], strlen(k_strings[i])) == 0); + nob_temp_rewind(mark); + + b = nob_set_current_dir(".."); + test(b); + + b = nob_delete_file(k_strings[i]); + test(b); + } +} + +static void test_unicode_utf8_file_operations(void) +{ + int n; + int i; + Nob_Fd fd; + bool b; + + nob_log(NOB_INFO, "%s", "Testing file operations..."); + n = NOB_ARRAY_LEN(k_strings); + for(i = 0; i != n; ++i) + { + fd = nob_fd_open_for_write(k_strings[i]); + test(fd != NOB_INVALID_FD); + nob_fd_close(fd); + + fd = nob_fd_open_for_read(k_strings[i]); + test(fd != NOB_INVALID_FD); + nob_fd_close(fd); + + b = nob_rename(k_strings[i], k_strings[(i + 1) % NOB_ARRAY_LEN(k_strings)]); + test(b); + b = nob_rename(k_strings[(i + 1) % NOB_ARRAY_LEN(k_strings)], k_strings[i]); + test(b); + + b = nob_delete_file(k_strings[i]); + test(b); + } +} + + +int main(void) +{ +#ifdef _WIN32 + SetConsoleOutputCP(CP_UTF8); +#endif // _WIN32 + test_unicode_utf8_printf(); + test_unicode_utf8_dir(); + test_unicode_utf8_file_operations(); + return 0; +} From 68cc604fe3d910fd128a6ce9857abeaa813579e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Wed, 8 Apr 2026 05:15:00 +0000 Subject: [PATCH 16/24] Add FILE_SHARE_READ and FILE_SHARE_DELETE. Allow other processes to read from or delete the same file while we have it currently opened for reading. Allow current process to open file for reading while other processes have this file already opened for reading and are sharing this file for reading. --- nob.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nob.h b/nob.h index 4d03807..f5a10d7 100644 --- a/nob.h +++ b/nob.h @@ -1715,7 +1715,7 @@ NOBDEF Nob_Fd nob_fd_open_for_read(const char *path) Nob_Fd result = CreateFileW( wide_path, GENERIC_READ, - 0, + FILE_SHARE_READ | FILE_SHARE_DELETE, &saAttr, OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, @@ -2521,7 +2521,7 @@ NOBDEF int nob_needs_rebuild(const char *output_path, const char **input_paths, #ifdef _WIN32 size_t mark = nob_temp_save(); wchar_t *wide_output_path = nob__unicode_utf8_to_unicode_utf16_temp(output_path); - HANDLE output_path_fd = CreateFileW(wide_output_path, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, NULL); + HANDLE output_path_fd = CreateFileW(wide_output_path, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, NULL); nob_temp_rewind(mark); if (output_path_fd == INVALID_HANDLE_VALUE) { // NOTE: if output does not exist it 100% must be rebuilt @@ -2541,7 +2541,7 @@ NOBDEF int nob_needs_rebuild(const char *output_path, const char **input_paths, const char *input_path = input_paths[i]; mark = nob_temp_save(); wchar_t *wide_input_path = nob__unicode_utf8_to_unicode_utf16_temp(input_path); - HANDLE input_path_fd = CreateFileW(wide_input_path, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, NULL); + HANDLE input_path_fd = CreateFileW(wide_input_path, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, NULL); nob_temp_rewind(mark); if (input_path_fd == INVALID_HANDLE_VALUE) { // NOTE: non-existing input is an error cause it is needed for building in the first place From 1c7cd82b1f0812eb8a86d256a6efca0c2f404c1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Wed, 8 Apr 2026 05:16:00 +0000 Subject: [PATCH 17/24] FormatMessageW --- nob.h | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/nob.h b/nob.h index f5a10d7..8992273 100644 --- a/nob.h +++ b/nob.h @@ -1058,31 +1058,32 @@ int nob__unicode_utf16_to_unicode_utf8(const wchar_t* wide_str, int wide_len, ch #endif // NOB_WIN32_ERR_MSG_SIZE NOBDEF char *nob_win32_error_message(DWORD err) { - static char win32ErrMsg[NOB_WIN32_ERR_MSG_SIZE] = {0}; - DWORD errMsgSize = FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, err, LANG_USER_DEFAULT, win32ErrMsg, + static wchar_t wide_win32ErrMsg[NOB_WIN32_ERR_MSG_SIZE] = {0}; + static char narrow_win32ErrMsg[nob__worst_case_utf16_to_utf8(NOB_ARRAY_LEN(wide_win32ErrMsg))] = {0}; + DWORD errMsgSize = FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, err, LANG_USER_DEFAULT, wide_win32ErrMsg, NOB_WIN32_ERR_MSG_SIZE, NULL); if (errMsgSize == 0) { if (GetLastError() != ERROR_MR_MID_NOT_FOUND) { - if (sprintf(win32ErrMsg, "Could not get error message for 0x%lX", err) > 0) { - return (char *)&win32ErrMsg; + if (sprintf(narrow_win32ErrMsg, "Could not get error message for 0x%lX", err) > 0) { + return (char *)&narrow_win32ErrMsg; } else { return NULL; } } else { - if (sprintf(win32ErrMsg, "Invalid Windows Error code (0x%lX)", err) > 0) { - return (char *)&win32ErrMsg; + if (sprintf(narrow_win32ErrMsg, "Invalid Windows Error code (0x%lX)", err) > 0) { + return (char *)&narrow_win32ErrMsg; } else { return NULL; } } } - - while (errMsgSize > 1 && isspace(win32ErrMsg[errMsgSize - 1])) { - win32ErrMsg[--errMsgSize] = '\0'; + errMsgSize = nob__unicode_utf16_to_unicode_utf8(wide_win32ErrMsg, errMsgSize + 1, narrow_win32ErrMsg, NOB_ARRAY_LEN(narrow_win32ErrMsg)) - 1; + while (errMsgSize > 1 && isspace(narrow_win32ErrMsg[errMsgSize - 1])) { + narrow_win32ErrMsg[--errMsgSize] = '\0'; } - return win32ErrMsg; + return narrow_win32ErrMsg; } #endif // _WIN32 From 218e42b19c17dd2988a94168205e8fb793d55939 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Wed, 8 Apr 2026 05:17:00 +0000 Subject: [PATCH 18/24] Add unicode tests to CI. --- nob.c | 1 + tests/unicode.win32.stdout.txt | 0 2 files changed, 1 insertion(+) create mode 100644 tests/unicode.win32.stdout.txt diff --git a/nob.c b/nob.c index f8925c4..cf2a161 100644 --- a/nob.c +++ b/nob.c @@ -18,6 +18,7 @@ const char *test_names[] = { "cmd_run_dont_reset", "chain", "private_functions_inside_public_macros", + "unicode", }; #define test_names_count ARRAY_LEN(test_names) diff --git a/tests/unicode.win32.stdout.txt b/tests/unicode.win32.stdout.txt new file mode 100644 index 0000000..e69de29 From 2b7de9a1fc345c3691e7a660892fe495c7ff1844 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Wed, 8 Apr 2026 05:18:00 +0000 Subject: [PATCH 19/24] GetModuleFileNameW --- nob.h | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/nob.h b/nob.h index 8992273..ae3532d 100644 --- a/nob.h +++ b/nob.h @@ -3006,9 +3006,21 @@ NOBDEF char *nob_temp_running_executable_path(void) if (length < 0) return nob_temp_strdup(""); return nob_temp_strndup(buf, length); #elif defined(_WIN32) - char buf[MAX_PATH]; - int length = GetModuleFileNameA(NULL, buf, MAX_PATH); - return nob_temp_strndup(buf, length); + wchar_t wide_buf[4096]; /* in reality max path len is 64 kB, meaning 32 thousand UTF-16 code units */ + char narrow_buf[nob__worst_case_utf16_to_utf8(NOB_ARRAY_LEN(wide_buf))]; + DWORD wide_len; + DWORD err; + wide_len = GetModuleFileNameW(NULL, wide_buf, NOB_ARRAY_LEN(wide_buf)); + if (wide_len == 0) { + err = GetLastError(); + (void)err; + return NULL; + } + if (!(wide_len < NOB_ARRAY_LEN(wide_buf))) { + NOB_TODO("Increase wide_buf size."); + } + int narrow_len = nob__unicode_utf16_to_unicode_utf8(wide_buf, wide_len + 1, narrow_buf, NOB_ARRAY_LEN(narrow_buf)) - 1; + return nob_temp_strndup(narrow_buf, narrow_len); #elif defined(__APPLE__) char buf[4096]; uint32_t size = NOB_ARRAY_LEN(buf); From 5284171acb4f628974ae6c6d956886c27ad67116 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Wed, 8 Apr 2026 05:19:00 +0000 Subject: [PATCH 20/24] GetFileAttributesW --- nob.h | 5 ++++- tests/unicode.c | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/nob.h b/nob.h index ae3532d..65cebe3 100644 --- a/nob.h +++ b/nob.h @@ -2307,7 +2307,10 @@ NOBDEF bool nob_write_entire_file(const char *path, const void *data, size_t siz NOBDEF Nob_File_Type nob_get_file_type(const char *path) { #ifdef _WIN32 - DWORD attr = GetFileAttributesA(path); + size_t mark = nob_temp_save(); + wchar_t *wide_path = nob__unicode_utf8_to_unicode_utf16_temp(path); + DWORD attr = GetFileAttributesW(wide_path); + nob_temp_rewind(mark); if (attr == INVALID_FILE_ATTRIBUTES) { nob_log(NOB_ERROR, "Could not get file attributes of %s: %s", path, nob_win32_error_message(GetLastError())); return (Nob_File_Type)-1; diff --git a/tests/unicode.c b/tests/unicode.c index 9b1686d..aad99c4 100644 --- a/tests/unicode.c +++ b/tests/unicode.c @@ -66,6 +66,7 @@ static void test_unicode_utf8_file_operations(void) int n; int i; Nob_Fd fd; + Nob_File_Type ft; bool b; nob_log(NOB_INFO, "%s", "Testing file operations..."); @@ -80,6 +81,9 @@ static void test_unicode_utf8_file_operations(void) test(fd != NOB_INVALID_FD); nob_fd_close(fd); + ft = nob_get_file_type(k_strings[i]); + test(ft == NOB_FILE_REGULAR); + b = nob_rename(k_strings[i], k_strings[(i + 1) % NOB_ARRAY_LEN(k_strings)]); test(b); b = nob_rename(k_strings[(i + 1) % NOB_ARRAY_LEN(k_strings)], k_strings[i]); From 66aca5d7f18812a450d6f1517fd9fe24fa8b18d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Wed, 8 Apr 2026 05:20:00 +0000 Subject: [PATCH 21/24] nob_write_entire_file --- nob.h | 37 +++++++++++++++++++++++++++++++++++++ tests/unicode.c | 3 +++ 2 files changed, 40 insertions(+) diff --git a/nob.h b/nob.h index 65cebe3..3b32a9d 100644 --- a/nob.h +++ b/nob.h @@ -2273,6 +2273,42 @@ NOBDEF bool nob_read_entire_dir(const char *parent, Nob_File_Paths *children) NOBDEF bool nob_write_entire_file(const char *path, const void *data, size_t size) { +#ifdef _WIN32 + bool result; + HANDLE file; + size_t mark; + wchar_t *wide_path; + DWORD err; + BOOL b; + DWORD written; + + result = true; + file = INVALID_HANDLE_VALUE; + mark = nob_temp_save(); + wide_path = nob__unicode_utf8_to_unicode_utf16_temp(path); + file = CreateFileW(wide_path, GENERIC_WRITE, FILE_SHARE_DELETE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + err = GetLastError(); + nob_temp_rewind(mark); + if (file == INVALID_HANDLE_VALUE) + { + nob_log(NOB_ERROR, "Could not open file %s for writing: %s\n", path, nob_win32_error_message(err)); + nob_return_defer(false); + } + b = WriteFile(file, data, (DWORD)size, &written, NULL); + if (!(b != FALSE && written == (DWORD)size)) + { + err = GetLastError(); + nob_log(NOB_ERROR, "Could not write into file %s: %s\n", path, nob_win32_error_message(err)); + nob_return_defer(false); + } +defer: + if (file != INVALID_HANDLE_VALUE) + { + b = CloseHandle(file); + NOB_ASSERT(b != FALSE); + } + return result; +#else bool result = true; const char *buf = NULL; @@ -2302,6 +2338,7 @@ NOBDEF bool nob_write_entire_file(const char *path, const void *data, size_t siz defer: if (f) fclose(f); return result; +#endif // _WIN32 } NOBDEF Nob_File_Type nob_get_file_type(const char *path) diff --git a/tests/unicode.c b/tests/unicode.c index aad99c4..7ed4d6c 100644 --- a/tests/unicode.c +++ b/tests/unicode.c @@ -89,6 +89,9 @@ static void test_unicode_utf8_file_operations(void) b = nob_rename(k_strings[(i + 1) % NOB_ARRAY_LEN(k_strings)], k_strings[i]); test(b); + b = nob_write_entire_file(k_strings[i], "test", 4); + test(b); + b = nob_delete_file(k_strings[i]); test(b); } From e2416ed69cbb3a64013470b7175af854f5afd1d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Wed, 8 Apr 2026 05:21:00 +0000 Subject: [PATCH 22/24] nob_read_entire_file --- nob.h | 59 +++++++++++++++++++++++++++++++++++++++++++++---- tests/unicode.c | 10 +++++++++ 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/nob.h b/nob.h index 3b32a9d..79f6210 100644 --- a/nob.h +++ b/nob.h @@ -2679,6 +2679,60 @@ NOBDEF bool nob_rename(const char *old_path, const char *new_path) NOBDEF bool nob_read_entire_file(const char *path, Nob_String_Builder *sb) { +#ifdef _WIN32 + bool result; + HANDLE file; + DWORD chunk_size; + size_t mark; + wchar_t *wide_path; + DWORD err; + size_t new_count; + BOOL b; + DWORD read; + + result = true; + file = INVALID_HANDLE_VALUE; + chunk_size = 64 * 1024; + mark = nob_temp_save(); + wide_path = nob__unicode_utf8_to_unicode_utf16_temp(path); + file = CreateFileW(wide_path, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + err = GetLastError(); + nob_temp_rewind(mark); + if (file == INVALID_HANDLE_VALUE) + { + nob_log(NOB_ERROR, "Could not open file %s for reading: %s\n", path, nob_win32_error_message(err)); + nob_return_defer(false); + } + for (;;) + { + new_count = sb->count + chunk_size; + if (new_count > sb->capacity) + { + sb->items = NOB_DECLTYPE_CAST(sb->items)NOB_REALLOC(sb->items, 2 * new_count); + NOB_ASSERT(sb->items != NULL && "Buy more RAM lool!!"); + sb->capacity = new_count; + } + b = ReadFile(file, sb->items + sb->count, chunk_size, &read, NULL); + if (b == FALSE) + { + err = GetLastError(); + nob_log(NOB_ERROR, "Could not read from file %s: %s\n", path, nob_win32_error_message(err)); + nob_return_defer(false); + } + sb->count += read; + if (read != chunk_size) + { + break; + } + } +defer: + if (file != INVALID_HANDLE_VALUE) + { + b = CloseHandle(file); + NOB_ASSERT(b != FALSE); + } + return result; +#else bool result = true; FILE *f = fopen(path, "rb"); @@ -2686,11 +2740,7 @@ NOBDEF bool nob_read_entire_file(const char *path, Nob_String_Builder *sb) long long m = 0; if (f == NULL) nob_return_defer(false); if (fseek(f, 0, SEEK_END) < 0) nob_return_defer(false); -#ifndef _WIN32 m = ftell(f); -#else - m = _telli64(_fileno(f)); -#endif if (m < 0) nob_return_defer(false); if (fseek(f, 0, SEEK_SET) < 0) nob_return_defer(false); @@ -2712,6 +2762,7 @@ NOBDEF bool nob_read_entire_file(const char *path, Nob_String_Builder *sb) if (!result) nob_log(NOB_ERROR, "Could not read file %s: %s", path, strerror(errno)); if (f) fclose(f); return result; +#endif // _WIN32 } NOBDEF int nob_sb_appendf(Nob_String_Builder *sb, const char *fmt, ...) diff --git a/tests/unicode.c b/tests/unicode.c index 7ed4d6c..f1851d7 100644 --- a/tests/unicode.c +++ b/tests/unicode.c @@ -68,6 +68,7 @@ static void test_unicode_utf8_file_operations(void) Nob_Fd fd; Nob_File_Type ft; bool b; + Nob_String_Builder sb; nob_log(NOB_INFO, "%s", "Testing file operations..."); n = NOB_ARRAY_LEN(k_strings); @@ -92,6 +93,15 @@ static void test_unicode_utf8_file_operations(void) b = nob_write_entire_file(k_strings[i], "test", 4); test(b); + sb.items = NULL; + sb.capacity = 0; + sb.count = 0; + b = nob_read_entire_file(k_strings[i], &sb); + test(b); + test(sb.count == 4); + test(memcmp(sb.items, "test", 4) == 0); + NOB_FREE(sb.items); + b = nob_delete_file(k_strings[i]); test(b); } From 0974ba1fd3de59029c8f1e0bf40ee7274fe2c176 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Wed, 8 Apr 2026 05:22:00 +0000 Subject: [PATCH 23/24] nob_file_exists --- nob.h | 12 ++++++++++-- tests/unicode.c | 6 ++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/nob.h b/nob.h index 79f6210..275c8c0 100644 --- a/nob.h +++ b/nob.h @@ -2960,10 +2960,18 @@ NOBDEF bool nob_sv_starts_with(Nob_String_View sv, Nob_String_View expected_pref NOBDEF int nob_file_exists(const char *file_path) { #if _WIN32 - return GetFileAttributesA(file_path) != INVALID_FILE_ATTRIBUTES; + size_t mark; + wchar_t *wide_file_path; + bool ret; + + mark = nob_temp_save(); + wide_file_path = nob__unicode_utf8_to_unicode_utf16_temp(file_path); + ret = GetFileAttributesW(wide_file_path) != INVALID_FILE_ATTRIBUTES; + nob_temp_rewind(mark); + return ret; #else return access(file_path, F_OK) == 0; -#endif +#endif // _WIN32 } NOBDEF const char *nob_get_current_dir_temp(void) diff --git a/tests/unicode.c b/tests/unicode.c index f1851d7..6436d5e 100644 --- a/tests/unicode.c +++ b/tests/unicode.c @@ -102,8 +102,14 @@ static void test_unicode_utf8_file_operations(void) test(memcmp(sb.items, "test", 4) == 0); NOB_FREE(sb.items); + b = nob_file_exists(k_strings[i]); + test(b); + b = nob_delete_file(k_strings[i]); test(b); + + b = nob_file_exists(k_strings[i]); + test(!b); } } From fa2e8d051bd631e420f112c88b6d7a497271d1c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Wed, 8 Apr 2026 05:23:00 +0000 Subject: [PATCH 24/24] Fix Unicode test record/replay. --- tests/unicode.stderr.txt | 33 +++++++++++++++++++++++++++++++++ tests/unicode.stdout.txt | 0 2 files changed, 33 insertions(+) create mode 100644 tests/unicode.stderr.txt create mode 100644 tests/unicode.stdout.txt diff --git a/tests/unicode.stderr.txt b/tests/unicode.stderr.txt new file mode 100644 index 0000000..ae89ebd --- /dev/null +++ b/tests/unicode.stderr.txt @@ -0,0 +1,33 @@ +[INFO] Testing log... +[INFO] Здравствуйте +[INFO] Γεια σας +[INFO] 안녕 +[INFO] こんにちは +[INFO] 您好 +[INFO] Testing mkdir... +[INFO] created directory `Здравствуйте` +[INFO] deleting Здравствуйте +[INFO] created directory `Γεια σας` +[INFO] deleting Γεια σας +[INFO] created directory `안녕` +[INFO] deleting 안녕 +[INFO] created directory `こんにちは` +[INFO] deleting こんにちは +[INFO] created directory `您好` +[INFO] deleting 您好 +[INFO] Testing file operations... +[INFO] renaming Здравствуйте -> Γεια σας +[INFO] renaming Γεια σας -> Здравствуйте +[INFO] deleting Здравствуйте +[INFO] renaming Γεια σας -> 안녕 +[INFO] renaming 안녕 -> Γεια σας +[INFO] deleting Γεια σας +[INFO] renaming 안녕 -> こんにちは +[INFO] renaming こんにちは -> 안녕 +[INFO] deleting 안녕 +[INFO] renaming こんにちは -> 您好 +[INFO] renaming 您好 -> こんにちは +[INFO] deleting こんにちは +[INFO] renaming 您好 -> Здравствуйте +[INFO] renaming Здравствуйте -> 您好 +[INFO] deleting 您好 diff --git a/tests/unicode.stdout.txt b/tests/unicode.stdout.txt new file mode 100644 index 0000000..e69de29