commit a9e137fcc7b174e45ba8e55d1cc75fae5767493e Author: Mikhail Romanko Date: Tue Aug 18 10:13:30 2026 +0300 Initial commit 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; +}