From a9e137fcc7b174e45ba8e55d1cc75fae5767493e Mon Sep 17 00:00:00 2001 From: Mikhail Romanko Date: Tue, 18 Aug 2026 10:13:30 +0300 Subject: [PATCH] Initial commit --- .github/workflows/ci.yaml | 89 +++++++++++++++++++ .gitignore | 68 ++++++++++++++ CMakeLists.txt | 25 ++++++ CgeFs.h | 60 +++++++++++++ LICENSE | 12 +++ Makefile.lint | 29 ++++++ Makefile.mingw | 23 +++++ Makefile.posix | 33 +++++++ Makefile.win32 | 23 +++++ README.md | 7 ++ posix/Dir.c | 135 ++++++++++++++++++++++++++++ posix/Stat.c | 114 ++++++++++++++++++++++++ win32/Dir.c | 180 ++++++++++++++++++++++++++++++++++++++ win32/Stat.c | 149 +++++++++++++++++++++++++++++++ 14 files changed, 947 insertions(+) create mode 100644 .github/workflows/ci.yaml create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 CgeFs.h create mode 100644 LICENSE create mode 100644 Makefile.lint create mode 100644 Makefile.mingw create mode 100644 Makefile.posix create mode 100644 Makefile.win32 create mode 100644 README.md create mode 100644 posix/Dir.c create mode 100644 posix/Stat.c create mode 100644 win32/Dir.c create mode 100644 win32/Stat.c diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..01e9e41 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,89 @@ +name: CI + +on: + push: + branches: [trunk] + pull_request: + branches: [trunk] + +jobs: + build-and-analyze: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install tools + run: | + sudo apt-get update -qq + sudo apt-get install -y build-essential clang clang-tools clang-tidy cppcheck gcc-mingw-w64-x86-64 cmake + + - name: Build with GCC + run: | + make -f Makefile.posix clean + make -f Makefile.posix CC=gcc CFLAGS="-std=c99 -Wall -Wextra -Wpedantic -Werror -O2" + + - name: Build with Clang + run: | + make -f Makefile.posix clean + make -f Makefile.posix CC=clang CFLAGS="-std=c99 -Wall -Wextra -Wpedantic -Werror -O2" + + - name: Build with MinGW + run: | + make -f Makefile.posix clean + make -f Makefile.mingw CC=x86_64-w64-mingw32-gcc AR=x86_64-w64-mingw32-ar CFLAGS="-std=c99 -Wall -Wextra -Wpedantic -Werror -O2" + + - name: Build with CMake + run: | + cmake -S . -B build + cmake --build build + + - name: Linting checks + run: | + echo "Running all linting checks..." + FAILED=0 + + # cppcheck + echo "=== Check 1/4: cppcheck ===" + if make -f Makefile.lint cppcheck; then + echo "cppcheck: PASSED" + else + echo "cppcheck: FAILED" + FAILED=1 + fi + + # clang-tidy + echo "=== Check 2/4: clang-tidy ===" + if make -f Makefile.lint clang-tidy; then + echo "clang-tidy: PASSED" + else + echo "clang-tidy: FAILED" + FAILED=1 + fi + + # scan-build + echo "=== Check 3/4: scan-build ===" + if make -f Makefile.lint scan-build; then + echo "scan-build: PASSED" + else + echo "scan-build: FAILED" + FAILED=1 + fi + + # security-check + echo "=== Check 4/4: security-check ===" + if make -f Makefile.lint security-check; then + echo "security-check: PASSED" + else + echo "security-check: FAILED" + FAILED=1 + fi + + # Final result + if [ $FAILED -ne 0 ]; then + echo "One or more linting checks failed." + exit 1 + else + echo "All linting checks passed." + fi \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f765426 --- /dev/null +++ b/.gitignore @@ -0,0 +1,68 @@ +# ---> C +# Prerequisites +*.d + +# Object files +*.o +*.ko +*.obj +*.elf + +# Linker output +*.ilk +*.map +*.exp + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su +*.idb +*.pdb + +# Kernel Module Compile Results +*.mod* +*.cmd +.tmp_versions/ +modules.order +Module.symvers +Mkfile.old +dkms.conf + +# ---> CMake +CMakeLists.txt.user +CMakeCache.txt +CMakeFiles +CMakeScripts +Testing +Makefile +cmake_install.cmake +install_manifest.txt +compile_commands.json +CTestTestfile.cmake +_deps +CMakeUserPresets.json + diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..bd5a2e9 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,25 @@ +cmake_minimum_required(VERSION 3.10) +project(CgeArgs LANGUAGES C) + +set(CMAKE_C_STANDARD 99) +set(CMAKE_C_STANDARD_REQUIRED ON) + +set(SOURCES + Args.c +) + +set(HEADERS + CgeArgs.h +) + +add_library(CgeArgs STATIC ${SOURCES} ${HEADERS}) + +target_include_directories(CgeArgs PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + +install(TARGETS CgeArgs + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib + RUNTIME DESTINATION bin +) + +install(FILES ${HEADERS} DESTINATION include) diff --git a/CgeFs.h b/CgeFs.h new file mode 100644 index 0000000..2813ddf --- /dev/null +++ b/CgeFs.h @@ -0,0 +1,60 @@ +#ifndef CGE_FS_H +#define CGE_FS_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +typedef struct CgeFsDir CgeFsDir; + +#if defined(_WIN32) || defined(_WIN64) +#define CGE_FS_NAME_MAX 512 +#elif defined(__NetBSD__) +#define CGE_FS_NAME_MAX 512 +#else +#define CGE_FS_NAME_MAX 256 +#endif + +enum CgeFsCode { + CGE_FS_EOK, + CGE_FS_EUNKNOWN, + CGE_FS_EEOF, + CGE_FS_ENOENT, + CGE_FS_EEXIST, + CGE_FS_EACCES, + CGE_FS_ENOTDIR, + CGE_FS_EMFILE, + CGE_FS_ENAMETOOLONG, + CGE_FS_EIO, +}; + +enum CgeFsType { + CGE_FS_FILE, + CGE_FS_DIR, +}; + +typedef struct CgeFsInfo { + char name[CGE_FS_NAME_MAX]; + int type; + uint64_t size; + uint64_t mtime; +} CgeFsInfo; + +CgeFsDir *CgeFsOpen(const char *path, int *result); +int CgeFsNext(CgeFsDir *d, CgeFsInfo *info, int *result); +void CgeFsClose(CgeFsDir *d); + +int CgeFsStat(const char *path, CgeFsInfo *info, int *result); +int CgeFsCreateDir(const char *path, int *result); +int CgeFsRemoveDir(const char *path, int *result); +int CgeFsDelete(const char *path, int *result); +int CgeFsRename(const char *from, const char *to, int *result); +int CgeFsCopy(const char *from, const char *to, int *result); + +#ifdef __cplusplus +} +#endif + +#endif /* CGE_FS_H */ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3d44ffd --- /dev/null +++ b/LICENSE @@ -0,0 +1,12 @@ +Copyright (C) 2026 by blankhex me@blankhex.com + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/Makefile.lint b/Makefile.lint new file mode 100644 index 0000000..cac9110 --- /dev/null +++ b/Makefile.lint @@ -0,0 +1,29 @@ +# Makefile.lint - Individual linting targets + +.PHONY: all +all: cppcheck clang-tidy scan-build security-check + @echo "All linting scheduled" + +.PHONY: cppcheck +cppcheck: + @echo "Running cppcheck..." + @cppcheck --enable=warning,performance,portability --std=c99 --quiet -Iinclude -I. . + +.PHONY: clang-tidy +clang-tidy: + @echo "Running clang-tidy..." + @for file in $$(find . -name "*.c" -type f -not -path "*/win32/*"); do \ + clang-tidy "$$file" -- -I. -Iinclude -std=c99; \ + done + +.PHONY: scan-build +scan-build: + @echo "Running scan-build..." + @scan-build --status-bugs make -f Makefile.posix clean all CC=clang + +.PHONY: security-check +security-check: + @echo "Scanning for unsafe C functions..." + @grep -rnE '\b(strcpy|strcat|sprintf|gets|scanf|sscanf|realpath|mktemp|tempnam|tmpnam|getwd|getlogin)\b' \ + . --include="*.c" --include="*.h" | grep -v "^Binary file" && \ + exit 1 || exit 0 \ No newline at end of file diff --git a/Makefile.mingw b/Makefile.mingw new file mode 100644 index 0000000..6247ebd --- /dev/null +++ b/Makefile.mingw @@ -0,0 +1,23 @@ +# MinGW Makefile for CgeFs + +CC = gcc +AR = ar +CFLAGS = -std=c99 -O2 -Wall -Wextra +ARFLAGS = rcs +TARGET = libCgeFs.a + +SOURCES = win32/Dir.c win32/Stat.c +OBJECTS = $(SOURCES:.c=.o) + +.PHONY: all clean + +all: $(TARGET) + +$(TARGET): $(OBJECTS) + $(AR) $(ARFLAGS) $@ $^ + +%.o: %.c CgeFs.h + $(CC) $(CFLAGS) -c $< -o $@ + +clean: + del $(OBJECTS) $(TARGET) 2>nul || exit 0 diff --git a/Makefile.posix b/Makefile.posix new file mode 100644 index 0000000..0391bd5 --- /dev/null +++ b/Makefile.posix @@ -0,0 +1,33 @@ +# POSIX Makefile for CgeFs + +CC = gcc +AR = ar +CFLAGS = -std=c99 -O2 -Wall -Wextra -fPIC +ARFLAGS = rcs +TARGET = libCgeFs.a + +SOURCES = posix/Dir.c posix/Stat.c +OBJECTS = $(SOURCES:.c=.o) + +.PHONY: all clean install + +all: $(TARGET) + +$(TARGET): $(OBJECTS) + $(AR) $(ARFLAGS) $@ $^ + +%.o: %.c CgeFs.h + $(CC) $(CFLAGS) -c $< -o $@ + +clean: + rm -f $(OBJECTS) $(TARGET) + +install: $(TARGET) + cp $(TARGET) /usr/local/lib/ + cp CgeFs.h /usr/local/include/ + ldconfig || echo "Run ldconfig manually if needed" + +uninstall: + rm -f /usr/local/lib/libCgeFs.a + rm -f /usr/local/include/CgeFs.h + ldconfig || true diff --git a/Makefile.win32 b/Makefile.win32 new file mode 100644 index 0000000..75fa4a9 --- /dev/null +++ b/Makefile.win32 @@ -0,0 +1,23 @@ +# Makefile.win32 for MSVC (NMake) +# Usage: Open "x86 Native Tools Command Prompt", then: +# nmake -f Makefile.win32 + +CC = cl +LIB = lib +CFLAGS = /c /nologo /W3 /O2 +LIBFLAGS = /nologo +TARGET = CgeFs.lib + +SOURCES = win32/Dir.c win32/Stat.c +OBJECTS = $(SOURCES:.c=.obj) + +$(TARGET): $(OBJECTS) + $(LIB) $(LIBFLAGS) /OUT:$(TARGET) $(OBJECTS) + +{.}.c{}.obj: + $(CC) $(CFLAGS) /Fo$@ $< + +clean: + del $(OBJECTS) $(TARGET) 2>nul + +.PHONY: clean diff --git a/README.md b/README.md new file mode 100644 index 0000000..e1f307f --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# CgeFS - filesystem abstraction + +TODO + +## License + +0BSD - a permissive license with no attribution required. diff --git a/posix/Dir.c b/posix/Dir.c new file mode 100644 index 0000000..534718f --- /dev/null +++ b/posix/Dir.c @@ -0,0 +1,135 @@ +#include "../CgeFs.h" +#include +#include +#include +#include +#include +#include +#include +#include + +typedef struct CgeFsDir { + DIR *dir; + char path[PATH_MAX]; + size_t size; +} CgeFsDir; + +static int mapErrno(int value) { + switch (value) { + case ENOENT: + return CGE_FS_ENOENT; + + case EACCES: + return CGE_FS_EACCES; + + case ENOTDIR: + return CGE_FS_ENOTDIR; + + default: + return CGE_FS_EUNKNOWN; + } +} + +CgeFsDir *CgeFsOpen(const char *path, int *result) { + CgeFsDir *d; + DIR *dir; + size_t size; + + dir = opendir(path); + if (!dir) { + if (result) + *result = mapErrno(errno); + return NULL; + } + + d = malloc(sizeof(*d)); + if (!d) { + closedir(dir); + if (result) + *result = CGE_FS_EUNKNOWN; + return NULL; + } + + d->dir = dir; + size = strlen(path); + if (size < PATH_MAX) { + memcpy(d->path, path, size); + if (size > 0 && d->path[size - 1] != '/') { + d->path[size] = '/'; + d->path[size + 1] = 0; + d->size = size + 1; + } else { + memcpy(d->path, path, size); + d->path[size] = 0; + d->size = size; + } + } else { + d->path[0] = 0; + d->size = 0; + } + + return d; +} + +int CgeFsNext(CgeFsDir *d, CgeFsInfo *info, int *result) { + struct dirent *ent; + struct stat st; + char path[PATH_MAX]; + + while ((ent = readdir(d->dir)) != NULL) { + if (!strcmp(ent->d_name, ".")) + continue; + if (!strcmp(ent->d_name, "..")) + continue; + + if (d->size > 0 && d->size < PATH_MAX - 2) { + strncpy(path, d->path, d->size); + path[d->size] = '\0'; + strncat(path, ent->d_name, PATH_MAX - strlen(path) - 1); + } else { + strncpy(path, ent->d_name, PATH_MAX - 1); + path[PATH_MAX - 1] = '\0'; + } + + if (stat(path, &st) != 0) + continue; + + strncpy(info->name, ent->d_name, CGE_FS_NAME_MAX - 1); + info->name[CGE_FS_NAME_MAX - 1] = '\0'; + info->type = S_ISDIR(st.st_mode) ? CGE_FS_DIR : CGE_FS_FILE; + info->size = (uint64_t)st.st_size; + info->mtime = (uint64_t)st.st_mtime; + + if (result) + *result = CGE_FS_EOK; + return 1; + } + + return 0; +} + +void CgeFsClose(CgeFsDir *d) { + if (!d) + return; + + closedir(d->dir); + free(d); +} + +int CgeFsCreateDir(const char *path, int *result) { + if (mkdir(path, 0777) != 0) { + if (result) + *result = mapErrno(errno); + return 0; + } + return 1; +} + +int CgeFsRemoveDir(const char *path, int *result) { + if (rmdir(path) != 0) { + if (result) + *result = mapErrno(errno); + return 0; + } + return 1; +} diff --git a/posix/Stat.c b/posix/Stat.c new file mode 100644 index 0000000..84c5120 --- /dev/null +++ b/posix/Stat.c @@ -0,0 +1,114 @@ +#include "../CgeFs.h" +#include +#include +#include +#include +#include +#include +#include +#include + +static int mapErrno(int err) { + switch (err) { + case ENOENT: + return CGE_FS_ENOENT; + + case EEXIST: + return CGE_FS_EEXIST; + + case EACCES: + return CGE_FS_EACCES; + + case ENOTDIR: + return CGE_FS_ENOTDIR; + + case EMFILE: + return CGE_FS_EMFILE; + + case ENAMETOOLONG: + return CGE_FS_ENAMETOOLONG; + + case EIO: + return CGE_FS_EIO; + + default: + return CGE_FS_EUNKNOWN; + } +} + +int CgeFsStat(const char *path, CgeFsInfo *info, int *result) { + struct stat st; + const char *p; + const char *base; + + if (stat(path, &st) != 0) { + if (result) + *result = mapErrno(errno); + return 0; + } + + base = NULL; + for (p = path; *p; p++) + if (*p == '/') base = p + 1; + + if (!base) + base = path; + + strncpy(info->name, base, CGE_FS_NAME_MAX - 1); + info->name[CGE_FS_NAME_MAX - 1] = '\0'; + info->type = S_ISDIR(st.st_mode) ? CGE_FS_DIR : CGE_FS_FILE; + info->size = (uint64_t)st.st_size; + info->mtime = (uint64_t)st.st_mtime; + return 1; +} + +int CgeFsDelete(const char *path, int *result) { + if (unlink(path) != 0) { + if (result) + *result = mapErrno(errno); + return 0; + } + return 1; +} + +int CgeFsRename(const char *from, const char *to, int *result) { + if (rename(from, to) != 0) { + if (result) + *result = mapErrno(errno); + return 0; + } + return 1; +} + +int CgeFsCopy(const char *from, const char *to, int *result) { + FILE *fin, *fout; + char buf[PATH_MAX]; + size_t n; + + if (!(fin = fopen(from, "rb"))) { + if (result) + *result = mapErrno(errno); + return 0; + } + + if (!(fout = fopen(to, "wb"))) { + fclose(fin); + if (result) + *result = mapErrno(errno); + return 0; + } + + while ((n = fread(buf, 1, sizeof(buf), fin)) > 0) { + if (fwrite(buf, 1, n, fout) != n) { + if (result) + *result = CGE_FS_EIO; + fclose(fin); + fclose(fout); + return 0; + } + } + + fclose(fin); + fclose(fout); + return 1; +} diff --git a/win32/Dir.c b/win32/Dir.c new file mode 100644 index 0000000..de8769d --- /dev/null +++ b/win32/Dir.c @@ -0,0 +1,180 @@ +#define WIN32_LEAN_AND_MEAN +#include "../CgeFs.h" +#include +#include +#include +#include + +#ifndef MAX_PATH +#define MAX_PATH 260 +#endif + +typedef struct CgeFsDir { + HANDLE handle; + WIN32_FIND_DATAW findData; + int hasEntry; +} CgeFsDir; + +static int utf8ToWchar(const char *utf8, wchar_t *out, int size) { + int len; + + if (!utf8 || !out || size <= 0) + return 0; + + len = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, out, size); + if (len <= 0 || len > size) { + out[0] = L'\0'; + return 0; + } + + return 1; +} + +static void wcharToUtf8(const wchar_t *w, char *out, int size) { + int len; + + if (!w || !out || size <= 0) { + out[0] = '\0'; + } + + len = WideCharToMultiByte(CP_UTF8, 0, w, -1, out, size, NULL, NULL); + if (len <= 0 || len > size) + out[0] = '\0'; +} + +static int mapWinError(DWORD value) { + switch (value) { + case ERROR_FILE_NOT_FOUND: + case ERROR_PATH_NOT_FOUND: + return CGE_FS_ENOENT; + + case ERROR_ACCESS_DENIED: + return CGE_FS_EACCES; + + case ERROR_DIRECTORY: + return CGE_FS_ENOTDIR; + + default: + return CGE_FS_EUNKNOWN; + } +} + +static uint64_t fileTimeToUint64(const FILETIME *ft) { + ULARGE_INTEGER u; + u.LowPart = ft->dwLowDateTime; + u.HighPart = ft->dwHighDateTime; + return u.QuadPart / 10000000ULL - 11644473600ULL; +} + +CgeFsDir *CgeFsOpen(const char *path, int *result) { + wchar_t findPathW[MAX_PATH]; + char findPathA[MAX_PATH * 4]; + size_t size; + CgeFsDir *d; + + snprintf(findPathA, sizeof(findPathA), "%s\\*", path); + + d = malloc(sizeof(*d)); + if (!d) { + if (result) + *result = CGE_FS_EUNKNOWN; + return NULL; + } + + d->handle = INVALID_HANDLE_VALUE; + d->hasEntry = 0; + + if (!utf8ToWchar(findPathA, findPathW, MAX_PATH)) { + if (result) + *result = CGE_FS_EUNKNOWN; + free(d); + return NULL; + } + + d->handle = FindFirstFileW(findPathW, &d->findData); + if (d->handle == INVALID_HANDLE_VALUE) { + if (result) + *result = mapWinError(GetLastError()); + free(d); + return NULL; + } + + d->hasEntry = 1; + return d; +} + +int CgeFsNext(CgeFsDir *d, CgeFsInfo *info, int *result) { + WIN32_FIND_DATAW *ent; + char nameUtf8[CGE_FS_NAME_MAX]; + + while (d->hasEntry) { + ent = &d->findData; + d->hasEntry = 0; + + if (!wcscmp(ent->cFileName, L".") || !wcscmp(ent->cFileName, L"..")) { + if (FindNextFileW(d->handle, &d->findData) != 0) + d->hasEntry = 1; + continue; + } + + wcharToUtf8(ent->cFileName, nameUtf8, sizeof(nameUtf8)); + + strncpy(info->name, nameUtf8, CGE_FS_NAME_MAX - 1); + info->name[CGE_FS_NAME_MAX - 1] = '\0'; + info->type = (ent->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ? CGE_FS_DIR : CGE_FS_FILE; + info->size = ((uint64_t)ent->nFileSizeHigh << 32) | ent->nFileSizeLow; + info->mtime = (uint64_t)(fileTimeToUint64(&ent->ftLastWriteTime)); + + if (FindNextFileW(d->handle, &d->findData) != 0) + d->hasEntry = 1; + + return 1; + } + + if (result) + *result = CGE_FS_EEOF; + return 0; +} + +void CgeFsClose(CgeFsDir *d) { + if (!d) + return; + + if (d->handle != INVALID_HANDLE_VALUE) + FindClose(d->handle); + free(d); +} + +int CgeFsCreateDir(const char *path, int *result) { + wchar_t wpath[MAX_PATH]; + + if (!utf8ToWchar(path, wpath, MAX_PATH)) { + if (result) + *result = CGE_FS_EUNKNOWN; + return 0; + } + + if (CreateDirectoryW(wpath, NULL) == 0) { + if (result) + *result = mapWinError(GetLastError()); + return 0; + } + return 1; +} + +int CgeFsRemoveDir(const char *path, int *result) { + wchar_t wpath[MAX_PATH]; + + if (!utf8ToWchar(path, wpath, MAX_PATH)) { + if (result) + *result = CGE_FS_EUNKNOWN; + return 0; + } + + if (RemoveDirectoryW(wpath) == 0) { + if (result) + *result = mapWinError(GetLastError()); + return 0; + } + return 1; +} diff --git a/win32/Stat.c b/win32/Stat.c new file mode 100644 index 0000000..5d868b8 --- /dev/null +++ b/win32/Stat.c @@ -0,0 +1,149 @@ +#include "../CgeFs.h" +#include +#include + +static int utf8ToWchar(const char *utf8, wchar_t *out, int size) { + int len; + + if (!utf8 || !out || size <= 0) + return 0; + + len = MultiByteToWideChar(CP_UTF8, 0, utf8, -1, out, size); + if (len <= 0 || len > size) { + out[0] = L'\0'; + return 0; + } + + return 1; +} + +static int mapWinError(DWORD err) { + switch (err) { + case ERROR_FILE_NOT_FOUND: + case ERROR_PATH_NOT_FOUND: + return CGE_FS_ENOENT; + + case ERROR_ALREADY_EXISTS: + return CGE_FS_EEXIST; + + case ERROR_ACCESS_DENIED: + return CGE_FS_EACCES; + + case ERROR_DIRECTORY: + return CGE_FS_ENOTDIR; + + case ERROR_TOO_MANY_OPEN_FILES: + return CGE_FS_EMFILE; + + case ERROR_FILENAME_EXCED_RANGE: + return CGE_FS_ENAMETOOLONG; + + default: + return CGE_FS_EUNKNOWN; + } +} + +static uint64_t fileTimeToUint64(const FILETIME *ft) { + ULARGE_INTEGER u; + u.LowPart = ft->dwLowDateTime; + u.HighPart = ft->dwHighDateTime; + return u.QuadPart / 10000000ULL - 11644473600ULL; +} + +int CgeFsStat(const char *path, CgeFsInfo *info, int *result) { + wchar_t wpath[MAX_PATH]; + WIN32_FIND_DATAW fd; + HANDLE handle; + const char *p; + const char *base; + + if (!utf8ToWchar(path, wpath, MAX_PATH)) { + if (result) + *result = CGE_FS_EUNKNOWN; + return 0; + } + + handle = FindFirstFileW(wpath, &fd); + if (handle == INVALID_HANDLE_VALUE) { + if (result) + *result = mapWinError(GetLastError()); + return 0; + } + + base = NULL; + for (p = path; *p; p++) + if (*p == '/' || *p == '\\') base = p + 1; + + if (!base) + base = path; + + strncpy(info->name, base, CGE_FS_NAME_MAX - 1); + info->name[CGE_FS_NAME_MAX - 1] = '\0'; + info->type = (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ? CGE_FS_DIR : CGE_FS_FILE; + info->size = ((uint64_t)fd.nFileSizeHigh << 32) | fd.nFileSizeLow; + info->mtime = (uint64_t)(fileTimeToUint64(&fd.ftLastWriteTime)); + + FindClose(handle); + return 1; +} + +int CgeFsDelete(const char *path, int *result) { + wchar_t wpath[MAX_PATH]; + + if (!utf8ToWchar(path, wpath, MAX_PATH)) { + if (result) + *result = CGE_FS_EUNKNOWN; + return 0; + } + + if (DeleteFileW(wpath) == 0) { + if (result) + *result = mapWinError(GetLastError()); + return 0; + } + return 1; +} + +int CgeFsRename(const char *from, const char *to, int *result) { + wchar_t wfrom[MAX_PATH], wto[MAX_PATH]; + + if (!utf8ToWchar(from, wfrom, MAX_PATH)) { + if (result) + *result = CGE_FS_EUNKNOWN; + return 0; + } + if (!utf8ToWchar(to, wto, MAX_PATH)) { + if (result) + *result = CGE_FS_EUNKNOWN; + return 0; + } + + if (MoveFileExW(wfrom, wto, MOVEFILE_REPLACE_EXISTING) == 0) { + if (result) + *result = mapWinError(GetLastError()); + return 0; + } + return 1; +} + +int CgeFsCopy(const char *from, const char *to, int *result) { + wchar_t wfrom[MAX_PATH], wto[MAX_PATH]; + + if (!utf8ToWchar(from, wfrom, MAX_PATH)) { + if (result) + *result = CGE_FS_EUNKNOWN; + return 0; + } + if (!utf8ToWchar(to, wto, MAX_PATH)) { + if (result) + *result = CGE_FS_EUNKNOWN; + return 0; + } + + if (CopyFileW(wfrom, wto, FALSE) == 0) { + if (result) + *result = mapWinError(GetLastError()); + return 0; + } + return 1; +}