From 27f50e8dbd336b9c1c6ceb54614dcfcee69c315d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Mon, 26 Jan 2026 16:00:00 +0100 Subject: [PATCH 01/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 eb0a140..387d98a 100644 --- a/nob.h +++ b/nob.h @@ -856,6 +856,40 @@ void nob__cmd_append(Nob_Cmd *cmd, size_t n, ...) va_end(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 97ce225dd53f7fe39033a479521c194e9ffd8719 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Mon, 26 Jan 2026 16:01:00 +0100 Subject: [PATCH 02/24] CopyFileW --- nob.h | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/nob.h b/nob.h index 387d98a..11bd589 100644 --- a/nob.h +++ b/nob.h @@ -1017,11 +1017,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 c16f59b0f86650ec108c471cfe896d9b9f2907d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Mon, 26 Jan 2026 16:02:00 +0100 Subject: [PATCH 03/24] CreateProcessW --- nob.h | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/nob.h b/nob.h index 11bd589..8f85345 100644 --- a/nob.h +++ b/nob.h @@ -1260,9 +1260,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 @@ -1277,7 +1277,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 293bde5577bc38ef1e485566ab87a41bd845605b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Mon, 26 Jan 2026 16:03:00 +0100 Subject: [PATCH 04/24] nob__unicode_utf16_to_unicode_utf8 --- nob.h | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/nob.h b/nob.h index 8f85345..893c9af 100644 --- a/nob.h +++ b/nob.h @@ -886,6 +886,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 8fb2ab547021842bb42b1491385d89e8cd6c75ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Mon, 26 Jan 2026 16:04:00 +0100 Subject: [PATCH 05/24] nob__worst_case_utf16_to_utf8 --- nob.h | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/nob.h b/nob.h index 893c9af..cd1142c 100644 --- a/nob.h +++ b/nob.h @@ -302,6 +302,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_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 3c96eb705a210896b60347132e3552a1532c9ce9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Mon, 26 Jan 2026 16:05:00 +0100 Subject: [PATCH 06/24] FindFirstFileW+FindNextFileW --- nob.h | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/nob.h b/nob.h index cd1142c..4fabe5e 100644 --- a/nob.h +++ b/nob.h @@ -313,13 +313,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 @@ -1742,8 +1749,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) { @@ -1751,6 +1759,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 == NULL) { @@ -1767,17 +1781,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 0dc7206bbdc13cc462a2090954097eb9bdccc625 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Mon, 26 Jan 2026 16:06:00 +0100 Subject: [PATCH 07/24] CreateFileW --- nob.h | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/nob.h b/nob.h index 4fabe5e..e73cac7 100644 --- a/nob.h +++ b/nob.h @@ -1419,14 +1419,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())); @@ -1453,8 +1456,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 @@ -1462,6 +1467,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 1ab61d90823d403d688ec6ec42516bfa09943f5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Mon, 26 Jan 2026 16:07:00 +0100 Subject: [PATCH 08/24] CreateDirectoryW --- nob.h | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/nob.h b/nob.h index e73cac7..2fdd7a1 100644 --- a/nob.h +++ b/nob.h @@ -1026,10 +1026,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 @@ -1045,6 +1077,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 5a91f2612180a8a6f46c6da7f81ef2a4169db35f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Mon, 26 Jan 2026 16:08:00 +0100 Subject: [PATCH 09/24] RemoveDirectoryW+DeleteFileW --- nob.h | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/nob.h b/nob.h index 2fdd7a1..58fbc13 100644 --- a/nob.h +++ b/nob.h @@ -2037,21 +2037,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 98c10e04843f7f59b15084b3a6b4ac95d4e96762 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Mon, 26 Jan 2026 16:09:00 +0100 Subject: [PATCH 10/24] nob_needs_rebuild --- nob.h | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/nob.h b/nob.h index 58fbc13..ff30a82 100644 --- a/nob.h +++ b/nob.h @@ -2221,9 +2221,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; @@ -2231,7 +2232,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())); @@ -2240,7 +2241,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 0382e0f55b82ab9243e96366a49a44ffedf4e9f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Mon, 26 Jan 2026 16:10:00 +0100 Subject: [PATCH 11/24] MoveFileExW --- nob.h | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/nob.h b/nob.h index ff30a82..e7b72a3 100644 --- a/nob.h +++ b/nob.h @@ -2314,17 +2314,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 f357bb3c6ed3129a1697f8308160ec0e0ebfef2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Mon, 26 Jan 2026 16:11:00 +0100 Subject: [PATCH 12/24] GetCurrentDirectoryW --- nob.h | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/nob.h b/nob.h index e7b72a3..90fe96c 100644 --- a/nob.h +++ b/nob.h @@ -2524,19 +2524,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 3b6fd1040c185787beb71e33669b6d45e2cb0ae9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Mon, 26 Jan 2026 16:12:00 +0100 Subject: [PATCH 13/24] SetCurrentDirectoryW --- nob.h | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/nob.h b/nob.h index 90fe96c..6fce1bc 100644 --- a/nob.h +++ b/nob.h @@ -2562,11 +2562,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 fecfb5af80efbc1fda59e81ed3b22326c1c59fa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Mon, 26 Jan 2026 16:13:00 +0100 Subject: [PATCH 14/24] UNICODE tests. --- tests/unicode.c | 100 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 tests/unicode.c diff --git a/tests/unicode.c b/tests/unicode.c new file mode 100644 index 0000000..dd97d9f --- /dev/null +++ b/tests/unicode.c @@ -0,0 +1,100 @@ +#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) +{ + test_unicode_utf8_printf(); + test_unicode_utf8_dir(); + test_unicode_utf8_file_operations(); + return 0; +} From 9c95f99ff79a76e10c0b1f612220c166089d74f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Mon, 26 Jan 2026 16:14:00 +0100 Subject: [PATCH 15/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 6fce1bc..115c6f5 100644 --- a/nob.h +++ b/nob.h @@ -1457,7 +1457,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, @@ -2223,7 +2223,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 @@ -2243,7 +2243,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 ec721fd1b2e8f91ab2f35088d22d5967e2fcd0f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Mon, 26 Jan 2026 16:15:00 +0100 Subject: [PATCH 16/24] FormatMessageW --- nob.h | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/nob.h b/nob.h index 115c6f5..af65c13 100644 --- a/nob.h +++ b/nob.h @@ -937,31 +937,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 da93abd7658c78edc486ef1a52c1cc64249e5a64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Mon, 26 Jan 2026 16:16:00 +0100 Subject: [PATCH 17/24] Add unicode tests to CI. --- nob.c | 1 + 1 file changed, 1 insertion(+) diff --git a/nob.c b/nob.c index 40e45ce..2eaf983 100644 --- a/nob.c +++ b/nob.c @@ -27,6 +27,7 @@ const char *test_names[] = { "temp_running_executable_path", "no_echo", "cmd_run_dont_reset", + "unicode", }; #define test_names_count ARRAY_LEN(test_names) From 2d103e391d1c74d6030a271809ba693fdfef3043 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Mon, 26 Jan 2026 16:17:00 +0100 Subject: [PATCH 18/24] GetModuleFileNameW --- nob.h | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/nob.h b/nob.h index af65c13..3f40b1f 100644 --- a/nob.h +++ b/nob.h @@ -2653,9 +2653,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 36a8ecf583eeb85fd022ef532dd0738ab805b9f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Mon, 26 Jan 2026 16:18:00 +0100 Subject: [PATCH 19/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 3f40b1f..53da811 100644 --- a/nob.h +++ b/nob.h @@ -2009,7 +2009,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 -1; diff --git a/tests/unicode.c b/tests/unicode.c index dd97d9f..eb9db24 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 ee9a1fe1a01e68f79d0d244ead27224829cb8b90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Mon, 26 Jan 2026 16:19:00 +0100 Subject: [PATCH 20/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 53da811..7563451 100644 --- a/nob.h +++ b/nob.h @@ -1975,6 +1975,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; @@ -2004,6 +2040,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 eb9db24..9c60903 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 081492269d7426d76dbc63a3d7fb7da41fb058f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Mon, 26 Jan 2026 16:20:00 +0100 Subject: [PATCH 21/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 7563451..7ff119c 100644 --- a/nob.h +++ b/nob.h @@ -2381,6 +2381,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"); @@ -2388,11 +2442,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); @@ -2414,6 +2464,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 9c60903..09f17f0 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 74261531c0f0bfee524d26d4848dc1387160469e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Mon, 26 Jan 2026 16:21:00 +0100 Subject: [PATCH 22/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 7ff119c..b7efaf5 100644 --- a/nob.h +++ b/nob.h @@ -2607,10 +2607,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 09f17f0..eca86bc 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 06ede7a0168fc122f892d9eed34cb30021292c87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Mon, 26 Jan 2026 16:22:00 +0100 Subject: [PATCH 23/24] Unicode printf on Windows. --- nob.h | 166 +++++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 153 insertions(+), 13 deletions(-) diff --git a/nob.h b/nob.h index b7efaf5..9d5f6d0 100644 --- a/nob.h +++ b/nob.h @@ -164,6 +164,8 @@ # include # include # include +# include +# include #else # ifdef __APPLE__ # include @@ -202,8 +204,8 @@ #endif #define NOB_UNUSED(value) (void)(value) -#define NOB_TODO(message) do { fprintf(stderr, "%s:%d: TODO: %s\n", __FILE__, __LINE__, message); abort(); } while(0) -#define NOB_UNREACHABLE(message) do { fprintf(stderr, "%s:%d: UNREACHABLE: %s\n", __FILE__, __LINE__, message); abort(); } while(0) +#define NOB_TODO(message) do { nob__fprintf(stderr, "%s:%d: TODO: %s\n", __FILE__, __LINE__, message); abort(); } while(0) +#define NOB_UNREACHABLE(message) do { nob__fprintf(stderr, "%s:%d: UNREACHABLE: %s\n", __FILE__, __LINE__, message); abort(); } while(0) #define NOB_ARRAY_LEN(array) (sizeof(array)/sizeof(array[0])) #define NOB_ARRAY_GET(array, index) \ @@ -874,6 +876,144 @@ void nob__cmd_append(Nob_Cmd *cmd, size_t n, ...) va_end(args); } + +#ifdef _WIN32 + + +WORD nob__unicode_debug_beg(HANDLE win_out) +{ +#ifdef NOB_DEBUG_UNIOCDE + BOOL b; + CONSOLE_SCREEN_BUFFER_INFO info; + WORD attributes; + + b = GetConsoleScreenBufferInfo(win_out, &info); + NOB_ASSERT(b != 0); + attributes = info.wAttributes; + b = SetConsoleTextAttribute(win_out, FOREGROUND_INTENSITY | FOREGROUND_GREEN); + NOB_ASSERT(b != 0); + return attributes; +#else + (void)win_out; + return 0; +#endif +} + +void nob__unicode_debug_end(HANDLE win_out, WORD attributes) +{ +#ifdef NOB_DEBUG_UNIOCDE + BOOL b; + + b = SetConsoleTextAttribute(win_out, attributes); + NOB_ASSERT(b != 0); +#else + (void)win_out; + (void)attributes; +#endif +} + +int nob__vfprintf(FILE *stream, const char *format, va_list args) +{ + char *narrow_ptr; + char narrow_buf[1024]; + size_t narrow_mark; + int narrow_len_a; + int narrow_len_b; + HANDLE win_out; + DWORD file_type; + wchar_t *wide_ptr; + wchar_t wide_buf[1024]; + size_t wide_mark; + int wide_len_a; + DWORD err; + int wide_len_b; + WORD revert; + BOOL b; + DWORD written; + + narrow_ptr = narrow_buf; + narrow_mark = 0; + narrow_len_a = vsnprintf(narrow_buf, NOB_ARRAY_LEN(narrow_buf), format, args); + NOB_ASSERT(narrow_len_a >= 0); /* vsnprintf failed, what now? */ + if (narrow_len_a >= NOB_ARRAY_LEN(narrow_buf)) { + narrow_mark = nob_temp_save(); + narrow_ptr = (char*)nob_temp_alloc(narrow_len_a + 1); + NOB_ASSERT(narrow_ptr); /* nob_temp_alloc failed, what now? */ + narrow_len_b = vsnprintf(narrow_ptr, narrow_len_a + 1, format, args); + NOB_ASSERT(narrow_len_b == narrow_len_a); /* Second call to vsnprintf produced different result thatn first call, what now? */ + } + NOB_ASSERT(stream == stdout || stream == stderr); /* It is user error to print to stdin or to non-standard stream. */ + win_out = GetStdHandle(stream == stdout ? STD_OUTPUT_HANDLE : STD_ERROR_HANDLE); + NOB_ASSERT(win_out != INVALID_HANDLE_VALUE); /* GetStdHandle failed, what now? */ + if (win_out == NULL) { + return 0; + } + file_type = GetFileType(win_out); + if (file_type == FILE_TYPE_CHAR) { + wide_ptr = wide_buf; + wide_mark = 0; + wide_len_a = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, narrow_ptr, narrow_len_a, wide_buf, NOB_ARRAY_LEN(wide_buf)); + err = GetLastError(); + NOB_ASSERT((wide_len_a != 0) || (wide_len_a == 0 && err == ERROR_INSUFFICIENT_BUFFER)); /* MultiByteToWideChar failed, what now? */ + if (wide_len_a == 0 && err == ERROR_INSUFFICIENT_BUFFER) { + wide_len_a = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, narrow_ptr, narrow_len_a, NULL, 0); + NOB_ASSERT(wide_len_a > NOB_ARRAY_LEN(wide_buf)); /* MultiByteToWideChar failed, what now? */ + wide_mark = nob_temp_save(); + wide_ptr = (wchar_t*)nob_temp_alloc(wide_len_a * sizeof(wchar_t)); + NOB_ASSERT(wide_ptr); /* nob_temp_alloc failed, what now? */ + wide_len_b = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, narrow_ptr, narrow_len_a, wide_ptr, wide_len_a); + NOB_ASSERT(wide_len_b == wide_len_a); /* MultiByteToWideChar failed, what now? */ + } + revert = nob__unicode_debug_beg(win_out); + b = WriteConsoleW(win_out, wide_ptr, (DWORD)wide_len_a, &written, NULL); + nob__unicode_debug_end(win_out, revert); + NOB_ASSERT(b != 0); /* WriteConsoleW failed, what now? */ + NOB_ASSERT(written == (DWORD)wide_len_a); /* WriteConsoleW failed, what now? */ + if (wide_ptr != wide_buf) { + nob_temp_rewind(wide_mark); + } + } else { + b = WriteFile(win_out, narrow_ptr, (DWORD)narrow_len_a, &written, NULL); + NOB_ASSERT(b != 0); /* WriteFile failed, what now? */ + NOB_ASSERT(written == (DWORD)narrow_len_a); /* WriteFile failed, what now? */ + } + if (narrow_ptr != narrow_buf) { + nob_temp_rewind(narrow_mark); + } + return narrow_len_a; +} + +int nob__fprintf(FILE *stream, const char *fmt, ...) +{ + va_list args; + int len; + + va_start(args, fmt); + len = nob__vfprintf(stream, fmt, args); + va_end(args); + return len; +} + +int nob__printf(const char *fmt, ...) +{ + va_list args; + int len; + + va_start(args, fmt); + len = nob__vfprintf(stdout, fmt, args); + va_end(args); + return len; +} + +#else + +#define nob__vfprintf vfprintf +#define nob__fprintf fprintf +#define nob__printf printf + +#endif // _WIN32 + + #ifdef _WIN32 @@ -1738,42 +1878,42 @@ NOBDEF void nob_default_log_handler(Nob_Log_Level level, const char *fmt, va_lis switch (level) { case NOB_INFO: - fprintf(stderr, "[INFO] "); + nob__fprintf(stderr, "[INFO] "); break; case NOB_WARNING: - fprintf(stderr, "[WARNING] "); + nob__fprintf(stderr, "[WARNING] "); break; case NOB_ERROR: - fprintf(stderr, "[ERROR] "); + nob__fprintf(stderr, "[ERROR] "); break; case NOB_NO_LOGS: return; default: NOB_UNREACHABLE("Nob_Log_Level"); } - vfprintf(stderr, fmt, args); - fprintf(stderr, "\n"); + nob__vfprintf(stderr, fmt, args); + nob__fprintf(stderr, "\n"); } NOBDEF void nob_cancer_log_handler(Nob_Log_Level level, const char *fmt, va_list args) { switch (level) { case NOB_INFO: - fprintf(stderr, "ℹ️ \x1b[36m[INFO]\x1b[0m "); + nob__fprintf(stderr, "ℹ️ \x1b[36m[INFO]\x1b[0m "); break; case NOB_WARNING: - fprintf(stderr, "⚠️ \x1b[33m[WARNING]\x1b[0m "); + nob__fprintf(stderr, "⚠️ \x1b[33m[WARNING]\x1b[0m "); break; case NOB_ERROR: - fprintf(stderr, "🚨 \x1b[31m[ERROR]\x1b[0m "); + nob__fprintf(stderr, "🚨 \x1b[31m[ERROR]\x1b[0m "); break; case NOB_NO_LOGS: return; default: NOB_UNREACHABLE("Nob_Log_Level"); } - vfprintf(stderr, fmt, args); - fprintf(stderr, "\n"); + nob__vfprintf(stderr, fmt, args); + nob__fprintf(stderr, "\n"); } NOBDEF void nob_log(Nob_Log_Level level, const char *fmt, ...) @@ -2787,7 +2927,7 @@ NOBDEF char *nob_temp_running_executable_path(void) break; return nob_temp_strndup(info.name, strlen(info.name)); #else - fprintf(stderr, "%s:%d: TODO: nob_temp_running_executable_path is not implemented for this platform\n", __FILE__, __LINE__); + nob__fprintf(stderr, "%s:%d: TODO: nob_temp_running_executable_path is not implemented for this platform\n", __FILE__, __LINE__); return nob_temp_strdup(""); #endif } From 43cb6011ff8418d1b2e99be0bea126b8e3cc0dd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Kn=C3=A1pek?= Date: Mon, 26 Jan 2026 16:23:00 +0100 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