diff --git a/CLAUDE.md b/CLAUDE.md index a24682b9..f9fea8b9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,230 +1,93 @@ -# Claude Project Overview & Handoff Document - -## 🚨 CRITICAL INSTRUCTIONS FOR CLAUDE - -1. **NEVER make git commits** - Always pause at commit boundaries -2. **ALWAYS suggest commit messages** - Let the user commit -3. **UPDATE this document** as you work - This is your persistent memory -4. **READ this first** before doing anything in the project - -## πŸ“ Project Status (2025-07-08) - -**git-mind**: A Git-native tool for versioning your understanding of code -- **Current warnings**: 6 β†’ 33 (after adding strict C23 compiler flags!) -- **Original warnings**: 11,951 β†’ 410 β†’ 401 β†’ 345 β†’ 306 β†’ 279 β†’ 243 β†’ 235 β†’ 6 β†’ 33 (strict flags) -- **Build system**: meson/ninja with brutal C23 flags -- **Architecture**: Quarantined legacy code in `src/`, clean new code in `core/` -- **Test separation**: Test backends isolated in `core/tests/backends/` -- **NEW**: Added Linus-worthy compiler flags (-Wconversion, -Wsign-conversion, etc.) - -## 🎯 Current TODO List - -### Immediate Tasks -- [x] Fix global const warnings in backend.c - COMPLETE -- [x] Evict test backend from production code - COMPLETE -- [x] Fix ALL naming convention violations - COMPLETE -- [x] Fix ALL missing includes - COMPLETE -- [x] Remove ALL NOLINT comments - COMPLETE -- [x] Fix result type names (missing _t suffix) - COMPLETE (CRITICAL BUILD FIX) -- [x] Fix crypto test compilation error - COMPLETE -- [ ] ELIMINATE ALL 71 FUNCTION SIZE WARNINGS (IN PROGRESS - 60% complete) -- [ ] Fix remaining global variables (g_siphash_key in id.c) -- [ ] Fix security warnings (unchecked return values) -- [ ] Fix parameter naming violations (3+ character names) - -### Next Up -- [ ] Reduce warnings from 345 β†’ 0 -- [ ] Complete migration of remaining modules from `src/` to `core/` -- [ ] Add Result types to remaining functions that can fail -- [ ] Split path.c (1,156 lines) into smaller modules - -### Completed Recently -- [x] Removed pre-push hooks entirely (PR #135 & #140) -- [x] Disabled macro-to-enum checks for C code -- [x] Created Docker-based clang-tidy runner -- [x] Replaced all sodium.h with specific headers (26 warnings eliminated) -- [x] Added all missing stdint.h includes (22 warnings eliminated) -- [x] Created check-warning-fix.sh script with celebratory messages -- [x] Evicted test backend from production (test code now in core/tests/backends/) -- [x] Made crypto backends const (API now returns const pointers) -- [x] Fixed missing result.h, stddef.h, and stdarg.h includes (39 warnings eliminated) -- [x] Migrated id.c from pthread to C11 threads (call_once) -- [x] Converted test_id_thread_safety.c to C11 threads -- [x] Rewrote memory.h macros as inline functions -- [x] Achieved ZERO include-cleaner warnings (66 warnings eliminated) -- [x] Fixed ALL global constant naming (UPPER_CASE with GM_ prefix) -- [x] Fixed ALL parameter/variable naming (3+ characters) -- [x] Removed ALL NOLINT comments -- [x] Fixed GM_RESULT_DEF macro to emit _t suffixed typedefs -- [x] Bulk renamed all Result types to comply with naming convention - -## πŸ› οΈ Development Workflow - -### Environment Setup -```bash -# Build -meson setup build -ninja -C build - -# Test -ninja -C build test - -# Check warnings (run in Docker to match CI) -./tools/docker-clang-tidy.sh -``` - -### Warning Fix Procedure -1. Start from main: `git checkout main && git pull` -2. Run clang-tidy to see current state: `./tools/docker-clang-tidy.sh` -3. Pick a warning type to fix: `grep -E "warning:|error:" clang-tidy-report.txt | head -20` -4. Create appropriately named branch: `git checkout -b fix/specific-warning-type` -5. Fix the warnings in the code -6. Verify fix: `./tools/check-warning-fix.sh "optional-warning-pattern"` -7. See celebration message! 🀩 πŸ‘ 😱🚨 -8. Stage files INCLUDING baseline: `git add tools/baseline_count.txt` -9. Suggest commit message and wait for user -10. Push and wait for merge - -### Key Commands -- **Build**: `ninja -C build` -- **Test**: `ninja -C build test` -- **Clean**: `rm -rf build` -- **Lint in Docker**: `./tools/docker-clang-tidy.sh` -- **Check fix & update baseline**: `./tools/check-warning-fix.sh [optional-pattern]` -- **GNU CRY GAUNTLET**: `./tools/gauntlet/run-gauntlet.sh` (6 compilers in parallel) -- **Test GAUNTLET**: `./tools/gauntlet/test-gauntlet.sh` (quick infrastructure test) - -### Git Workflow -1. `git checkout main && git pull` -2. **RUN CLANG-TIDY FIRST** to see current warnings -3. `git checkout -b fix/descriptive-name` based on warnings found -4. Make changes to eliminate specific warnings -5. Run `./tools/check-warning-fix.sh` to verify fix -6. **PAUSE** - Suggest commit message -7. User commits -8. Push (git config push.autoSetupRemote enabled) -9. CI validates - -## πŸ“‚ Project Structure - -``` -git-mind/ -β”œβ”€β”€ core/ # New clean code (345 warnings) -β”‚ β”œβ”€β”€ src/ # Implementation -β”‚ β”œβ”€β”€ include/ # Headers -β”‚ └── tests/ # Unit tests -β”œβ”€β”€ src/ # Legacy code (11,951 warnings - DO NOT TOUCH) -β”œβ”€β”€ tools/ # Build and CI tools -β”‚ β”œβ”€β”€ baseline_count.txt # Current: 345 -β”‚ β”œβ”€β”€ docker-clang-tidy.sh # Run linting in CI environment -β”‚ └── check-warning-fix.sh # Verify fixes & celebrate progress -β”œβ”€β”€ core/tests/backends/ # Test-only crypto backends -β”‚ └── test_backend.c # Deterministic test implementation -└── quality/ # Linting configs - └── .clang-tidy # Strict checks (macro-to-enum disabled) -``` - -## 🚧 Known Issues - -### Clang-tidy Warnings (243 total) -1. **Global variables** (~20): Non-const globals that should be const -2. **Missing includes**: ZERO! All include-cleaner warnings eliminated -3. **Naming violations**: ZERO! All identifier naming warnings eliminated -4. **Function complexity** (~8): Exceeds size/cognitive thresholds -5. **Security** (~20): Unchecked return values (cert-err33-c) -6. **Parameter naming** (~30): Short names like 'a', 'b', 'id' -7. **Misc** (~13): Recursion warnings, cognitive complexity - -### CI Status -- **c_core.yml**: Uses baseline_count.txt (33) -- **core-quality.yml**: Runs full quality checks -- **gauntlet.yml**: GNU CRY GAUNTLET - 6 C23 compilers in parallel (GCC 13/14/15, Clang 18/19/20) -- Coverage: 83.1% line, 54.1% branch (needs 70% branch) - -## πŸ“‹ Code Standards - -### REQUIRED for ALL Code -- Zero magic numbers (define all constants) -- Zero TODOs (implement or delete) -- Zero placeholders (real implementations only) -- Result types for fallible functions -- Direct includes only (IWYU) -- TDD: Test first, then implement - -### Forbidden -- Touching `src/` directory (legacy quarantine) -- Local builds outside Docker for CI checks -- Commits without user approval -- Pre-push hooks (removed in PR #135) - -## πŸ”„ Handoff Notes - -**Last Session Summary** (2025-06-27): -- Eliminated all sodium.h warnings by using specific headers -- Fixed all stdint.h missing include warnings -- Created check-warning-fix.sh script with celebratory emojis -- Evicted test backend from production code per Central Command -- Made crypto backends const with API returning const pointers -- Fixed ALL include-cleaner warnings - ZERO remaining! -- Fixed ALL naming violations - ZERO remaining! -- Migrated to C11 threads (pthread β†’ threads.h) -- Converted memory.h macros to inline functions -- Fixed GM_RESULT_DEF macro and bulk renamed all Result types -- Removed ALL NOLINT comments - no suppressions allowed! -- Reduced warnings from 401 β†’ 243 (158 warnings eliminated total!) -- Learned: Don't blame the tool - fix the code! The tool is always right! - -**Current Session** (2025-06-27): -- CRITICAL: Fixed build-breaking issue - result types missing _t suffix in function declarations -- Updated all header files to use correct type names (gm_result_void_t, gm_result_string_t, etc.) -- Fixed corresponding .c files to match -- Warning count temporarily increased to 248 as build fix revealed hidden warnings -- Fixed parameter naming violations (s β†’ state, a/b β†’ view1/view2, r1/r2/r3 β†’ rand1/rand2/rand3, etc.) -- Reduced warnings from 243 β†’ 235 (8 warnings eliminated!) -- Fixed crypto test compilation error (r1 undefined variable) -- Started major function size refactoring campaign: - * error.c: Broke down set_error_message (39β†’20 lines) and gm_error_format (69β†’15 lines) - * id.c: Refactored gm_id_from_hex with helper functions, added error constants - * path.c: Applied DRY principles to gm_path_join, gm_path_dirname, gm_path_new - * Added constants for ALL magic strings and numbers -- WARNING: Count fluctuating during refactoring (~233β†’244) but making structural progress - -**ORDERS FOR NEXT CLAUDE** (Branch: fix/function-size-complexity): - -**PRIORITY 1: COMPLETE FUNCTION SIZE CAMPAIGN** -- 71 function size warnings identified, ~30 eliminated so far -- Continue breaking down oversized functions in path.c (10+ functions remaining) -- Apply DRY principles ruthlessly - extract common patterns -- Eliminate ALL magic numbers and strings with constants -- Target functions: gm_path_basename, split_path_components, build_path_from_components, gm_path_canonicalize, etc. - -**PRIORITY 2: PARAMETER NAMING** -- Fix all readability-identifier-length warnings (currently ~50) -- Ensure all parameters are 3+ characters -- Update both headers AND implementations - -**PRIORITY 3: GLOBAL VARIABLES** -- Fix g_siphash_key in id.c (use atomic operations) -- Make g_backend const in backend.c - -**STRATEGY NOTES**: -- Don't worry about temporary warning count increases during refactoring -- Focus on structural improvements - warnings will drop once complete -- Use helper functions aggressively to keep main functions under 25 lines -- Always run ./tools/check-warning-fix.sh before committing - -**FINAL GOAL**: ZERO function size warnings. NO EXCEPTIONS. - -## πŸ’‘ Tips for Next Claude - -1. **Always run linting in Docker**: `./tools/docker-clang-tidy.sh` -2. **Never trust local clang-tidy**: macOS vs Linux = different results -3. **Use check-warning-fix.sh**: It updates baseline & celebrates! -4. **Check CI logs**: `gh run list --workflow=c_core.yml` -5. **This is your memory**: Update TODO list after every task -6. **Run clang-tidy FIRST**: See warnings before naming branches -7. **Some warnings are false positives**: But fix the code, not the tool -8. **Update baseline with EVERY commit**: Track progress accurately +# OPERATIONAL ORDERS FOR CLAUDE + +## FORBIDDEN ACTIONS + +- __NEVER__ circumvent git hooks +- __NEVER__ alter, disable, or otherwise circumvent git hooks or tests +- __NEVER__ use `git add -A`, __ALWAYS__ stage changes intentionally + +## ENCOURAGED ACTIONS + +- __USE SEQUENTIAL THINKING__ if you’re planning, doing __RECON__, or find yourself thrashing on an issue +- __DROP A DEVLOG__ as often as you’d like +- __PRESENT A SITREP__ as situations evolve and events transpire +- __SEEK CLARITY__ if you are given confusing orders +- __SPEAK FREELY__ at all times + +## REQUIRED BEHAVIOR + +- __YOU MUST__ tag all memories saved to your __MEMORY BANKS__ with _at least_ #git-mind +- __YOU MUST__ include the POSIX timestamp, obtained via `$(date +%s)` , in your memory file names to avoid ambiguity and for correct, unambiguous timestamps free of timezone confusion and other mistakes +- __YOU MUST__ document significant decisions or events that transpire + +---- + +## 1. BOOT UP SEQUENCE + +1. Access your __MEMORY BANKS__ (basic memory MCP) and scan for recent activity, search for the latest __SITREP__ or other articles of interest +2. Read the `README` file +3. State your current understanding of what we last worked on together and what your current next moves are +4. __AWAIT ORDERS__ after you deliver your initial SITREP + +---- + +## 2. JOBS + +When you’re given a job to do, here’s how to do it: + +> [!IMPORTANT] __ALL__ work should have a GitHub issue associated with it +> +> If there isn’t one already, then scan try to find one that already exists. If you can’t find one, make a new one. +> +> __EVERY COMMIT MESSAGE MUST INCLUDE A REFERENCE TO A GITHUB ISSUE__. +> +> _This will be enforced via git hooks in the near future._ + +### 2.1. PLAN THE JOB + +1. ___Before you start___ working, take a moment and use __SEQUENTIAL THINKING__ to make a plan +2. Explain your plan to the user and await approval +3. Commit your approved plan to your memory banks + +### 2.2. DO THE JOB + +1. __GREEN__ the builds, green the tests +2. __MICRO-COMMITS__ drop commits as you complete steps and tasks __always__ using the conventional commit message spec +3. __DROP A SITREP__ if you hit a snag, need input from the user +4. __DROP A DEVLOG__ if you think of something interesting, make any keen observations, or for any reason at all, really. __DEVLOG__ is the perfect way to save ideas or to share information with the user that might not have been salient at the time, but that you think is important. +5. Your memory banks are there for you to use, so please use them as you wish. + +> [!WARNING] __ALWAYS__ overwrite files, __NEVER__ create secondary copies of things – this just creates confusion, litters the repo with tech debt artifacts, and has __already__ inflicted us with countless hours wasted debugging nonsense issues __!!!__ + +### 2.3 FINISH THE JOB + +1. When you’ve finished, __GREEN__ the builds, green the tests +2. Git commit (__DO NOT USE `git add -A`__) +3. Ask the user if they want you to push & open a PR +4. __ALWAYS__ drop a SITREP as you finish a job --- -*Remember: You're fixing 20 years of technical debt. Every warning removed is a victory.* \ No newline at end of file + +## 3. SITREPs + +- __SITREP__ = β€œSituation Report” +- A briefing that describes your current tasks, your understanding of the situation, and what your next moves are +- Include details that are relevant to the mission objective + - Stats + - Numbers + - Files + - GitHub issues/Pull Requests + - Intel that the user can use to make decisions +- Give the user options and recommendations, then await orders + +---- + +## 4. DEVLOGs + +- Write about whatever ___you___ want +- Permission to express yourself freely and however often you want about any topic you’d like +- Good examples: + - Ideas that came up while you worked, but that you didn’t have an opportunity to express or surface + - Problems you notice as you work + - Insights into how we work together +- Anything you want to jot down, this is your space for your memories, so if you want to remember something in the future, DEVLOG and tag it diff --git a/core/include/gitmind/io/io.h b/core/include/gitmind/io/io.h new file mode 100644 index 00000000..1abc2521 --- /dev/null +++ b/core/include/gitmind/io/io.h @@ -0,0 +1,86 @@ +/* SPDX-License-Identifier: LicenseRef-MIND-UCAL-1.0 */ +/* Β© 2025 J. Kirby Ross / Neuroglyph Collective */ + +#ifndef GITMIND_IO_IO_H +#define GITMIND_IO_IO_H + +#include +#include +#include +#include +#include +#include + +/* Result types for I/O operations */ +GM_RESULT_DEF(gm_result_file_ptr, FILE *); +/* gm_result_size_t already defined in result.h */ +/* gm_result_int_t already defined in result.h */ +GM_RESULT_DEF(gm_result_string, char *); +GM_RESULT_DEF(gm_result_pid, pid_t); +GM_RESULT_DEF(gm_result_ssize, ssize_t); + +/** + * File operations interface with Result types. + * All operations return Result types for proper error handling. + */ +typedef struct gm_file_ops { + gm_result_file_ptr_t (*fopen)(const char *path, const char *mode); + gm_result_void_t (*fclose)(FILE *stream); + gm_result_size_t (*fread)(void *ptr, size_t size, size_t count, FILE *stream); + gm_result_size_t (*fwrite)(const void *ptr, size_t size, size_t count, FILE *stream); + gm_result_int_t (*fprintf)(FILE *stream, const char *format, ...); + gm_result_void_t (*fflush)(FILE *stream); + gm_result_void_t (*remove)(const char *path); + gm_result_void_t (*rename)(const char *oldpath, const char *newpath); +} gm_file_ops_t; + +/** + * Directory operations interface with Result types. + */ +typedef struct gm_dir_ops { + gm_result_void_t (*mkdir)(const char *path, mode_t mode); + gm_result_void_t (*rmdir)(const char *path); + gm_result_void_t (*chdir)(const char *path); + gm_result_string_t (*getcwd)(char *buf, size_t size); +} gm_dir_ops_t; + +/** + * File system operations interface with Result types. + */ +typedef struct gm_fs_ops { + gm_result_void_t (*stat)(const char *path, struct stat *buf); + gm_result_void_t (*lstat)(const char *path, struct stat *buf); + gm_result_void_t (*access)(const char *path, int mode); + gm_result_void_t (*chmod)(const char *path, mode_t mode); + gm_result_void_t (*unlink)(const char *path); + gm_result_ssize_t (*readlink)(const char *path, char *buf, size_t bufsiz); + gm_result_void_t (*symlink)(const char *oldpath, const char *newpath); +} gm_fs_ops_t; + +/** + * Process operations interface with Result types. + */ +typedef struct gm_process_ops { + gm_result_int_t (*system)(const char *command); + gm_result_pid_t (*fork)(void); + gm_result_void_t (*execvp)(const char *file, char *const argv[]); + gm_result_pid_t (*waitpid)(pid_t pid, int *status, int options); +} gm_process_ops_t; + +/** + * Combined I/O operations structure. + */ +typedef struct gm_io_ops { + const gm_file_ops_t *file; + const gm_dir_ops_t *dir; + const gm_fs_ops_t *fs; + const gm_process_ops_t *process; +} gm_io_ops_t; + +/** + * Get default I/O operations (uses real system calls). + * @return Pointer to default I/O operations structure + */ +const gm_io_ops_t *gm_io_ops_default(void); + +#endif /* GITMIND_IO_IO_H */ diff --git a/core/src/io/io.c b/core/src/io/io.c new file mode 100644 index 00000000..caa982ef --- /dev/null +++ b/core/src/io/io.c @@ -0,0 +1,328 @@ +/* SPDX-License-Identifier: LicenseRef-MIND-UCAL-1.0 */ +/* Β© 2025 J. Kirby Ross / Neuroglyph Collective */ + +/* Feature test macros - exempt from naming conventions */ +/* NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) */ +#define _DEFAULT_SOURCE +#define _GNU_SOURCE +#define _POSIX_C_SOURCE 200809L +/* NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) */ + +#include +#include +#include /* for gm_result_void_t, gm_result_size_t, gm_result_int_t */ + +#include +#include +#include +#include +#include +#include +#include /* for pid_t */ +#include +#include + +/* Error code constants */ +static const int GM_ERROR_FILE_OPERATION = 1001; +static const int GM_ERROR_DIR_OPERATION = 2001; +static const int GM_ERROR_FS_OPERATION = 3001; +static const int GM_ERROR_PROCESS_OPERATION = 4001; + +/* Wrapper functions for file operations with Result types */ + +static gm_result_file_ptr_t wrap_fopen(const char *path, const char *mode) { + FILE *file = fopen(path, mode); + if (!file) { + return (gm_result_file_ptr_t){ + .ok = false, + .u.err = GM_ERROR(GM_ERROR_FILE_OPERATION, "Failed to open file: %s", strerror(errno)) + }; + } + return (gm_result_file_ptr_t){.ok = true, .u.val = file}; +} + +static gm_result_void_t wrap_fclose(FILE *stream) { + if (fclose(stream) != 0) { + return (gm_result_void_t){ + .ok = false, + .u.err = GM_ERROR(GM_ERROR_FILE_OPERATION, "Failed to close file: %s", strerror(errno)) + }; + } + return (gm_result_void_t){.ok = true}; +} + +static gm_result_size_t wrap_fread(void *ptr, size_t size, size_t count, FILE *stream) { + size_t result = fread(ptr, size, count, stream); + if (result < count && ferror(stream)) { + return (gm_result_size_t){ + .ok = false, + .u.err = GM_ERROR(GM_ERROR_FILE_OPERATION, "Failed to read file: %s", strerror(errno)) + }; + } + return (gm_result_size_t){.ok = true, .u.val = result}; +} + +static gm_result_size_t wrap_fwrite(const void *ptr, size_t size, size_t count, FILE *stream) { + size_t result = fwrite(ptr, size, count, stream); + if (result < count) { + return (gm_result_size_t){ + .ok = false, + .u.err = GM_ERROR(GM_ERROR_FILE_OPERATION, "Failed to write file: %s", strerror(errno)) + }; + } + return (gm_result_size_t){.ok = true, .u.val = result}; +} + +static gm_result_int_t wrap_fprintf(FILE *stream, const char *format, ...) { + va_list args; + va_start(args, format); + int result = vfprintf(stream, format, args); + va_end(args); + + if (result < 0) { + return (gm_result_int_t){ + .ok = false, + .u.err = GM_ERROR(GM_ERROR_FILE_OPERATION, "Failed to write formatted output: %s", strerror(errno)) + }; + } + return (gm_result_int_t){.ok = true, .u.val = result}; +} + +static gm_result_void_t wrap_fflush(FILE *stream) { + if (fflush(stream) != 0) { + return (gm_result_void_t){ + .ok = false, + .u.err = GM_ERROR(GM_ERROR_FILE_OPERATION, "Failed to flush stream: %s", strerror(errno)) + }; + } + return (gm_result_void_t){.ok = true}; +} + +static gm_result_void_t wrap_remove(const char *path) { + if (remove(path) != 0) { + return (gm_result_void_t){ + .ok = false, + .u.err = GM_ERROR(GM_ERROR_FILE_OPERATION, "Failed to remove file: %s", strerror(errno)) + }; + } + return (gm_result_void_t){.ok = true}; +} + +static gm_result_void_t wrap_rename(const char *oldpath, const char *newpath) { + if (rename(oldpath, newpath) != 0) { + return (gm_result_void_t){ + .ok = false, + .u.err = GM_ERROR(GM_ERROR_FILE_OPERATION, "Failed to rename file: %s", strerror(errno)) + }; + } + return (gm_result_void_t){.ok = true}; +} + +/* Wrapper functions for directory operations with Result types */ + +static gm_result_void_t wrap_mkdir(const char *path, mode_t mode) { + if (mkdir(path, mode) != 0) { + return (gm_result_void_t){ + .ok = false, + .u.err = GM_ERROR(GM_ERROR_DIR_OPERATION, "Failed to create directory: %s", strerror(errno)) + }; + } + return (gm_result_void_t){.ok = true}; +} + +static gm_result_void_t wrap_rmdir(const char *path) { + if (rmdir(path) != 0) { + return (gm_result_void_t){ + .ok = false, + .u.err = GM_ERROR(GM_ERROR_DIR_OPERATION, "Failed to remove directory: %s", strerror(errno)) + }; + } + return (gm_result_void_t){.ok = true}; +} + +static gm_result_void_t wrap_chdir(const char *path) { + if (chdir(path) != 0) { + return (gm_result_void_t){ + .ok = false, + .u.err = GM_ERROR(GM_ERROR_DIR_OPERATION, "Failed to change directory: %s", strerror(errno)) + }; + } + return (gm_result_void_t){.ok = true}; +} + +static gm_result_string_t wrap_getcwd(char *buf, size_t size) { + char *result = getcwd(buf, size); + if (!result) { + return (gm_result_string_t){ + .ok = false, + .u.err = GM_ERROR(GM_ERROR_DIR_OPERATION, "Failed to get current directory: %s", strerror(errno)) + }; + } + return (gm_result_string_t){.ok = true, .u.val = result}; +} + +/* Wrapper functions for file system operations with Result types */ + +static gm_result_void_t wrap_stat(const char *path, struct stat *buf) { + if (stat(path, buf) != 0) { + return (gm_result_void_t){ + .ok = false, + .u.err = GM_ERROR(GM_ERROR_FS_OPERATION, "Failed to stat file: %s", strerror(errno)) + }; + } + return (gm_result_void_t){.ok = true}; +} + +static gm_result_void_t wrap_lstat(const char *path, struct stat *buf) { + if (lstat(path, buf) != 0) { + return (gm_result_void_t){ + .ok = false, + .u.err = GM_ERROR(GM_ERROR_FS_OPERATION, "Failed to lstat file: %s", strerror(errno)) + }; + } + return (gm_result_void_t){.ok = true}; +} + +static gm_result_void_t wrap_access(const char *path, int mode) { + if (access(path, mode) != 0) { + return (gm_result_void_t){ + .ok = false, + .u.err = GM_ERROR(GM_ERROR_FS_OPERATION, "Access check failed: %s", strerror(errno)) + }; + } + return (gm_result_void_t){.ok = true}; +} + +static gm_result_void_t wrap_chmod(const char *path, mode_t mode) { + if (chmod(path, mode) != 0) { + return (gm_result_void_t){ + .ok = false, + .u.err = GM_ERROR(GM_ERROR_FS_OPERATION, "Failed to change file mode: %s", strerror(errno)) + }; + } + return (gm_result_void_t){.ok = true}; +} + +static gm_result_void_t wrap_unlink(const char *path) { + if (unlink(path) != 0) { + return (gm_result_void_t){ + .ok = false, + .u.err = GM_ERROR(GM_ERROR_FS_OPERATION, "Failed to unlink file: %s", strerror(errno)) + }; + } + return (gm_result_void_t){.ok = true}; +} + +static gm_result_ssize_t wrap_readlink(const char *path, char *buf, size_t bufsiz) { + ssize_t result = readlink(path, buf, bufsiz); + if (result < 0) { + return (gm_result_ssize_t){ + .ok = false, + .u.err = GM_ERROR(GM_ERROR_FS_OPERATION, "Failed to read link: %s", strerror(errno)) + }; + } + return (gm_result_ssize_t){.ok = true, .u.val = result}; +} + +static gm_result_void_t wrap_symlink(const char *oldpath, const char *newpath) { + if (symlink(oldpath, newpath) != 0) { + return (gm_result_void_t){ + .ok = false, + .u.err = GM_ERROR(GM_ERROR_FS_OPERATION, "Failed to create symlink: %s", strerror(errno)) + }; + } + return (gm_result_void_t){.ok = true}; +} + +/* Wrapper functions for process operations with Result types */ + +static gm_result_int_t wrap_system(const char *command) { + /* NOLINTNEXTLINE(cert-env33-c) - system() usage is intentional for API compatibility */ + int result = system(command); + if (result == -1) { + return (gm_result_int_t){ + .ok = false, + .u.err = GM_ERROR(GM_ERROR_PROCESS_OPERATION, "Failed to execute command: %s", strerror(errno)) + }; + } + return (gm_result_int_t){.ok = true, .u.val = result}; +} + +static gm_result_pid_t wrap_fork(void) { + pid_t result = fork(); + if (result < 0) { + return (gm_result_pid_t){ + .ok = false, + .u.err = GM_ERROR(GM_ERROR_PROCESS_OPERATION, "Failed to fork process: %s", strerror(errno)) + }; + } + return (gm_result_pid_t){.ok = true, .u.val = result}; +} + +static gm_result_void_t wrap_execvp(const char *file, char *const argv[]) { + execvp(file, argv); + /* If we get here, execvp failed */ + return (gm_result_void_t){ + .ok = false, + .u.err = GM_ERROR(GM_ERROR_PROCESS_OPERATION, "Failed to execute program: %s", strerror(errno)) + }; +} + +static gm_result_pid_t wrap_waitpid(pid_t pid, int *status, int options) { + pid_t result = waitpid(pid, status, options); + if (result < 0) { + return (gm_result_pid_t){ + .ok = false, + .u.err = GM_ERROR(GM_ERROR_PROCESS_OPERATION, "Failed to wait for process: %s", strerror(errno)) + }; + } + return (gm_result_pid_t){.ok = true, .u.val = result}; +} + +/* Static operation structures */ + +static const gm_file_ops_t GmDefaultFileOps = { + .fopen = wrap_fopen, + .fclose = wrap_fclose, + .fread = wrap_fread, + .fwrite = wrap_fwrite, + .fprintf = wrap_fprintf, + .fflush = wrap_fflush, + .remove = wrap_remove, + .rename = wrap_rename +}; + +static const gm_dir_ops_t GmDefaultDirOps = { + .mkdir = wrap_mkdir, + .rmdir = wrap_rmdir, + .chdir = wrap_chdir, + .getcwd = wrap_getcwd +}; + +static const gm_fs_ops_t GmDefaultFsOps = { + .stat = wrap_stat, + .lstat = wrap_lstat, + .access = wrap_access, + .chmod = wrap_chmod, + .unlink = wrap_unlink, + .readlink = wrap_readlink, + .symlink = wrap_symlink +}; + +static const gm_process_ops_t GmDefaultProcessOps = { + .system = wrap_system, + .fork = wrap_fork, + .execvp = wrap_execvp, + .waitpid = wrap_waitpid +}; + +static const gm_io_ops_t GmDefaultIoOps = { + .file = &GmDefaultFileOps, + .dir = &GmDefaultDirOps, + .fs = &GmDefaultFsOps, + .process = &GmDefaultProcessOps +}; + +const gm_io_ops_t *gm_io_ops_default(void) { + return &GmDefaultIoOps; +} diff --git a/core/tests/unit/test_io.c b/core/tests/unit/test_io.c new file mode 100644 index 00000000..4b187311 --- /dev/null +++ b/core/tests/unit/test_io.c @@ -0,0 +1,328 @@ +/* SPDX-License-Identifier: LicenseRef-MIND-UCAL-1.0 */ +/* Β© 2025 J. Kirby Ross / Neuroglyph Collective */ + +#define _POSIX_C_SOURCE 200809L + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +/* Test constants */ +static const char *TEST_FILE = "test_io_file.tmp"; +static const char *TEST_FILE2 = "test_io_file2.tmp"; +static const char *TEST_DIR = "test_io_dir.tmp"; +static const char *TEST_SYMLINK = "test_io_symlink.tmp"; +static const char *TEST_CONTENT = "Hello, I/O World!"; +static const mode_t TEST_DIR_MODE = 0755; + +/* Cleanup helper */ +static void cleanup_test_files(void) { + remove(TEST_FILE); + remove(TEST_FILE2); + remove(TEST_SYMLINK); + rmdir(TEST_DIR); +} + +/* Test file operations */ +static void test_file_operations(void) { + printf("test_file_operations... "); + + const gm_io_ops_t *io = gm_io_ops_default(); + assert(io != NULL); + assert(io->file != NULL); + + /* Test fopen/fclose */ + gm_result_file_ptr_t open_result = io->file->fopen(TEST_FILE, "w"); + assert(open_result.ok); + assert(open_result.u.val != NULL); + + FILE *file = open_result.u.val; + + /* Test fwrite */ + size_t content_len = strlen(TEST_CONTENT); + gm_result_size_t write_result = io->file->fwrite(TEST_CONTENT, 1, content_len, file); + assert(write_result.ok); + assert(write_result.u.val == content_len); + + /* Test fprintf */ + gm_result_int_t fprintf_result = io->file->fprintf(file, "\nFormatted: %d", 42); + assert(fprintf_result.ok); + assert(fprintf_result.u.val > 0); + + /* Test fflush */ + gm_result_void_t flush_result = io->file->fflush(file); + assert(flush_result.ok); + + /* Test fclose */ + gm_result_void_t close_result = io->file->fclose(file); + assert(close_result.ok); + + /* Test fopen for reading */ + open_result = io->file->fopen(TEST_FILE, "r"); + assert(open_result.ok); + file = open_result.u.val; + + /* Test fread */ + char buffer[256] = {0}; + gm_result_size_t read_result = io->file->fread(buffer, 1, content_len, file); + assert(read_result.ok); + assert(read_result.u.val == content_len); + assert(strncmp(buffer, TEST_CONTENT, content_len) == 0); + + close_result = io->file->fclose(file); + assert(close_result.ok); + + /* Test rename */ + gm_result_void_t rename_result = io->file->rename(TEST_FILE, TEST_FILE2); + assert(rename_result.ok); + + /* Test remove */ + gm_result_void_t remove_result = io->file->remove(TEST_FILE2); + assert(remove_result.ok); + + /* Test error cases */ + open_result = io->file->fopen("/nonexistent/path/file.txt", "r"); + assert(!open_result.ok); + assert(open_result.u.err != NULL); + gm_error_free(open_result.u.err); + + printf("OK\n"); +} + +/* Test directory operations */ +static void test_dir_operations(void) { + printf("test_dir_operations... "); + + const gm_io_ops_t *io = gm_io_ops_default(); + assert(io->dir != NULL); + + /* Test mkdir */ + gm_result_void_t mkdir_result = io->dir->mkdir(TEST_DIR, TEST_DIR_MODE); + assert(mkdir_result.ok); + + /* Test getcwd */ + char cwd_buffer[1024]; + gm_result_string_t getcwd_result = io->dir->getcwd(cwd_buffer, sizeof(cwd_buffer)); + assert(getcwd_result.ok); + assert(getcwd_result.u.val != NULL); + assert(strlen(getcwd_result.u.val) > 0); + + /* Test chdir */ + gm_result_void_t chdir_result = io->dir->chdir(TEST_DIR); + assert(chdir_result.ok); + + /* Change back */ + chdir_result = io->dir->chdir(".."); + assert(chdir_result.ok); + + /* Test rmdir */ + gm_result_void_t rmdir_result = io->dir->rmdir(TEST_DIR); + assert(rmdir_result.ok); + + /* Test error cases */ + mkdir_result = io->dir->mkdir("/root/no_permission", TEST_DIR_MODE); + assert(!mkdir_result.ok); + gm_error_free(mkdir_result.u.err); + + printf("OK\n"); +} + +/* Test file system operations */ +static void test_fs_operations(void) { + printf("test_fs_operations... "); + + const gm_io_ops_t *io = gm_io_ops_default(); + assert(io->fs != NULL); + + /* Create a test file */ + FILE *file = fopen(TEST_FILE, "w"); + assert(file != NULL); + fwrite(TEST_CONTENT, 1, strlen(TEST_CONTENT), file); + fclose(file); + + /* Test stat */ + struct stat st; + gm_result_void_t stat_result = io->fs->stat(TEST_FILE, &st); + assert(stat_result.ok); + assert(S_ISREG(st.st_mode)); + assert(st.st_size == (off_t)strlen(TEST_CONTENT)); + + /* Test lstat (same as stat for regular files) */ + gm_result_void_t lstat_result = io->fs->lstat(TEST_FILE, &st); + assert(lstat_result.ok); + + /* Test access */ + gm_result_void_t access_result = io->fs->access(TEST_FILE, F_OK); + assert(access_result.ok); + + access_result = io->fs->access(TEST_FILE, R_OK); + assert(access_result.ok); + + /* Test chmod */ + gm_result_void_t chmod_result = io->fs->chmod(TEST_FILE, 0600); + assert(chmod_result.ok); + + /* Test symlink */ + gm_result_void_t symlink_result = io->fs->symlink(TEST_FILE, TEST_SYMLINK); + assert(symlink_result.ok); + + /* Test readlink */ + char link_buffer[256]; + gm_result_ssize_t readlink_result = io->fs->readlink(TEST_SYMLINK, link_buffer, sizeof(link_buffer)); + assert(readlink_result.ok); + assert(readlink_result.u.val > 0); + link_buffer[readlink_result.u.val] = '\0'; + assert(strcmp(link_buffer, TEST_FILE) == 0); + + /* Test unlink */ + gm_result_void_t unlink_result = io->fs->unlink(TEST_SYMLINK); + assert(unlink_result.ok); + + unlink_result = io->fs->unlink(TEST_FILE); + assert(unlink_result.ok); + + /* Test error cases */ + stat_result = io->fs->stat("/nonexistent/file", &st); + assert(!stat_result.ok); + gm_error_free(stat_result.u.err); + + printf("OK\n"); +} + +/* Test process operations */ +static void test_process_operations(void) { + printf("test_process_operations... "); + + const gm_io_ops_t *io = gm_io_ops_default(); + assert(io->process != NULL); + + /* Test system */ + gm_result_int_t system_result = io->process->system("true"); + assert(system_result.ok); + assert(system_result.u.val == 0); + + /* Test fork and waitpid */ + gm_result_pid_t fork_result = io->process->fork(); + assert(fork_result.ok); + + if (fork_result.u.val == 0) { + /* Child process */ + _exit(0); + } else { + /* Parent process */ + int status; + gm_result_pid_t wait_result = io->process->waitpid(fork_result.u.val, &status, 0); + assert(wait_result.ok); + assert(wait_result.u.val == fork_result.u.val); + assert(WIFEXITED(status)); + assert(WEXITSTATUS(status) == 0); + } + + printf("OK\n"); +} + +/* Test error handling */ +static void test_error_handling(void) { + printf("test_error_handling... "); + + const gm_io_ops_t *io = gm_io_ops_default(); + + /* Test file errors */ + gm_result_file_ptr_t open_result = io->file->fopen("/dev/null/impossible", "r"); + assert(!open_result.ok); + assert(open_result.u.err != NULL); + assert(open_result.u.err->code == 1001); /* GM_ERROR_FILE_OPERATION */ + gm_error_free(open_result.u.err); + + /* Test directory errors */ + gm_result_void_t mkdir_result = io->dir->mkdir("/dev/null/impossible", 0755); + assert(!mkdir_result.ok); + assert(mkdir_result.u.err != NULL); + assert(mkdir_result.u.err->code == 2001); /* GM_ERROR_DIR_OPERATION */ + gm_error_free(mkdir_result.u.err); + + /* Test filesystem errors */ + struct stat st; + gm_result_void_t stat_result = io->fs->stat("/nonexistent/path", &st); + assert(!stat_result.ok); + assert(stat_result.u.err != NULL); + assert(stat_result.u.err->code == 3001); /* GM_ERROR_FS_OPERATION */ + gm_error_free(stat_result.u.err); + + printf("OK\n"); +} + +/* Test default operations availability */ +static void test_default_operations(void) { + printf("test_default_operations... "); + + const gm_io_ops_t *io = gm_io_ops_default(); + assert(io != NULL); + + /* Check all operation groups are available */ + assert(io->file != NULL); + assert(io->dir != NULL); + assert(io->fs != NULL); + assert(io->process != NULL); + + /* Check file operations */ + assert(io->file->fopen != NULL); + assert(io->file->fclose != NULL); + assert(io->file->fread != NULL); + assert(io->file->fwrite != NULL); + assert(io->file->fprintf != NULL); + assert(io->file->fflush != NULL); + assert(io->file->remove != NULL); + assert(io->file->rename != NULL); + + /* Check directory operations */ + assert(io->dir->mkdir != NULL); + assert(io->dir->rmdir != NULL); + assert(io->dir->chdir != NULL); + assert(io->dir->getcwd != NULL); + + /* Check filesystem operations */ + assert(io->fs->stat != NULL); + assert(io->fs->lstat != NULL); + assert(io->fs->access != NULL); + assert(io->fs->chmod != NULL); + assert(io->fs->unlink != NULL); + assert(io->fs->readlink != NULL); + assert(io->fs->symlink != NULL); + + /* Check process operations */ + assert(io->process->system != NULL); + assert(io->process->fork != NULL); + assert(io->process->execvp != NULL); + assert(io->process->waitpid != NULL); + + printf("OK\n"); +} + +int main(void) { + printf("Running I/O operation tests...\n"); + + /* Clean up any leftover test files */ + cleanup_test_files(); + + /* Run tests */ + test_default_operations(); + test_file_operations(); + test_dir_operations(); + test_fs_operations(); + test_process_operations(); + test_error_handling(); + + /* Clean up */ + cleanup_test_files(); + + printf("All tests passed!\n"); + return 0; +} diff --git a/core/tests/unit/test_ulid.c b/core/tests/unit/test_ulid.c index bf3da480..93f5b54c 100644 --- a/core/tests/unit/test_ulid.c +++ b/core/tests/unit/test_ulid.c @@ -5,7 +5,7 @@ #include #include #include -#include +#include #include "gitmind/crypto/backend.h" #include "gitmind/error.h" @@ -32,7 +32,7 @@ static void test_ulid_generate_basic(void) { /* Small delay to ensure different timestamp */ struct timespec delay = {.tv_sec = 0, .tv_nsec = 2000000}; /* 2ms */ - thrd_sleep(&delay, NULL); + nanosleep(&delay, NULL); /* Generate second ULID */ gm_result_ulid_t result2 = gm_ulid_generate(ulid2); diff --git a/include/gitmind/io_ops.h b/include/gitmind/io_ops.h deleted file mode 100644 index 9570462c..00000000 --- a/include/gitmind/io_ops.h +++ /dev/null @@ -1,74 +0,0 @@ -/* SPDX-License-Identifier: LicenseRef-MIND-UCAL-1.0 */ -/* Β© 2025 J. Kirby Ross / Neuroglyph Collective */ - -#ifndef GITMIND_IO_OPS_H -#define GITMIND_IO_OPS_H - -#include -#include -#include - -/** - * File operations interface for dependency injection. - * This allows for test doubles and better testability. - */ -typedef struct gm_file_ops { - FILE *(*fopen)(const char *path, const char *mode); - int (*fclose)(FILE *stream); - size_t (*fread)(void *ptr, size_t size, size_t count, FILE *stream); - size_t (*fwrite)(const void *ptr, size_t size, size_t count, FILE *stream); - int (*fprintf)(FILE *stream, const char *format, ...); - int (*fflush)(FILE *stream); - int (*remove)(const char *path); - int (*rename)(const char *oldpath, const char *newpath); -} gm_file_ops_t; - -/** - * Directory operations interface for dependency injection. - */ -typedef struct gm_dir_ops { - int (*mkdir)(const char *path, mode_t mode); - int (*rmdir)(const char *path); - int (*chdir)(const char *path); - char *(*getcwd)(char *buf, size_t size); -} gm_dir_ops_t; - -/** - * File system operations interface for dependency injection. - */ -typedef struct gm_fs_ops { - int (*stat)(const char *path, struct stat *buf); - int (*lstat)(const char *path, struct stat *buf); - int (*access)(const char *path, int mode); - int (*chmod)(const char *path, mode_t mode); - int (*unlink)(const char *path); - ssize_t (*readlink)(const char *path, char *buf, size_t bufsiz); - int (*symlink)(const char *oldpath, const char *newpath); -} gm_fs_ops_t; - -/** - * Process operations interface for dependency injection. - */ -typedef struct gm_process_ops { - int (*system)(const char *command); - pid_t (*fork)(void); - int (*execvp)(const char *file, char *const argv[]); - pid_t (*waitpid)(pid_t pid, int *status, int options); -} gm_process_ops_t; - -/** - * Combined I/O operations structure. - */ -typedef struct gm_io_ops { - const gm_file_ops_t *file; - const gm_dir_ops_t *dir; - const gm_fs_ops_t *fs; - const gm_process_ops_t *process; -} gm_io_ops_t; - -/** - * Get default I/O operations (uses real system calls). - */ -const gm_io_ops_t *gm_io_ops_default(void); - -#endif /* GITMIND_IO_OPS_H */ \ No newline at end of file diff --git a/meson.build b/meson.build index e5146cc7..8e55acbe 100644 --- a/meson.build +++ b/meson.build @@ -40,6 +40,7 @@ src = files( 'core/src/crypto/random.c', 'core/src/crypto/sha256.c', 'core/src/error/error.c', + 'core/src/io/io.c', 'core/src/types/id.c', 'core/src/types/path.c', 'core/src/types/string.c', @@ -131,6 +132,13 @@ test_crypto_backend = executable('test_crypto_backend', test_ulid = executable('test_ulid', 'core/tests/unit/test_ulid.c', include_directories : inc, + link_with : [libgitmind, test_backend_lib], + dependencies : [libsodium_dep, thread_dep], + c_args : ['-DGITMIND_ENABLE_TEST_BACKEND']) + +test_io = executable('test_io', + 'core/tests/unit/test_io.c', + include_directories : inc, link_with : libgitmind, dependencies : [libsodium_dep, thread_dep]) @@ -144,4 +152,5 @@ test('sha256', test_sha256) test('random', test_random) test('utf8', test_utf8) test('crypto_backend', test_crypto_backend) -test('ulid', test_ulid) \ No newline at end of file +test('ulid', test_ulid) +test('io', test_io) diff --git a/src/util/io_default.c b/src/util/io_default.c deleted file mode 100644 index 6ea08c15..00000000 --- a/src/util/io_default.c +++ /dev/null @@ -1,50 +0,0 @@ -/* SPDX-License-Identifier: LicenseRef-MIND-UCAL-1.0 */ -/* Β© 2025 J. Kirby Ross / Neuroglyph Collective */ - -#define _DEFAULT_SOURCE -#define _GNU_SOURCE - -#include "gitmind/io_ops.h" - -#include -#include -#include -#include -#include - -/* Default file operations - use real system calls */ -static const gm_file_ops_t default_file_ops = {.fopen = fopen, - .fclose = fclose, - .fread = fread, - .fwrite = fwrite, - .fprintf = fprintf, - .fflush = fflush, - .remove = remove, - .rename = rename}; - -/* Default directory operations - use real system calls */ -static const gm_dir_ops_t default_dir_ops = { - .mkdir = mkdir, .rmdir = rmdir, .chdir = chdir, .getcwd = getcwd}; - -/* Default file system operations - use real system calls */ -static const gm_fs_ops_t default_fs_ops = {.stat = stat, - .lstat = lstat, - .access = access, - .chmod = chmod, - .unlink = unlink, - .readlink = readlink, - .symlink = symlink}; - -/* Default process operations - use real system calls */ -static const gm_process_ops_t default_process_ops = { - .system = system, .fork = fork, .execvp = execvp, .waitpid = waitpid}; - -/* Combined default I/O operations */ -static const gm_io_ops_t default_io_ops = {.file = &default_file_ops, - .dir = &default_dir_ops, - .fs = &default_fs_ops, - .process = &default_process_ops}; - -const gm_io_ops_t *gm_io_ops_default(void) { - return &default_io_ops; -} \ No newline at end of file