From 2e04babfdd73cc7a2d8804ecbb52f158217f53ba Mon Sep 17 00:00:00 2001 From: "J. Kirby Ross" Date: Tue, 8 Jul 2025 12:14:03 -0700 Subject: [PATCH 1/4] docs: Update CLAUDE.md --- CLAUDE.md | 319 ++++++++++++++++-------------------------------------- 1 file changed, 91 insertions(+), 228 deletions(-) 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 From 5b48d4e33749786ea8af2ab40c63976221cdaeeb Mon Sep 17 00:00:00 2001 From: "J. Kirby Ross" Date: Tue, 8 Jul 2025 12:30:24 -0700 Subject: [PATCH 2/4] feat: migrate I/O operations to core/ with Result types - Create modern I/O interface with Result-based error handling - All I/O operations now return Result types for explicit error handling - Comprehensive test coverage for file, directory, filesystem, and process operations - Zero warnings achieved with strict C23 compiler flags - Delete legacy io_default.c and io_ops.h from src/ --- core/include/gitmind/io/io.h | 87 ++++++++++ core/src/io/io.c | 322 +++++++++++++++++++++++++++++++++++ core/tests/unit/test_io.c | 322 +++++++++++++++++++++++++++++++++++ include/gitmind/io_ops.h | 74 -------- meson.build | 11 +- src/util/io_default.c | 50 ------ 6 files changed, 741 insertions(+), 125 deletions(-) create mode 100644 core/include/gitmind/io/io.h create mode 100644 core/src/io/io.c create mode 100644 core/tests/unit/test_io.c delete mode 100644 include/gitmind/io_ops.h delete mode 100644 src/util/io_default.c diff --git a/core/include/gitmind/io/io.h b/core/include/gitmind/io/io.h new file mode 100644 index 00000000..afa3b1f0 --- /dev/null +++ b/core/include/gitmind/io/io.h @@ -0,0 +1,87 @@ +/* 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 +#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..4ed24f6f --- /dev/null +++ b/core/src/io/io.c @@ -0,0 +1,322 @@ +/* SPDX-License-Identifier: LicenseRef-MIND-UCAL-1.0 */ +/* Β© 2025 J. Kirby Ross / Neuroglyph Collective */ + +#define _DEFAULT_SOURCE +#define _GNU_SOURCE +#define _POSIX_C_SOURCE 200809L + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +/* Error code constants */ +static const int ERROR_FILE_OPERATION = 1001; +static const int ERROR_DIR_OPERATION = 2001; +static const int ERROR_FS_OPERATION = 3001; +static const int 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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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) { + int result = system(command); + if (result == -1) { + return (gm_result_int_t){ + .ok = false, + .u.err = GM_ERROR(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(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(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(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 default_file_ops = { + .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 default_dir_ops = { + .mkdir = wrap_mkdir, + .rmdir = wrap_rmdir, + .chdir = wrap_chdir, + .getcwd = wrap_getcwd +}; + +static const gm_fs_ops_t default_fs_ops = { + .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 default_process_ops = { + .system = wrap_system, + .fork = wrap_fork, + .execvp = wrap_execvp, + .waitpid = wrap_waitpid +}; + +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; +} diff --git a/core/tests/unit/test_io.c b/core/tests/unit/test_io.c new file mode 100644 index 00000000..f18f0735 --- /dev/null +++ b/core/tests/unit/test_io.c @@ -0,0 +1,322 @@ +/* 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); + + 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); + + 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); + + 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); /* ERROR_FILE_OPERATION */ + + /* 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); /* ERROR_DIR_OPERATION */ + + /* 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); /* ERROR_FS_OPERATION */ + + 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/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 From a8a5c357b3e95c2f3703bd5c8eb16a0e8690b383 Mon Sep 17 00:00:00 2001 From: "J. Kirby Ross" Date: Tue, 8 Jul 2025 12:42:21 -0700 Subject: [PATCH 3/4] fix: resolve CI failures for I/O module migration - Fix memory leaks by freeing error objects in tests - Fix naming conventions (CamelCase for struct constants) - Add NOLINT for feature test macros - Remove unused error.h include from io.h - Add missing includes for types (result.h, sys/types.h) - Suppress cert-env33-c warning for system() function --- core/include/gitmind/io/io.h | 1 - core/src/io/io.c | 80 +++++++++++++++++++----------------- core/tests/unit/test_io.c | 9 ++-- 3 files changed, 49 insertions(+), 41 deletions(-) diff --git a/core/include/gitmind/io/io.h b/core/include/gitmind/io/io.h index afa3b1f0..1abc2521 100644 --- a/core/include/gitmind/io/io.h +++ b/core/include/gitmind/io/io.h @@ -4,7 +4,6 @@ #ifndef GITMIND_IO_IO_H #define GITMIND_IO_IO_H -#include #include #include #include diff --git a/core/src/io/io.c b/core/src/io/io.c index 4ed24f6f..caa982ef 100644 --- a/core/src/io/io.c +++ b/core/src/io/io.c @@ -1,12 +1,16 @@ /* 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 @@ -14,14 +18,15 @@ #include #include #include +#include /* for pid_t */ #include #include /* Error code constants */ -static const int ERROR_FILE_OPERATION = 1001; -static const int ERROR_DIR_OPERATION = 2001; -static const int ERROR_FS_OPERATION = 3001; -static const int ERROR_PROCESS_OPERATION = 4001; +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 */ @@ -30,7 +35,7 @@ static gm_result_file_ptr_t wrap_fopen(const char *path, const char *mode) { if (!file) { return (gm_result_file_ptr_t){ .ok = false, - .u.err = GM_ERROR(ERROR_FILE_OPERATION, "Failed to open file: %s", strerror(errno)) + .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}; @@ -40,7 +45,7 @@ static gm_result_void_t wrap_fclose(FILE *stream) { if (fclose(stream) != 0) { return (gm_result_void_t){ .ok = false, - .u.err = GM_ERROR(ERROR_FILE_OPERATION, "Failed to close file: %s", strerror(errno)) + .u.err = GM_ERROR(GM_ERROR_FILE_OPERATION, "Failed to close file: %s", strerror(errno)) }; } return (gm_result_void_t){.ok = true}; @@ -51,7 +56,7 @@ static gm_result_size_t wrap_fread(void *ptr, size_t size, size_t count, FILE *s if (result < count && ferror(stream)) { return (gm_result_size_t){ .ok = false, - .u.err = GM_ERROR(ERROR_FILE_OPERATION, "Failed to read file: %s", strerror(errno)) + .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}; @@ -62,7 +67,7 @@ static gm_result_size_t wrap_fwrite(const void *ptr, size_t size, size_t count, if (result < count) { return (gm_result_size_t){ .ok = false, - .u.err = GM_ERROR(ERROR_FILE_OPERATION, "Failed to write file: %s", strerror(errno)) + .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}; @@ -77,7 +82,7 @@ static gm_result_int_t wrap_fprintf(FILE *stream, const char *format, ...) { if (result < 0) { return (gm_result_int_t){ .ok = false, - .u.err = GM_ERROR(ERROR_FILE_OPERATION, "Failed to write formatted output: %s", strerror(errno)) + .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}; @@ -87,7 +92,7 @@ static gm_result_void_t wrap_fflush(FILE *stream) { if (fflush(stream) != 0) { return (gm_result_void_t){ .ok = false, - .u.err = GM_ERROR(ERROR_FILE_OPERATION, "Failed to flush stream: %s", strerror(errno)) + .u.err = GM_ERROR(GM_ERROR_FILE_OPERATION, "Failed to flush stream: %s", strerror(errno)) }; } return (gm_result_void_t){.ok = true}; @@ -97,7 +102,7 @@ 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(ERROR_FILE_OPERATION, "Failed to remove file: %s", strerror(errno)) + .u.err = GM_ERROR(GM_ERROR_FILE_OPERATION, "Failed to remove file: %s", strerror(errno)) }; } return (gm_result_void_t){.ok = true}; @@ -107,7 +112,7 @@ 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(ERROR_FILE_OPERATION, "Failed to rename file: %s", strerror(errno)) + .u.err = GM_ERROR(GM_ERROR_FILE_OPERATION, "Failed to rename file: %s", strerror(errno)) }; } return (gm_result_void_t){.ok = true}; @@ -119,7 +124,7 @@ 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(ERROR_DIR_OPERATION, "Failed to create directory: %s", strerror(errno)) + .u.err = GM_ERROR(GM_ERROR_DIR_OPERATION, "Failed to create directory: %s", strerror(errno)) }; } return (gm_result_void_t){.ok = true}; @@ -129,7 +134,7 @@ 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(ERROR_DIR_OPERATION, "Failed to remove directory: %s", strerror(errno)) + .u.err = GM_ERROR(GM_ERROR_DIR_OPERATION, "Failed to remove directory: %s", strerror(errno)) }; } return (gm_result_void_t){.ok = true}; @@ -139,7 +144,7 @@ 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(ERROR_DIR_OPERATION, "Failed to change directory: %s", strerror(errno)) + .u.err = GM_ERROR(GM_ERROR_DIR_OPERATION, "Failed to change directory: %s", strerror(errno)) }; } return (gm_result_void_t){.ok = true}; @@ -150,7 +155,7 @@ static gm_result_string_t wrap_getcwd(char *buf, size_t size) { if (!result) { return (gm_result_string_t){ .ok = false, - .u.err = GM_ERROR(ERROR_DIR_OPERATION, "Failed to get current directory: %s", strerror(errno)) + .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}; @@ -162,7 +167,7 @@ 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(ERROR_FS_OPERATION, "Failed to stat file: %s", strerror(errno)) + .u.err = GM_ERROR(GM_ERROR_FS_OPERATION, "Failed to stat file: %s", strerror(errno)) }; } return (gm_result_void_t){.ok = true}; @@ -172,7 +177,7 @@ 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(ERROR_FS_OPERATION, "Failed to lstat file: %s", strerror(errno)) + .u.err = GM_ERROR(GM_ERROR_FS_OPERATION, "Failed to lstat file: %s", strerror(errno)) }; } return (gm_result_void_t){.ok = true}; @@ -182,7 +187,7 @@ 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(ERROR_FS_OPERATION, "Access check failed: %s", strerror(errno)) + .u.err = GM_ERROR(GM_ERROR_FS_OPERATION, "Access check failed: %s", strerror(errno)) }; } return (gm_result_void_t){.ok = true}; @@ -192,7 +197,7 @@ 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(ERROR_FS_OPERATION, "Failed to change file mode: %s", strerror(errno)) + .u.err = GM_ERROR(GM_ERROR_FS_OPERATION, "Failed to change file mode: %s", strerror(errno)) }; } return (gm_result_void_t){.ok = true}; @@ -202,7 +207,7 @@ 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(ERROR_FS_OPERATION, "Failed to unlink file: %s", strerror(errno)) + .u.err = GM_ERROR(GM_ERROR_FS_OPERATION, "Failed to unlink file: %s", strerror(errno)) }; } return (gm_result_void_t){.ok = true}; @@ -213,7 +218,7 @@ static gm_result_ssize_t wrap_readlink(const char *path, char *buf, size_t bufsi if (result < 0) { return (gm_result_ssize_t){ .ok = false, - .u.err = GM_ERROR(ERROR_FS_OPERATION, "Failed to read link: %s", strerror(errno)) + .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}; @@ -223,7 +228,7 @@ 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(ERROR_FS_OPERATION, "Failed to create symlink: %s", strerror(errno)) + .u.err = GM_ERROR(GM_ERROR_FS_OPERATION, "Failed to create symlink: %s", strerror(errno)) }; } return (gm_result_void_t){.ok = true}; @@ -232,11 +237,12 @@ static gm_result_void_t wrap_symlink(const char *oldpath, const char *newpath) { /* 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(ERROR_PROCESS_OPERATION, "Failed to execute command: %s", strerror(errno)) + .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}; @@ -247,7 +253,7 @@ static gm_result_pid_t wrap_fork(void) { if (result < 0) { return (gm_result_pid_t){ .ok = false, - .u.err = GM_ERROR(ERROR_PROCESS_OPERATION, "Failed to fork process: %s", strerror(errno)) + .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}; @@ -258,7 +264,7 @@ static gm_result_void_t wrap_execvp(const char *file, char *const argv[]) { /* If we get here, execvp failed */ return (gm_result_void_t){ .ok = false, - .u.err = GM_ERROR(ERROR_PROCESS_OPERATION, "Failed to execute program: %s", strerror(errno)) + .u.err = GM_ERROR(GM_ERROR_PROCESS_OPERATION, "Failed to execute program: %s", strerror(errno)) }; } @@ -267,7 +273,7 @@ static gm_result_pid_t wrap_waitpid(pid_t pid, int *status, int options) { if (result < 0) { return (gm_result_pid_t){ .ok = false, - .u.err = GM_ERROR(ERROR_PROCESS_OPERATION, "Failed to wait for process: %s", strerror(errno)) + .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}; @@ -275,7 +281,7 @@ static gm_result_pid_t wrap_waitpid(pid_t pid, int *status, int options) { /* Static operation structures */ -static const gm_file_ops_t default_file_ops = { +static const gm_file_ops_t GmDefaultFileOps = { .fopen = wrap_fopen, .fclose = wrap_fclose, .fread = wrap_fread, @@ -286,14 +292,14 @@ static const gm_file_ops_t default_file_ops = { .rename = wrap_rename }; -static const gm_dir_ops_t default_dir_ops = { +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 default_fs_ops = { +static const gm_fs_ops_t GmDefaultFsOps = { .stat = wrap_stat, .lstat = wrap_lstat, .access = wrap_access, @@ -303,20 +309,20 @@ static const gm_fs_ops_t default_fs_ops = { .symlink = wrap_symlink }; -static const gm_process_ops_t default_process_ops = { +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 default_io_ops = { - .file = &default_file_ops, - .dir = &default_dir_ops, - .fs = &default_fs_ops, - .process = &default_process_ops +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 &default_io_ops; + return &GmDefaultIoOps; } diff --git a/core/tests/unit/test_io.c b/core/tests/unit/test_io.c index f18f0735..fecfe151 100644 --- a/core/tests/unit/test_io.c +++ b/core/tests/unit/test_io.c @@ -235,20 +235,23 @@ static void test_error_handling(void) { 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); /* ERROR_FILE_OPERATION */ + 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); /* ERROR_DIR_OPERATION */ + 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); /* ERROR_FS_OPERATION */ + assert(stat_result.u.err->code == 3001); /* GM_ERROR_FS_OPERATION */ + gm_error_free(stat_result.u.err); printf("OK\n"); } From c87ab77f1c810aa30b3be4d9decddbb7da01e8b4 Mon Sep 17 00:00:00 2001 From: "J. Kirby Ross" Date: Tue, 8 Jul 2025 13:04:55 -0700 Subject: [PATCH 4/4] fix: eliminate memory leaks in I/O tests and resolve macOS build issues - Added gm_error_free() calls for all error cases in test_io.c - Fixed test_file_operations error handling (line 94) - Fixed test_dir_operations error handling (line 132) - Fixed test_fs_operations error handling (line 194) - Replaced threads.h with pthread.h for macOS compatibility - Changed thrd_sleep to nanosleep for POSIX compliance - All tests now pass with zero memory leaks --- core/tests/unit/test_io.c | 3 +++ core/tests/unit/test_ulid.c | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/core/tests/unit/test_io.c b/core/tests/unit/test_io.c index fecfe151..4b187311 100644 --- a/core/tests/unit/test_io.c +++ b/core/tests/unit/test_io.c @@ -91,6 +91,7 @@ static void test_file_operations(void) { 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"); } @@ -128,6 +129,7 @@ static void test_dir_operations(void) { /* 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"); } @@ -189,6 +191,7 @@ static void test_fs_operations(void) { /* 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"); } 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);