| 233 | } |
| 234 | |
| 235 | static bool config_write_atomic(const char *path, const char *json, size_t json_length) { |
| 236 | wchar_t *wide_path = cbm_path_to_wide(path); |
| 237 | if (!wide_path) { |
| 238 | return false; |
| 239 | } |
| 240 | size_t temporary_capacity = wcslen(wide_path) + 40U; |
| 241 | wchar_t *temporary = calloc(temporary_capacity, sizeof(*temporary)); |
| 242 | HANDLE file = INVALID_HANDLE_VALUE; |
| 243 | if (temporary) { |
| 244 | for (unsigned int attempt = 0; attempt < 128U; attempt++) { |
| 245 | ULONG sequence = (ULONG)InterlockedIncrement(&g_config_temp_sequence); |
| 246 | int written = swprintf(temporary, temporary_capacity, L"%ls.tmp.%08lX.%08lX", wide_path, |
| 247 | (unsigned long)GetCurrentProcessId(), (unsigned long)sequence); |
| 248 | if (written <= 0 || (size_t)written >= temporary_capacity) { |
| 249 | break; |
| 250 | } |
| 251 | /* DELETE access on the temp handle is what permits the |
| 252 | * rename-by-handle publish below. */ |
| 253 | file = CreateFileW(temporary, GENERIC_WRITE | DELETE, 0, NULL, CREATE_NEW, |
| 254 | FILE_ATTRIBUTE_NORMAL | FILE_FLAG_WRITE_THROUGH, NULL); |
| 255 | if (file != INVALID_HANDLE_VALUE || GetLastError() != ERROR_FILE_EXISTS) { |
| 256 | break; |
| 257 | } |
| 258 | } |
| 259 | } |
| 260 | bool ok = file != INVALID_HANDLE_VALUE; |
| 261 | size_t offset = 0; |
| 262 | while (ok && offset < json_length) { |
| 263 | size_t remaining = json_length - offset; |
| 264 | DWORD chunk = remaining > UINT32_MAX ? UINT32_MAX : (DWORD)remaining; |
| 265 | DWORD written = 0; |
| 266 | ok = WriteFile(file, json + offset, chunk, &written, NULL) != 0 && written > 0; |
| 267 | offset += written; |
| 268 | } |
| 269 | if (ok) { |
| 270 | ok = FlushFileBuffers(file) != 0; |
| 271 | } |
| 272 | bool published = ok && config_posix_rename_handle(file, wide_path); |
| 273 | if (file != INVALID_HANDLE_VALUE && !CloseHandle(file)) { |
| 274 | ok = false; |
| 275 | } |
| 276 | if (ok && !published) { |
| 277 | /* Error-driven fallback, not a version probe: POSIX rename needs |
| 278 | * NTFS-class filesystem support, so exFAT/SMB-homed configs land |
| 279 | * here, keeping the pre-POSIX behavior (replace can fail while a |
| 280 | * reader holds the destination open). */ |
| 281 | published = MoveFileExW(temporary, wide_path, |
| 282 | MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) != 0; |
| 283 | } |
| 284 | ok = ok && published; |
| 285 | if (!ok && temporary) { |
| 286 | (void)DeleteFileW(temporary); |
| 287 | } |
| 288 | free(temporary); |
| 289 | free(wide_path); |
| 290 | return ok; |
| 291 | } |
| 292 | #else |
no test coverage detected