myxce/src/controller/testsuite/rcfile.c
Rongsong Shen 77aba21fb0
feat(xce-test-rc-file): task 1.2 rc-file line-loading via pluggable runner
Adds rcfile_load(path, runner) to rcfile.c/rcfile.h: reads lines with
getline() (no fixed-size buffer, so a long line like a multi-peer
`set masters ...` can never be silently truncated or split), strips the
trailing newline, invokes the caller-supplied runner once per line, and
stops at the first nonzero runner return, propagating that value. A missing
rc file is a silent no-op; any other open failure prints one stderr warning
and returns 0.

Found during GREEN: fopen() succeeds on a directory path on this platform,
so the EISDIR failure only surfaces on the first getline() call (ferror(f)
set) -- added a post-loop ferror() check to catch it (bug-1716).

5 new unit tests (11 total with task 1.1): ENOENT-silent, per-line-in-order
with newline stripped, stop-on-nonzero with exact propagation, non-ENOENT
open-failure warning, and a >4KB single line proving no truncation/split.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AgYRU3zDTqNGV5Jk8aiKLg
2026-08-26 15:02:03 +00:00

80 lines
2.5 KiB
C

#define _GNU_SOURCE
#include "rcfile.h"
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* Suffixes appended to the resolved base directory to form the rc-file
* path -- kept as two separate constants (rather than one shared tail)
* since the XDG_CONFIG_HOME branch is already rooted at a "config home"
* directory while the HOME fallback still needs the "/.config" segment. */
#define RCFILE_XDG_SUFFIX "/xce_test/rc"
#define RCFILE_HOME_SUFFIX "/.config/xce_test/rc"
char* rcfile_resolve_path(void)
{
const char* xdg_config_home = getenv("XDG_CONFIG_HOME");
if (xdg_config_home && *xdg_config_home) {
size_t len = strlen(xdg_config_home) + strlen(RCFILE_XDG_SUFFIX) + 1;
char* path = (char*)malloc(len);
if (!path)
return NULL;
snprintf(path, len, "%s%s", xdg_config_home, RCFILE_XDG_SUFFIX);
return path;
}
const char* home = getenv("HOME");
if (home && *home) {
size_t len = strlen(home) + strlen(RCFILE_HOME_SUFFIX) + 1;
char* path = (char*)malloc(len);
if (!path)
return NULL;
snprintf(path, len, "%s%s", home, RCFILE_HOME_SUFFIX);
return path;
}
return NULL;
}
int rcfile_load(const char* path, int (*runner)(char* line))
{
if (!path)
return 0;
FILE* f = fopen(path, "r");
if (!f) {
if (errno == ENOENT)
return 0; /* missing rc file: silent no-op success */
fprintf(stderr, "xce_test: warning: could not open rc file %s: %s\n",
path, strerror(errno));
return 0; /* present but unreadable: non-fatal warning */
}
int rc = 0;
char* line = NULL;
size_t cap = 0;
ssize_t n;
while ((n = getline(&line, &cap, f)) != -1) {
if (n > 0 && line[n - 1] == '\n')
line[n - 1] = '\0';
rc = runner(line);
if (rc != 0)
break;
}
/* fopen() succeeds on a directory on this platform -- the failure only
* surfaces on the first read (getline() returns -1 with errno==EISDIR
* and ferror(f) set, feof(f) NOT set). Treat that the same as an open
* failure: one non-fatal stderr warning, no runner calls attributed to
* it (rc is still 0 here since no line was ever successfully read). */
if (rc == 0 && ferror(f)) {
fprintf(stderr, "xce_test: warning: could not read rc file %s: %s\n",
path, strerror(errno));
}
free(line);
fclose(f);
return rc;
}