diff --git a/lib/file_io.c b/lib/file_io.c new file mode 100644 index 00000000..f39496b5 --- /dev/null +++ b/lib/file_io.c @@ -0,0 +1,180 @@ +#include "file_io.h" +#include "platform_compat.h" +#include "debug_config.h" +#include +#include +#include +#include + +#ifdef _WIN32 +/* Общие пути API — UTF-8; ANSI Win32 API не применяется. */ +static int file_wide(const char *path, wchar_t wide[FILE_IO_PATH_MAX]) { + if (path && *path && strlen(path) < FILE_IO_PATH_MAX + && MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, path, -1, wide, FILE_IO_PATH_MAX)) return 0; + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file invalid UTF-8/oversized path winerr=%lu", GetLastError()); + return -1; +} +#endif + +int file_io_inspect(const char *path, int follow, struct file_io_info *info) { + if (!path || !*path || strlen(path) >= FILE_IO_PATH_MAX || !info) { + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file inspect invalid path/output"); return -1; + } + struct file_io_info result = {0}; +#ifdef _WIN32 + wchar_t wide[FILE_IO_PATH_MAX]; + if (file_wide(path, wide)) return -1; + DWORD attributes = GetFileAttributesW(wide); + if (attributes == INVALID_FILE_ATTRIBUTES) { + DWORD error = GetLastError(); + if (error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND) return 1; + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file inspect failed path=%s winerr=%lu", path, error); return -1; + } + if (!follow && (attributes & FILE_ATTRIBUTE_REPARSE_POINT)) result.type = FILE_IO_LINK; + else { + HANDLE handle = CreateFileW(wide, FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL); + BY_HANDLE_FILE_INFORMATION data; + int valid = handle != INVALID_HANDLE_VALUE && GetFileInformationByHandle(handle, &data); + if (valid) { + result.type = GetFileType(handle) != FILE_TYPE_DISK ? FILE_IO_OTHER + : (data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ? FILE_IO_DIRECTORY : FILE_IO_REGULAR; + uint64_t size = ((uint64_t)data.nFileSizeHigh << 32) | data.nFileSizeLow; + valid = size <= INT64_MAX; + result.size = (int64_t)size; + result.identity[0] = data.dwVolumeSerialNumber; + result.identity[1] = ((uint64_t)data.nFileIndexHigh << 32) | data.nFileIndexLow; + } + DWORD error = GetLastError(); + if (handle != INVALID_HANDLE_VALUE) CloseHandle(handle); + if (!valid) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file handle inspect failed path=%s winerr=%lu", path, error); return -1; } + } +#else + struct stat data; + if ((follow ? stat(path, &data) : lstat(path, &data))) { + if (errno == ENOENT) return 1; + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file inspect failed path=%s errno=%d", path, errno); return -1; + } + result.type = S_ISREG(data.st_mode) ? FILE_IO_REGULAR : S_ISDIR(data.st_mode) ? FILE_IO_DIRECTORY + : S_ISLNK(data.st_mode) ? FILE_IO_LINK : FILE_IO_OTHER; + if (data.st_size < 0 || (uint64_t)data.st_size > INT64_MAX) { + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file size out of range path=%s", path); return -1; + } + result.size = (int64_t)data.st_size; + result.identity[0] = data.st_dev; result.identity[1] = data.st_ino; +#endif + *info = result; + return 0; +} + +FILE *file_io_open_read(const char *path) { + struct file_io_info info; + if (file_io_inspect(path, 1, &info) || info.type != FILE_IO_REGULAR) { + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file source is not regular/readable path=%s", path ? path : "(null)"); return NULL; + } +#ifdef _WIN32 + wchar_t wide[FILE_IO_PATH_MAX]; + if (file_wide(path, wide)) return NULL; + FILE *file = _wfopen(wide, L"rb"); +#else + FILE *file = fopen(path, "rb"); +#endif + if (!file) DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file open read failed path=%s errno=%d", path, errno); + return file; +} + +int file_io_paths(const char *source, const char *destination) { + struct file_io_info input, output; + if (file_io_inspect(source, 1, &input) || input.type != FILE_IO_REGULAR) { + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file invalid source path=%s", source ? source : "(null)"); return -1; + } + int status = file_io_inspect(destination, 0, &output); + if (status == 1) return 0; + if (status < 0) return -1; + if (output.type != FILE_IO_REGULAR) { + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file destination is not regular path=%s type=%d", destination, output.type); return -1; + } + return !memcmp(input.identity, output.identity, sizeof(input.identity)); +} + +int file_io_temp_open(struct file_io_temp *output, const char *destination) { + if (!output || output->file || output->temporary[0] || !destination || !*destination + || strlen(destination) + 38 >= FILE_IO_PATH_MAX) { + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file temporary invalid owner/path"); return -1; + } + for (unsigned attempt = 0; attempt < 8; attempt++) { + uint8_t random[16]; char suffix[33]; + if (random_bytes(random, sizeof(random))) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file temporary RNG failed"); return -1; } + for (unsigned i = 0; i < sizeof(random); i++) snprintf(suffix + i * 2, 3, "%02x", random[i]); + snprintf(output->temporary, sizeof(output->temporary), "%s.tmp.%s", destination, suffix); +#ifdef _WIN32 + wchar_t wide[FILE_IO_PATH_MAX]; + if (file_wide(output->temporary, wide)) { output->temporary[0] = 0; return -1; } + int fd = _wopen(wide, _O_CREAT | _O_EXCL | _O_RDWR | _O_BINARY, _S_IREAD | _S_IWRITE); +#else + int fd = open(output->temporary, O_CREAT | O_EXCL | O_RDWR, 0600); +#endif + if (fd < 0) { + output->temporary[0] = 0; + if (errno == EEXIST) continue; + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file temporary open failed destination=%s errno=%d", destination, errno); return -1; + } +#ifdef _WIN32 + output->file = _fdopen(fd, "wb+"); + if (!output->file) _close(fd); +#else + output->file = fdopen(fd, "wb+"); + if (!output->file) close(fd); +#endif + if (!output->file) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file fdopen failed path=%s errno=%d", output->temporary, errno); return -1; } + DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "file temporary opened path=%s", output->temporary); + return 0; + } + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file temporary collision limit destination=%s", destination); return -1; +} + +int file_io_temp_finish(struct file_io_temp *output) { + if (!output || !output->file) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file finish without open file"); return -1; } + int error = fflush(output->file) ? (errno ? errno : EIO) : 0; + if (fclose(output->file) && !error) error = errno ? errno : EIO; + output->file = NULL; + if (error) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file flush/close failed path=%s errno=%d", output->temporary, error); return -1; } + return 0; +} + +void file_io_temp_discard(struct file_io_temp *output) { + if (!output) return; + if (output->file) { + if (fclose(output->file)) DEBUG_WARN(DEBUG_CATEGORY_SYS, "file cleanup close failed errno=%d", errno); + output->file = NULL; + } + if (!output->temporary[0]) return; +#ifdef _WIN32 + wchar_t wide[FILE_IO_PATH_MAX]; + int error = file_wide(output->temporary, wide) ? -1 : _wremove(wide); +#else + int error = remove(output->temporary); +#endif + if (error && errno != ENOENT) DEBUG_WARN(DEBUG_CATEGORY_SYS, "file cleanup remove failed path=%s errno=%d", output->temporary, errno); + output->temporary[0] = 0; +} + +int file_io_temp_publish(struct file_io_temp *output, const char *source, const char *destination) { + if (!output || output->file || !output->temporary[0] || file_io_paths(source, destination)) { + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file publish invalid state/paths"); return -1; + } +#ifdef _WIN32 + wchar_t from[FILE_IO_PATH_MAX], to[FILE_IO_PATH_MAX]; + if (file_wide(output->temporary, from) || file_wide(destination, to)) return -1; + if (!MoveFileExW(from, to, MOVEFILE_REPLACE_EXISTING)) { + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file publish failed destination=%s winerr=%lu", destination, GetLastError()); return -1; + } +#else + if (rename(output->temporary, destination)) { + DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file publish failed destination=%s errno=%d", destination, errno); return -1; + } +#endif + output->temporary[0] = 0; + DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "file published destination=%s", destination); + return 0; +} diff --git a/lib/file_io.h b/lib/file_io.h new file mode 100644 index 00000000..f27f3fc5 --- /dev/null +++ b/lib/file_io.h @@ -0,0 +1,37 @@ +/* file_io — синхронные файловые операции для workers: UTF-8 пути, идентичность и публикация через соседний temp. + * inspect: 0 найден, 1 отсутствует, -1 ошибка; остальные операции 0/-1, paths также 1 для одного файла. + * Temp обнулить → open → писать в file → finish → publish; discard обязателен также при ошибке. + * Исходник и каталоги должны оставаться стабильными на время операции. Это не защита от подмены родительских каталогов. + * Publish заменяет имя атомарно, но не обещает сохранность при аварии питания. POSIX temp имеет права 0600. */ +#ifndef FILE_IO_H +#define FILE_IO_H +#include +#include + +#define FILE_IO_PATH_MAX 4096 +enum file_io_type { FILE_IO_REGULAR = 1, FILE_IO_DIRECTORY, FILE_IO_LINK, FILE_IO_OTHER }; +struct file_io_info { + int type; // обычный файл, каталог, ссылка/reparse point или другой объект + int64_t size; // размер в байтах, без сужения до int + uint64_t identity[3]; // идентификатор файла внутри файловой системы, сравнивать весь массив +}; +struct file_io_temp { + char temporary[FILE_IO_PATH_MAX]; // только собственный temp; пусто после discard/publish + FILE *file; // открытый wb+; NULL после finish +}; + +// path — UTF-8; follow!=0 разрешает конечную ссылку. info обязателен, заполняется только при успехе. +int file_io_inspect(const char *path, int follow, struct file_io_info *info); +// Открыть обычный файл для чтения; путь UTF-8. NULL при ошибке, fclose принадлежит вызывающему. +FILE *file_io_open_read(const char *path); +// Источник должен быть regular; назначение отсутствует или regular без конечной ссылки. 1 — один файл, 0 — разные, -1 — ошибка. +int file_io_paths(const char *source, const char *destination); +// Создать эксклюзивный temp рядом с destination; структура заранее обнулена, даже после ошибки вызвать discard. +int file_io_temp_open(struct file_io_temp *output, const char *destination); +// Проверить flush/close, обнулить file при любом результате; temp остаётся собственностью output. +int file_io_temp_finish(struct file_io_temp *output); +// Закрыть/удалить собственный temp; повторный вызов допустим. Ошибки очистки логируются. +void file_io_temp_discard(struct file_io_temp *output); +// Повторно проверить paths и заменить destination закрытым temp; одинаковые файлы отклоняются. +int file_io_temp_publish(struct file_io_temp *output, const char *source, const char *destination); +#endif