mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
Since this branch was written, main routes cbm_safe_getenv through a startup settings resolver before it reads the process environment. cbm_env_dup, its allocating sibling, read only the process environment, so after the merge the two helpers could give different answers for the same name. Consult the resolver first, exactly as cbm_safe_getenv does: a handled value is copied through the memory core, and a handled NULL masks the variable. The resolver serves CBM_* settings today, so PATH lookups behave as before; this keeps both helpers one view of the environment by construction. platform_env_dup_honours_the_settings_resolver pins the three cases: resolved, masked, and falling through to the process environment. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
732 lines
22 KiB
C
732 lines
22 KiB
C
/*
|
|
* platform.c — OS abstraction implementations.
|
|
*
|
|
* macOS, Linux, and Windows. Platform-specific code behind #ifdef guards.
|
|
*/
|
|
#include "platform.h"
|
|
|
|
#include "foundation/compat.h"
|
|
#include "foundation/constants.h"
|
|
#include "foundation/mem_core.h"
|
|
#include "foundation/platform_internal.h"
|
|
#include <ctype.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#define CBM_NSEC_PER_SEC 1000000000ULL
|
|
|
|
static uint64_t cbm_platform_scale_fraction(uint64_t remainder, uint64_t multiplier,
|
|
uint64_t divisor) {
|
|
uint64_t quotient = 0;
|
|
uint64_t reduced = 0;
|
|
uint64_t mask = 1;
|
|
while (mask <= multiplier / 2) {
|
|
mask <<= 1U;
|
|
}
|
|
for (; mask != 0; mask >>= 1U) {
|
|
quotient *= 2;
|
|
if (reduced >= divisor - reduced) {
|
|
reduced -= divisor - reduced;
|
|
quotient++;
|
|
} else {
|
|
reduced += reduced;
|
|
}
|
|
if ((multiplier & mask) != 0) {
|
|
if (reduced >= divisor - remainder) {
|
|
reduced -= divisor - remainder;
|
|
quotient++;
|
|
} else {
|
|
reduced += remainder;
|
|
}
|
|
}
|
|
}
|
|
return quotient;
|
|
}
|
|
|
|
uint64_t cbm_platform_scale_counter_ns(uint64_t counter, uint64_t frequency) {
|
|
if (frequency == 0) {
|
|
return UINT64_MAX;
|
|
}
|
|
uint64_t whole_seconds = counter / frequency;
|
|
uint64_t remainder = counter % frequency;
|
|
if (whole_seconds > UINT64_MAX / CBM_NSEC_PER_SEC) {
|
|
return UINT64_MAX;
|
|
}
|
|
uint64_t whole_ns = whole_seconds * CBM_NSEC_PER_SEC;
|
|
uint64_t fraction_ns = cbm_platform_scale_fraction(remainder, CBM_NSEC_PER_SEC, frequency);
|
|
return fraction_ns > UINT64_MAX - whole_ns ? UINT64_MAX : whole_ns + fraction_ns;
|
|
}
|
|
|
|
/* Canonicalize a Windows drive letter to upper-case in place: "c:/x" -> "C:/x".
|
|
* Windows drive letters are case-insensitive, but a lowercase one (as agent
|
|
* CWDs often report, e.g. Claude Code's "c:\...") otherwise produces a distinct
|
|
* project key ("c-..." vs "C-...") and, on a case-insensitive FS, a colliding
|
|
* cache file that clobbers the good index (#227/#367/#394). Folding to a single
|
|
* canonical form here — at the one path-normalization choke point — keeps the
|
|
* project key, cache file and integrity check consistent regardless of case.
|
|
* Only the strict drive-root form `X:/` or bare `X:` is touched, so ordinary
|
|
* POSIX paths (which never start that way) are unaffected. */
|
|
static void cbm_canonicalize_drive(char *path) {
|
|
if (path && path[0] >= 'a' && path[0] <= 'z' && path[1] == ':' &&
|
|
(path[2] == '/' || path[2] == '\0')) {
|
|
path[0] = (char)(path[0] - 'a' + 'A');
|
|
}
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
|
|
/* ── Windows implementation ────────────────────────────────── */
|
|
|
|
#ifndef WIN32_LEAN_AND_MEAN
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#endif
|
|
#include <windows.h>
|
|
#include <io.h>
|
|
#include <sys/stat.h>
|
|
#include "foundation/win_utf8.h"
|
|
|
|
void *cbm_mmap_read(const char *path, size_t *out_size) {
|
|
if (!path || !out_size) {
|
|
return NULL;
|
|
}
|
|
*out_size = 0;
|
|
|
|
wchar_t *wpath = cbm_path_to_wide(path);
|
|
if (!wpath) {
|
|
return NULL;
|
|
}
|
|
|
|
HANDLE file = CreateFileW(wpath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING,
|
|
FILE_ATTRIBUTE_NORMAL, NULL);
|
|
if (file == INVALID_HANDLE_VALUE) {
|
|
free(wpath);
|
|
return NULL;
|
|
}
|
|
LARGE_INTEGER sz;
|
|
if (!GetFileSizeEx(file, &sz) || sz.QuadPart == 0) {
|
|
CloseHandle(file);
|
|
free(wpath);
|
|
return NULL;
|
|
}
|
|
HANDLE mapping = CreateFileMappingW(file, NULL, PAGE_READONLY, 0, 0, NULL);
|
|
if (!mapping) {
|
|
CloseHandle(file);
|
|
free(wpath);
|
|
return NULL;
|
|
}
|
|
void *addr = MapViewOfFile(mapping, FILE_MAP_READ, 0, 0, 0);
|
|
CloseHandle(mapping);
|
|
CloseHandle(file);
|
|
free(wpath);
|
|
if (!addr) {
|
|
return NULL;
|
|
}
|
|
*out_size = (size_t)sz.QuadPart;
|
|
return addr;
|
|
}
|
|
|
|
void cbm_munmap(void *addr, size_t size) {
|
|
(void)size;
|
|
if (addr) {
|
|
UnmapViewOfFile(addr);
|
|
}
|
|
}
|
|
|
|
uint64_t cbm_now_ns(void) {
|
|
LARGE_INTEGER freq, count;
|
|
QueryPerformanceFrequency(&freq);
|
|
QueryPerformanceCounter(&count);
|
|
return cbm_platform_scale_counter_ns((uint64_t)count.QuadPart, (uint64_t)freq.QuadPart);
|
|
}
|
|
|
|
#define CBM_USEC_PER_SEC 1000000ULL
|
|
|
|
uint64_t cbm_now_ms(void) {
|
|
return cbm_now_ns() / CBM_USEC_PER_SEC;
|
|
}
|
|
|
|
int cbm_nprocs(void) {
|
|
SYSTEM_INFO si;
|
|
GetSystemInfo(&si);
|
|
return (int)si.dwNumberOfProcessors > 0 ? (int)si.dwNumberOfProcessors : 1;
|
|
}
|
|
|
|
bool cbm_file_exists(const char *path) {
|
|
wchar_t *wpath = cbm_path_to_wide(path);
|
|
if (!wpath) {
|
|
return false;
|
|
}
|
|
DWORD attr = GetFileAttributesW(wpath);
|
|
free(wpath);
|
|
return attr != INVALID_FILE_ATTRIBUTES;
|
|
}
|
|
|
|
bool cbm_is_dir(const char *path) {
|
|
wchar_t *wpath = cbm_path_to_wide(path);
|
|
if (!wpath) {
|
|
return false;
|
|
}
|
|
DWORD attr = GetFileAttributesW(wpath);
|
|
free(wpath);
|
|
return attr != INVALID_FILE_ATTRIBUTES && (attr & FILE_ATTRIBUTE_DIRECTORY);
|
|
}
|
|
|
|
int64_t cbm_file_size(const char *path) {
|
|
wchar_t *wpath = cbm_path_to_wide(path);
|
|
if (!wpath) {
|
|
return CBM_NOT_FOUND;
|
|
}
|
|
WIN32_FILE_ATTRIBUTE_DATA fad;
|
|
BOOL ok = GetFileAttributesExW(wpath, GetFileExInfoStandard, &fad);
|
|
free(wpath);
|
|
if (!ok) {
|
|
return CBM_NOT_FOUND;
|
|
}
|
|
LARGE_INTEGER sz;
|
|
sz.HighPart = (LONG)fad.nFileSizeHigh; // cppcheck-suppress unreadVariable
|
|
sz.LowPart = fad.nFileSizeLow; // cppcheck-suppress unreadVariable
|
|
return (int64_t)sz.QuadPart;
|
|
}
|
|
|
|
char *cbm_normalize_path_sep(char *path) {
|
|
if (path) {
|
|
for (char *p = path; *p; p++) {
|
|
if (*p == '\\') {
|
|
*p = '/';
|
|
}
|
|
}
|
|
cbm_canonicalize_drive(path);
|
|
}
|
|
return path;
|
|
}
|
|
|
|
#else /* POSIX (macOS + Linux) */
|
|
|
|
/* ── POSIX implementation ────────────────────────────────── */
|
|
|
|
#include <sys/mman.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
#include <time.h>
|
|
|
|
#ifdef __APPLE__
|
|
#include <mach/mach_time.h>
|
|
#include <pthread.h>
|
|
#include <sys/sysctl.h>
|
|
#else
|
|
#include <sched.h>
|
|
#endif
|
|
|
|
/* ── Memory mapping ──────────────────────────── */
|
|
|
|
void *cbm_mmap_read(const char *path, size_t *out_size) {
|
|
if (!path || !out_size) {
|
|
return NULL;
|
|
}
|
|
*out_size = 0;
|
|
|
|
int fd = open(path, O_RDONLY);
|
|
if (fd < 0) {
|
|
return NULL;
|
|
}
|
|
|
|
struct stat st;
|
|
if (fstat(fd, &st) != 0 || st.st_size == 0) {
|
|
close(fd);
|
|
return NULL;
|
|
}
|
|
|
|
void *addr = mmap(NULL, (size_t)st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
|
|
close(fd);
|
|
|
|
if (addr == MAP_FAILED) {
|
|
return NULL;
|
|
}
|
|
*out_size = (size_t)st.st_size;
|
|
return addr;
|
|
}
|
|
|
|
void cbm_munmap(void *addr, size_t size) {
|
|
if (addr && size > 0) {
|
|
munmap(addr, size);
|
|
}
|
|
}
|
|
|
|
/* ── Timing ───────────────────────────── */
|
|
|
|
#ifdef __APPLE__
|
|
static mach_timebase_info_data_t timebase_info = {1, 1};
|
|
static pthread_once_t timebase_once = PTHREAD_ONCE_INIT;
|
|
|
|
static void cbm_timebase_initialize(void) {
|
|
(void)mach_timebase_info(&timebase_info);
|
|
if (timebase_info.numer == 0 || timebase_info.denom == 0) {
|
|
timebase_info.numer = 1;
|
|
timebase_info.denom = 1;
|
|
}
|
|
}
|
|
|
|
uint64_t cbm_now_ns(void) {
|
|
(void)pthread_once(&timebase_once, cbm_timebase_initialize);
|
|
uint64_t ticks = mach_absolute_time();
|
|
return ticks * timebase_info.numer / timebase_info.denom;
|
|
}
|
|
#else
|
|
uint64_t cbm_now_ns(void) {
|
|
struct timespec ts;
|
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
return (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec;
|
|
}
|
|
#endif
|
|
|
|
#define CBM_USEC_PER_SEC 1000000ULL
|
|
|
|
uint64_t cbm_now_ms(void) {
|
|
return cbm_now_ns() / CBM_USEC_PER_SEC;
|
|
}
|
|
|
|
/* ── System info ───────────────────────────── */
|
|
|
|
int cbm_nprocs(void) {
|
|
#ifdef __APPLE__
|
|
int ncpu = 0;
|
|
size_t len = sizeof(ncpu);
|
|
if (sysctlbyname("hw.ncpu", &ncpu, &len, NULL, 0) == 0 && ncpu > 0) {
|
|
return ncpu;
|
|
}
|
|
enum { FILE_EXISTS = 1 };
|
|
return FILE_EXISTS;
|
|
#else
|
|
long n = sysconf(_SC_NPROCESSORS_ONLN);
|
|
return n > 0 ? (int)n : 1;
|
|
#endif
|
|
}
|
|
|
|
/* ── File system ──────────────────────────── */
|
|
|
|
bool cbm_file_exists(const char *path) {
|
|
struct stat st;
|
|
return stat(path, &st) == 0;
|
|
}
|
|
|
|
bool cbm_is_dir(const char *path) {
|
|
struct stat st;
|
|
return stat(path, &st) == 0 && S_ISDIR(st.st_mode);
|
|
}
|
|
|
|
int64_t cbm_file_size(const char *path) {
|
|
struct stat st;
|
|
if (stat(path, &st) != 0) {
|
|
return CBM_NOT_FOUND;
|
|
}
|
|
return (int64_t)st.st_size;
|
|
}
|
|
|
|
char *cbm_normalize_path_sep(char *path) {
|
|
/* Normalize on ALL platforms — backslash paths can arrive via stored
|
|
* data, cross-platform DB files, or Windows-style arguments. */
|
|
if (path) {
|
|
for (char *p = path; *p; p++) {
|
|
if (*p == '\\') {
|
|
*p = '/';
|
|
}
|
|
}
|
|
cbm_canonicalize_drive(path);
|
|
}
|
|
return path;
|
|
}
|
|
|
|
#endif /* _WIN32 */
|
|
|
|
/* ── Environment variables ──────────────────────────── */
|
|
|
|
/* Thread-safe getenv: iterates environ directly instead of calling getenv().
|
|
* getenv() is flagged by concurrency-mt-unsafe because the returned pointer
|
|
* can be invalidated by setenv/putenv in another thread. We copy to a
|
|
* caller-owned buffer immediately. */
|
|
#ifdef _WIN32
|
|
#include <stdlib.h>
|
|
#define CBM_ENVIRON _environ
|
|
#elif defined(__APPLE__)
|
|
#include <crt_externs.h>
|
|
#define CBM_ENVIRON (*_NSGetEnviron())
|
|
#else
|
|
extern char **environ;
|
|
#define CBM_ENVIRON environ
|
|
#endif
|
|
|
|
static const char *platform_copy_environment_value(char *buf, size_t buf_sz, const char *value) {
|
|
if (!buf || buf_sz == 0 || !value) {
|
|
return NULL;
|
|
}
|
|
size_t length = strlen(value);
|
|
if (length >= buf_sz) {
|
|
buf[0] = '\0';
|
|
return NULL;
|
|
}
|
|
memcpy(buf, value, length + 1U);
|
|
return buf;
|
|
}
|
|
|
|
static cbm_environment_resolver_t environment_resolver;
|
|
|
|
void cbm_set_environment_resolver(cbm_environment_resolver_t resolver) {
|
|
environment_resolver = resolver;
|
|
}
|
|
|
|
const char *cbm_runtime_getenv(const char *name) {
|
|
/* A small per-thread ring supports callers that inspect several inherited
|
|
* values together without exposing libc's mutable environment storage. */
|
|
static CBM_TLS char inherited[4][CBM_SZ_4K];
|
|
static CBM_TLS unsigned slot;
|
|
const char *value = NULL;
|
|
if (environment_resolver && environment_resolver(name, &value)) {
|
|
return value;
|
|
}
|
|
char *buffer = inherited[slot++ % 4U];
|
|
return cbm_native_getenv(name, buffer, CBM_SZ_4K, NULL);
|
|
}
|
|
|
|
const char *cbm_safe_getenv(const char *name, char *buf, size_t buf_sz, const char *fallback) {
|
|
const char *value = NULL;
|
|
if (environment_resolver && environment_resolver(name, &value)) {
|
|
if (buf && buf_sz) {
|
|
buf[0] = '\0';
|
|
}
|
|
return platform_copy_environment_value(buf, buf_sz, value ? value : fallback);
|
|
}
|
|
return cbm_native_getenv(name, buf, buf_sz, fallback);
|
|
}
|
|
|
|
const char *cbm_native_getenv(const char *name, char *buf, size_t buf_sz, const char *fallback) {
|
|
if (!name || !name[0] || !buf || buf_sz == 0) {
|
|
return NULL;
|
|
}
|
|
buf[0] = '\0';
|
|
#ifdef _WIN32
|
|
/* #996 Layer 2: _environ holds ANSI-code-page bytes, NOT UTF-8. A
|
|
* non-ASCII value (USERPROFILE of C:\Users\Kovács János, or a Greek/CJK
|
|
* CBM_CACHE_DIR) arrives here either mojibake'd or with unrepresentable
|
|
* characters replaced by '?', which is INVALID in Windows paths — every
|
|
* downstream wide-safe file API then fails no matter how correct it is.
|
|
* Read the value wide and convert to genuine UTF-8, matching the
|
|
* UTF-8-path convention the rest of the codebase (cbm_fopen, _wmkdir,
|
|
* SQLite VFS) already assumes. */
|
|
{
|
|
wchar_t wname[CBM_SZ_256];
|
|
int wn = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, name, -1, wname, CBM_SZ_256);
|
|
if (wn > 0) {
|
|
SetLastError(ERROR_SUCCESS);
|
|
DWORD needed = GetEnvironmentVariableW(wname, NULL, 0U);
|
|
DWORD environment_error = GetLastError();
|
|
if (needed == 0U) {
|
|
if (environment_error == ERROR_ENVVAR_NOT_FOUND) {
|
|
return fallback ? platform_copy_environment_value(buf, buf_sz, fallback) : NULL;
|
|
}
|
|
/* An existing empty variable is distinct from a missing one. */
|
|
return buf;
|
|
}
|
|
wchar_t *wval = calloc((size_t)needed, sizeof(*wval));
|
|
if (!wval) {
|
|
return NULL;
|
|
}
|
|
SetLastError(ERROR_SUCCESS);
|
|
DWORD got = GetEnvironmentVariableW(wname, wval, needed);
|
|
DWORD read_error = GetLastError();
|
|
if (got >= needed || (got == 0U && read_error != ERROR_SUCCESS)) {
|
|
free(wval);
|
|
return NULL;
|
|
}
|
|
char *utf8 = cbm_wide_to_utf8(wval);
|
|
free(wval);
|
|
if (!utf8) {
|
|
return NULL;
|
|
}
|
|
const char *copied = platform_copy_environment_value(buf, buf_sz, utf8);
|
|
free(utf8);
|
|
return copied;
|
|
}
|
|
return NULL;
|
|
}
|
|
#else
|
|
char **env = CBM_ENVIRON;
|
|
if (env) {
|
|
size_t nlen = strlen(name);
|
|
for (; *env; env++) {
|
|
if (strncmp(*env, name, nlen) == 0 && (*env)[nlen] == '=') {
|
|
return platform_copy_environment_value(buf, buf_sz, *env + nlen + SKIP_ONE);
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
if (fallback) {
|
|
return platform_copy_environment_value(buf, buf_sz, fallback);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* See platform.h. The long-value sibling of cbm_safe_getenv: there is no caller
|
|
* buffer to overrun, so a PATH past 4 KB is read in full instead of being
|
|
* refused (#221). Both the wide staging buffer and the UTF-8 result are
|
|
* allocated through the memory core, which is also the only allocator the
|
|
* caller may release them with -- keeping the memory-core ratchet honest means
|
|
* this file adds no raw allocation site to pay for the new read. */
|
|
char *cbm_env_dup(const char *name) {
|
|
if (!name || !name[0]) {
|
|
return NULL;
|
|
}
|
|
/* The same view cbm_safe_getenv gives: the startup settings resolver
|
|
* answers first, and a handled NULL masks the variable. */
|
|
const char *resolved = NULL;
|
|
if (environment_resolver && environment_resolver(name, &resolved)) {
|
|
return resolved ? cbm_mem_strdup(CBM_MEM_CLASS_OTHER, resolved) : NULL;
|
|
}
|
|
#ifdef _WIN32
|
|
/* Same wide route as cbm_safe_getenv: _environ holds ANSI-code-page bytes,
|
|
* so a non-ASCII PATH would arrive mojibake'd or with '?' substituted --
|
|
* invalid in a Windows path and fatal to every file API downstream. */
|
|
wchar_t wname[CBM_SZ_256];
|
|
int wn = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, name, -1, wname, CBM_SZ_256);
|
|
if (wn <= 0) {
|
|
return NULL;
|
|
}
|
|
SetLastError(ERROR_SUCCESS);
|
|
DWORD needed = GetEnvironmentVariableW(wname, NULL, 0U);
|
|
DWORD lookup_error = GetLastError();
|
|
if (needed == 0U) {
|
|
if (lookup_error != ERROR_ENVVAR_NOT_FOUND) {
|
|
/* Present but empty. Distinct from unset, and callers are entitled
|
|
* to tell the two apart, so return "" and not NULL. */
|
|
return cbm_mem_strdup(CBM_MEM_CLASS_OTHER, "");
|
|
}
|
|
return NULL;
|
|
}
|
|
wchar_t *wvalue = cbm_calloc(CBM_MEM_CLASS_OTHER, (size_t)needed * sizeof(*wvalue));
|
|
if (!wvalue) {
|
|
return NULL;
|
|
}
|
|
SetLastError(ERROR_SUCCESS);
|
|
DWORD got = GetEnvironmentVariableW(wname, wvalue, needed);
|
|
DWORD read_error = GetLastError();
|
|
if (got >= needed || (got == 0U && read_error != ERROR_SUCCESS)) {
|
|
cbm_free(CBM_MEM_CLASS_OTHER, wvalue);
|
|
return NULL;
|
|
}
|
|
int utf8_size =
|
|
WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, wvalue, -1, NULL, 0, NULL, NULL);
|
|
char *utf8 = utf8_size > 0 ? cbm_alloc(CBM_MEM_CLASS_OTHER, (size_t)utf8_size) : NULL;
|
|
int converted = utf8 ? WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, wvalue, -1, utf8,
|
|
utf8_size, NULL, NULL)
|
|
: 0;
|
|
if (!utf8 || converted != utf8_size) {
|
|
cbm_free(CBM_MEM_CLASS_OTHER, utf8);
|
|
cbm_free(CBM_MEM_CLASS_OTHER, wvalue);
|
|
return NULL;
|
|
}
|
|
cbm_free(CBM_MEM_CLASS_OTHER, wvalue);
|
|
return utf8;
|
|
#else
|
|
/* Same environ walk as cbm_safe_getenv: getenv() returns a pointer another
|
|
* thread can invalidate with setenv(), so the value is copied out here. */
|
|
char **env = CBM_ENVIRON;
|
|
if (env) {
|
|
size_t name_len = strlen(name);
|
|
for (; *env; env++) {
|
|
if (strncmp(*env, name, name_len) == 0 && (*env)[name_len] == '=') {
|
|
return cbm_mem_strdup(CBM_MEM_CLASS_OTHER, *env + name_len + SKIP_ONE);
|
|
}
|
|
}
|
|
}
|
|
return NULL;
|
|
#endif
|
|
}
|
|
|
|
/* See platform.h. The shape here is the one src/main.c:1104 already uses for
|
|
* --port=: an end pointer says where the read stopped, errno catches a number
|
|
* too large, and *end == '\0' catches anything left over. */
|
|
bool cbm_env_long(const char *name, long *out) {
|
|
if (!out) {
|
|
return false;
|
|
}
|
|
char raw[CBM_SZ_64] = {0};
|
|
if (!cbm_safe_getenv(name, raw, sizeof(raw), NULL) || !raw[0]) {
|
|
return false;
|
|
}
|
|
/* strtol skips leading blanks of its own accord, so " 5" would read as 5.
|
|
* A blank in front of a setting is a slip, not a number, so refuse it here
|
|
* rather than let strtol quietly step over it. */
|
|
if (isspace((unsigned char)raw[0])) {
|
|
return false;
|
|
}
|
|
char *end = NULL;
|
|
errno = 0;
|
|
long value = strtol(raw, &end, CBM_DECIMAL_BASE);
|
|
if (errno != 0 || !end || end == raw || *end != '\0') {
|
|
return false;
|
|
}
|
|
*out = value;
|
|
return true;
|
|
}
|
|
|
|
/* ── Home directory (cross-platform) ───────────────────── */
|
|
|
|
const char *cbm_get_home_dir(void) {
|
|
static CBM_TLS char buf[CBM_SZ_1K];
|
|
char tmp[CBM_SZ_256] = "";
|
|
|
|
cbm_safe_getenv("HOME", tmp, sizeof(tmp), NULL);
|
|
if (tmp[0]) {
|
|
snprintf(buf, sizeof(buf), "%s", tmp);
|
|
cbm_normalize_path_sep(buf);
|
|
return buf;
|
|
}
|
|
|
|
cbm_safe_getenv("USERPROFILE", tmp, sizeof(tmp), NULL);
|
|
if (tmp[0]) {
|
|
snprintf(buf, sizeof(buf), "%s", tmp);
|
|
cbm_normalize_path_sep(buf);
|
|
return buf;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* ── App config directories (cross-platform) ────────── */
|
|
|
|
const char *cbm_app_config_dir(void) {
|
|
static CBM_TLS char buf[CBM_SZ_1K];
|
|
char tmp[CBM_SZ_256] = "";
|
|
#ifdef _WIN32
|
|
cbm_safe_getenv("APPDATA", tmp, sizeof(tmp), NULL);
|
|
if (tmp[0]) {
|
|
snprintf(buf, sizeof(buf), "%s", tmp);
|
|
cbm_normalize_path_sep(buf);
|
|
return buf;
|
|
}
|
|
const char *home = cbm_get_home_dir();
|
|
if (home) {
|
|
snprintf(buf, sizeof(buf), "%s/AppData/Roaming", home);
|
|
return buf;
|
|
}
|
|
return NULL;
|
|
#else
|
|
/* Linux: XDG_CONFIG_HOME or ~/.config */
|
|
cbm_safe_getenv("XDG_CONFIG_HOME", tmp, sizeof(tmp), NULL);
|
|
if (tmp[0]) {
|
|
snprintf(buf, sizeof(buf), "%s", tmp);
|
|
return buf;
|
|
}
|
|
const char *home = cbm_get_home_dir();
|
|
if (home) {
|
|
snprintf(buf, sizeof(buf), "%s/.config", home);
|
|
return buf;
|
|
}
|
|
return NULL;
|
|
#endif /* _WIN32 */
|
|
}
|
|
|
|
const char *cbm_app_local_dir(void) {
|
|
#ifdef _WIN32
|
|
static CBM_TLS char buf[CBM_SZ_1K];
|
|
char tmp[CBM_SZ_256] = "";
|
|
cbm_safe_getenv("LOCALAPPDATA", tmp, sizeof(tmp), NULL);
|
|
if (tmp[0]) {
|
|
snprintf(buf, sizeof(buf), "%s", tmp);
|
|
cbm_normalize_path_sep(buf);
|
|
return buf;
|
|
}
|
|
const char *home = cbm_get_home_dir();
|
|
if (home) {
|
|
snprintf(buf, sizeof(buf), "%s/AppData/Local", home);
|
|
return buf;
|
|
}
|
|
return NULL;
|
|
#else
|
|
return cbm_app_config_dir();
|
|
#endif
|
|
}
|
|
|
|
/* ── Cache directory ────────────────────────── */
|
|
|
|
const char *cbm_errno_name(int error) {
|
|
static const struct {
|
|
int value;
|
|
const char *name;
|
|
} names[] = {
|
|
{EACCES, "EACCES"},
|
|
{EAGAIN, "EAGAIN"},
|
|
{EBUSY, "EBUSY"},
|
|
{EEXIST, "EEXIST"},
|
|
{EFBIG, "EFBIG"},
|
|
{EINTR, "EINTR"},
|
|
{EINVAL, "EINVAL"},
|
|
{EIO, "EIO"},
|
|
{EISDIR, "EISDIR"},
|
|
{ELOOP, "ELOOP"},
|
|
{EMFILE, "EMFILE"},
|
|
{EMLINK, "EMLINK"},
|
|
{ENAMETOOLONG, "ENAMETOOLONG"},
|
|
{ENFILE, "ENFILE"},
|
|
{ENOENT, "ENOENT"},
|
|
{ENOMEM, "ENOMEM"},
|
|
{ENOSPC, "ENOSPC"},
|
|
{ENOTDIR, "ENOTDIR"},
|
|
{ENOTEMPTY, "ENOTEMPTY"},
|
|
{ENXIO, "ENXIO"},
|
|
{EPERM, "EPERM"},
|
|
{EROFS, "EROFS"},
|
|
{ETXTBSY, "ETXTBSY"},
|
|
{EXDEV, "EXDEV"},
|
|
{ETIMEDOUT, "ETIMEDOUT"},
|
|
{ECONNREFUSED, "ECONNREFUSED"},
|
|
{EADDRINUSE, "EADDRINUSE"},
|
|
{ENOTSOCK, "ENOTSOCK"},
|
|
{EPIPE, "EPIPE"},
|
|
#ifdef EDQUOT
|
|
{EDQUOT, "EDQUOT"},
|
|
#endif
|
|
#ifdef EOVERFLOW
|
|
{EOVERFLOW, "EOVERFLOW"},
|
|
#endif
|
|
#ifdef ENOTSUP
|
|
{ENOTSUP, "ENOTSUP"},
|
|
#endif
|
|
};
|
|
for (size_t i = 0; i < sizeof(names) / sizeof(names[0]); i++) {
|
|
if (names[i].value == error) {
|
|
return names[i].name;
|
|
}
|
|
}
|
|
static CBM_TLS char fallback[16];
|
|
(void)snprintf(fallback, sizeof(fallback), "%d", error);
|
|
return fallback;
|
|
}
|
|
|
|
const char *cbm_resolve_cache_dir(void) {
|
|
static CBM_TLS char buf[CBM_SZ_4K];
|
|
static const char missing[] = "\x1f"
|
|
"CBM_CACHE_DIR_MISSING"
|
|
"\x1f";
|
|
const char *configured = cbm_safe_getenv("CBM_CACHE_DIR", buf, sizeof(buf), missing);
|
|
if (!configured) {
|
|
/* Present but not representable in the product path bound. */
|
|
return NULL;
|
|
}
|
|
if (strcmp(configured, missing) != 0 && configured[0]) {
|
|
cbm_normalize_path_sep(buf);
|
|
return buf;
|
|
}
|
|
const char *home = cbm_get_home_dir();
|
|
if (!home) {
|
|
return NULL;
|
|
}
|
|
int written = snprintf(buf, sizeof(buf), "%s/.cache/codebase-memory-mcp", home);
|
|
if (written <= 0 || (size_t)written >= sizeof(buf)) {
|
|
buf[0] = '\0';
|
|
return NULL;
|
|
}
|
|
return buf;
|
|
}
|