diff options
| author | info@mode42.com <info@mode42.com> | 2026-08-08 03:54:55 +0000 |
|---|---|---|
| committer | info@mode42.com <info@mode42.com> | 2026-08-08 03:54:55 +0000 |
| commit | 20cb29c2f8c5c87bc590896854a20b1473ceb358 (patch) | |
| tree | 2857f41513a56ad41af97b57362639298aa7f033 /src/core/util.c | |
#2
Diffstat (limited to 'src/core/util.c')
| -rw-r--r-- | src/core/util.c | 832 |
1 files changed, 832 insertions, 0 deletions
diff --git a/src/core/util.c b/src/core/util.c new file mode 100644 index 0000000..1c5e9e4 --- /dev/null +++ b/src/core/util.c @@ -0,0 +1,832 @@ +#if defined(__linux__) +#define _DEFAULT_SOURCE +#endif + +#include "hybbx/util.h" +#include "hybbx/limits.h" +#include "hybbx/socket.h" +#if !defined(HYBBX_CLIENT_BUILD) +#include "hybbx/log.h" +#endif + +#include <ctype.h> +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> + +#if !defined(_WIN32) && !defined(__AMIGA__) +#include <arpa/inet.h> +#include <errno.h> +#include <netinet/in.h> +#include <sys/socket.h> +#endif + +#if !defined(_WIN32) && !defined(__AMIGA__) +#include <sys/utsname.h> +#endif + +#if !defined(HYBBX_CLIENT_BUILD) +#include "hybbx/config.h" +#endif + +static int bool_token_ieq(const char *value, const char *token) +{ + if (value == NULL || token == NULL) { + return 0; + } + + while (*value != '\0' && *token != '\0') { + unsigned char cv = (unsigned char)tolower((unsigned char)*value); + unsigned char ct = (unsigned char)tolower((unsigned char)*token); + + if (cv != ct) { + return 0; + } + value++; + token++; + } + + return *value == '\0' && *token == '\0'; +} + +static int bool_match_any(const char *value, const char *const *tokens, size_t count) +{ + size_t i; + + if (value == NULL || value[0] == '\0') { + return 0; + } + + for (i = 0; i < count; i++) { + if (bool_token_ieq(value, tokens[i])) { + return 1; + } + } + + return 0; +} + +static const char *const hybbx_bool_true_tokens[] = { + HYBBX_BOOL_YES, "true", "enable", "enabled", "on", "1" +}; + +static const char *const hybbx_bool_false_tokens[] = { + HYBBX_BOOL_NO, "false", "disable", "disabled", "off", "0" +}; + +int hybbx_bool_is_true(const char *value) +{ + return bool_match_any(value, hybbx_bool_true_tokens, + sizeof(hybbx_bool_true_tokens) / + sizeof(hybbx_bool_true_tokens[0])); +} + +int hybbx_bool_is_false(const char *value) +{ + return bool_match_any(value, hybbx_bool_false_tokens, + sizeof(hybbx_bool_false_tokens) / + sizeof(hybbx_bool_false_tokens[0])); +} + +int hybbx_parse_bool(const char *value, int default_value) +{ + if (value == NULL || value[0] == '\0') { + return default_value; + } + + if (hybbx_bool_is_true(value)) { + return 1; + } + + if (hybbx_bool_is_false(value)) { + return 0; + } + + return default_value; +} + +const char *hybbx_bool_to_string(int value) +{ + return value ? HYBBX_BOOL_YES : HYBBX_BOOL_NO; +} + +const char *hybbx_result_name(hybbx_result_t rc) +{ + switch (rc) { + case HYBBX_OK: + return "ok"; + case HYBBX_LOCAL_CMD: + return "local_cmd"; + case HYBBX_SESSION_END: + return "session_end"; + case HYBBX_ERR_INVALID: + return "invalid"; + case HYBBX_ERR_NOMEM: + return "nomem"; + case HYBBX_ERR_NOT_FOUND: + return "not_found"; + case HYBBX_ERR_IO: + return "io"; + case HYBBX_ERR_UNSUPPORTED: + return "unsupported"; + case HYBBX_ERR_BUSY: + return "busy"; + case HYBBX_ERR_DENIED: + return "denied"; + default: + return "unknown"; + } +} + +size_t hybbx_strlcpy(char *dst, const char *src, size_t dst_size) +{ + size_t src_len; + + if (dst_size == 0) { + return src != NULL ? strlen(src) : 0; + } + + if (dst == NULL) { + return 0; + } + + if (src == NULL) { + dst[0] = '\0'; + return 0; + } + + src_len = strlen(src); + if (src_len >= dst_size) { + memcpy(dst, src, dst_size - 1); + dst[dst_size - 1] = '\0'; + return dst_size - 1; + } + + memcpy(dst, src, src_len + 1); + return src_len; +} + +hybbx_result_t hybbx_default_user_data_path(char *out, size_t out_len) +{ + if (out == NULL || out_len == 0) { + return HYBBX_ERR_INVALID; + } + + if (hybbx_strlcpy(out, HYBBX_DIR_DATA, out_len) >= out_len) { + return HYBBX_ERR_INVALID; + } + + return HYBBX_OK; +} + +static char g_install_root[HYBBX_PATH_MAX]; + +void hybbx_install_root_set(const char *root) +{ + if (root == NULL || root[0] == '\0') { + g_install_root[0] = '\0'; + return; + } + + hybbx_strlcpy(g_install_root, root, sizeof(g_install_root)); +} + +const char *hybbx_install_root_get(void) +{ + return g_install_root[0] != '\0' ? g_install_root : NULL; +} + +static int path_is_absolute(const char *path) +{ + if (path == NULL || path[0] == '\0') { + return 0; + } + + if (path[0] == '/') { + return 1; + } + +#if defined(_WIN32) || defined(__CYGWIN__) + if (((path[0] >= 'A' && path[0] <= 'Z') || + (path[0] >= 'a' && path[0] <= 'z')) && + path[1] == ':') { + return 1; + } +#endif + + return 0; +} + +hybbx_result_t hybbx_path_resolve(char *out, size_t out_len, const char *path) +{ + char expanded[HYBBX_PATH_MAX]; + const char *root; + hybbx_result_t rc; + + if (out == NULL || out_len == 0) { + return HYBBX_ERR_INVALID; + } + + rc = hybbx_path_expand(expanded, sizeof(expanded), path); + if (rc != HYBBX_OK) { + return rc; + } + + if (path_is_absolute(expanded)) { + if (hybbx_strlcpy(out, expanded, out_len) >= out_len) { + return HYBBX_ERR_INVALID; + } + return HYBBX_OK; + } + + root = hybbx_install_root_get(); + if (root != NULL && root[0] != '\0') { + return hybbx_path_join(out, out_len, root, expanded); + } + + if (hybbx_strlcpy(out, expanded, out_len) >= out_len) { + return HYBBX_ERR_INVALID; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_path_dirname(const char *path, char *out, size_t out_len) +{ + char copy[HYBBX_PATH_MAX]; + char *slash; + + if (path == NULL || out == NULL || out_len == 0) { + return HYBBX_ERR_INVALID; + } + + if (hybbx_strlcpy(copy, path, sizeof(copy)) >= sizeof(copy)) { + return HYBBX_ERR_INVALID; + } + + slash = strrchr(copy, '/'); + if (slash == NULL) { + if (hybbx_strlcpy(out, ".", out_len) >= out_len) { + return HYBBX_ERR_INVALID; + } + return HYBBX_OK; + } + + if (slash == copy) { + if (hybbx_strlcpy(out, "/", out_len) >= out_len) { + return HYBBX_ERR_INVALID; + } + return HYBBX_OK; + } + + *slash = '\0'; + if (hybbx_strlcpy(out, copy, out_len) >= out_len) { + return HYBBX_ERR_INVALID; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_platform_os_name(char *out, size_t out_len) +{ + const char *name; + + if (out == NULL || out_len == 0) { + return HYBBX_ERR_INVALID; + } + +#if defined(_WIN32) + name = "Windows"; +#elif defined(__AMIGA__) + name = "AmigaOS"; +#else + { + struct utsname u; + + if (uname(&u) != 0) { + return HYBBX_ERR_IO; + } + + name = u.sysname; + if (name == NULL || name[0] == '\0') { + return HYBBX_ERR_IO; + } + + if (strcmp(name, "Darwin") == 0) { + name = "MacOS"; + } + } +#endif + + if (hybbx_strlcpy(out, name, out_len) >= out_len) { + return HYBBX_ERR_INVALID; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_path_expand(char *out, size_t out_len, const char *path) +{ + const char *home; + + if (out == NULL || out_len == 0) { + return HYBBX_ERR_INVALID; + } + + if (path == NULL || path[0] == '\0') { + return hybbx_default_user_data_path(out, out_len); + } + + if (path[0] != '~') { + if (hybbx_strlcpy(out, path, out_len) >= out_len) { + return HYBBX_ERR_INVALID; + } + return HYBBX_OK; + } + + if (path[1] != '\0' && path[1] != '/') { + return HYBBX_ERR_INVALID; + } + + home = getenv("HOME"); + if (home == NULL || home[0] == '\0') { + if (path[1] == '\0') { + return hybbx_default_user_data_path(out, out_len); + } + if (hybbx_strlcpy(out, path + 2, out_len) >= out_len) { + return HYBBX_ERR_INVALID; + } + return HYBBX_OK; + } + + if (path[1] == '\0') { + if (hybbx_strlcpy(out, home, out_len) >= out_len) { + return HYBBX_ERR_INVALID; + } + return HYBBX_OK; + } + + if (snprintf(out, out_len, "%s%s", home, path + 1) >= (int)out_len) { + return HYBBX_ERR_INVALID; + } + + return HYBBX_OK; +} + +int hybbx_size_ok(size_t len) +{ + return len > 0 && len <= HYBBX_ALLOC_MAX; +} + +static int path_component_valid(const char *name) +{ + if (name == NULL || name[0] == '\0') { + return 0; + } + + if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) { + return 0; + } + + if (strchr(name, '/') != NULL || strchr(name, '\\') != NULL) { + return 0; + } + + return 1; +} + +hybbx_result_t hybbx_path_join(char *out, size_t out_len, + const char *base, const char *name) +{ + size_t base_len; + int need_slash; + int written; + + if (out == NULL || out_len == 0 || base == NULL || name == NULL) { + return HYBBX_ERR_INVALID; + } + + if (!path_component_valid(name)) { + return HYBBX_ERR_INVALID; + } + + base_len = strlen(base); + if (!hybbx_size_ok(base_len + 1) || !hybbx_size_ok(strlen(name) + 1)) { + return HYBBX_ERR_INVALID; + } + + need_slash = base_len > 0 && base[base_len - 1] != '/'; + written = snprintf(out, out_len, "%s%s%s", base, need_slash ? "/" : "", name); + if (written < 0 || (size_t)written >= out_len) { + return HYBBX_ERR_INVALID; + } + + return HYBBX_OK; +} + +static hybbx_time_format_t g_time_format; +static int g_time_format_ready; + +void hybbx_time_format_defaults(hybbx_time_format_t *fmt) +{ + if (fmt == NULL) { + return; + } + + fmt->clock_12h = 0; + fmt->seconds = 1; + fmt->date_format = HYBBX_DATE_ISO; +} + +const char *hybbx_date_format_name(hybbx_date_format_t fmt) +{ + switch (fmt) { + case HYBBX_DATE_ISO_SHORT: + return "iso_short"; + case HYBBX_DATE_US: + return "us"; + case HYBBX_DATE_EU: + return "eu"; + case HYBBX_DATE_ISO: + default: + return "iso"; + } +} + +#if !defined(HYBBX_CLIENT_BUILD) +static hybbx_date_format_t parse_date_format(const char *value) +{ + if (value == NULL || value[0] == '\0') { + return HYBBX_DATE_ISO; + } + + if (bool_token_ieq(value, "iso") || bool_token_ieq(value, "yyyy_mm_dd") || + bool_token_ieq(value, "yyyy/mm/dd")) { + return HYBBX_DATE_ISO; + } + + if (bool_token_ieq(value, "iso_short") || bool_token_ieq(value, "yy_mm_dd") || + bool_token_ieq(value, "yy/mm/dd")) { + return HYBBX_DATE_ISO_SHORT; + } + + if (bool_token_ieq(value, "us") || bool_token_ieq(value, "mm_dd_yyyy") || + bool_token_ieq(value, "mm/dd/yyyy")) { + return HYBBX_DATE_US; + } + + if (bool_token_ieq(value, "eu") || bool_token_ieq(value, "dd_mm_yyyy") || + bool_token_ieq(value, "dd/mm/yyyy")) { + return HYBBX_DATE_EU; + } + + return HYBBX_DATE_ISO; +} +#endif + +hybbx_result_t hybbx_time_local_now(struct tm *out) +{ + time_t now; + + if (out == NULL) { + return HYBBX_ERR_INVALID; + } + + now = time(NULL); + if (now == (time_t)-1) { + return HYBBX_ERR_IO; + } + +#if defined(_WIN32) + if (localtime_s(out, &now) != 0) { + return HYBBX_ERR_IO; + } +#else + if (localtime_r(&now, out) == NULL) { + return HYBBX_ERR_IO; + } +#endif + + return HYBBX_OK; +} + +const hybbx_time_format_t *hybbx_time_format_get(void) +{ + if (!g_time_format_ready) { + hybbx_time_format_defaults(&g_time_format); + g_time_format_ready = 1; + } + + return &g_time_format; +} + +#if !defined(HYBBX_CLIENT_BUILD) +void hybbx_time_config_apply(const struct hybbx_config *config) +{ + const char *clock; + int clock_12h; + + hybbx_time_format_defaults(&g_time_format); + + if (config != NULL) { + clock_12h = hybbx_config_get_bool(config, "time", "clock_12h", 0); + if (!clock_12h) { + clock_12h = hybbx_config_get_bool(config, "time", "am_pm", 0); + } + + clock = hybbx_config_get(config, "time", "clock", NULL); + if (clock != NULL && clock[0] != '\0') { + if (bool_token_ieq(clock, "12h") || bool_token_ieq(clock, "12") || + bool_token_ieq(clock, "am_pm") || bool_token_ieq(clock, "am/pm")) { + clock_12h = 1; + } else if (bool_token_ieq(clock, "24h") || bool_token_ieq(clock, "24")) { + clock_12h = 0; + } + } + + g_time_format.clock_12h = clock_12h; + g_time_format.seconds = + hybbx_config_get_bool(config, "time", "seconds", 1); + g_time_format.date_format = parse_date_format( + hybbx_config_get(config, "time", "date", NULL)); + } + + g_time_format_ready = 1; + + hybbx_log_info("[time] clock=%s seconds=%s date=%s", + g_time_format.clock_12h ? "12h" : "24h", + hybbx_bool_to_string(g_time_format.seconds), + hybbx_date_format_name(g_time_format.date_format)); +} +#else +void hybbx_time_config_apply(const struct hybbx_config *config) +{ + (void)config; + hybbx_time_format_defaults(&g_time_format); + g_time_format_ready = 1; +} +#endif + +hybbx_result_t hybbx_time_format_time(char *out, size_t out_len, + const struct tm *tm, + const hybbx_time_format_t *fmt) +{ + int hour; + int minute; + int second; + const char *suffix; + int n; + + if (out == NULL || out_len == 0 || tm == NULL) { + return HYBBX_ERR_INVALID; + } + + if (fmt == NULL) { + fmt = hybbx_time_format_get(); + } + + hour = tm->tm_hour; + minute = tm->tm_min; + second = tm->tm_sec; + suffix = ""; + + if (fmt->clock_12h) { + if (hour >= 12) { + suffix = "pm"; + if (hour > 12) { + hour -= 12; + } + } else { + suffix = "am"; + if (hour == 0) { + hour = 12; + } + } + } + + if (fmt->clock_12h) { + if (fmt->seconds) { + n = snprintf(out, out_len, "%02d:%02d:%02d %s", + hour, minute, second, suffix); + } else { + n = snprintf(out, out_len, "%02d:%02d %s", hour, minute, suffix); + } + } else if (fmt->seconds) { + n = snprintf(out, out_len, "%02d:%02d:%02d", hour, minute, second); + } else { + n = snprintf(out, out_len, "%02d:%02d", hour, minute); + } + + if (n < 0 || (size_t)n >= out_len) { + out[0] = '\0'; + return HYBBX_ERR_INVALID; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_time_format_date(char *out, size_t out_len, + const struct tm *tm, + const hybbx_time_format_t *fmt) +{ + int year; + int month; + int day; + int n; + + if (out == NULL || out_len == 0 || tm == NULL) { + return HYBBX_ERR_INVALID; + } + + if (fmt == NULL) { + fmt = hybbx_time_format_get(); + } + + year = tm->tm_year + 1900; + month = tm->tm_mon + 1; + day = tm->tm_mday; + + switch (fmt->date_format) { + case HYBBX_DATE_ISO_SHORT: + n = snprintf(out, out_len, "%02d/%02d/%02d", + year % 100, month, day); + break; + case HYBBX_DATE_US: + n = snprintf(out, out_len, "%02d/%02d/%04d", month, day, year); + break; + case HYBBX_DATE_EU: + n = snprintf(out, out_len, "%02d/%02d/%04d", day, month, year); + break; + case HYBBX_DATE_ISO: + default: + n = snprintf(out, out_len, "%04d/%02d/%02d", year, month, day); + break; + } + + if (n < 0 || (size_t)n >= out_len) { + out[0] = '\0'; + return HYBBX_ERR_INVALID; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_time_format_stamp(char *out, size_t out_len, + const struct tm *tm, + const hybbx_time_format_t *fmt) +{ + int year; + int month; + int day; + int hour; + int minute; + int second; + const char *suffix; + int n; + + if (out == NULL || out_len == 0 || tm == NULL) { + return HYBBX_ERR_INVALID; + } + + if (fmt == NULL) { + fmt = hybbx_time_format_get(); + } + + year = tm->tm_year + 1900; + month = tm->tm_mon + 1; + day = tm->tm_mday; + hour = tm->tm_hour; + minute = tm->tm_min; + second = tm->tm_sec; + suffix = ""; + + if (fmt->clock_12h) { + if (hour >= 12) { + suffix = "pm"; + if (hour > 12) { + hour -= 12; + } + } else { + suffix = "am"; + if (hour == 0) { + hour = 12; + } + } + } + + if (fmt->clock_12h) { + if (fmt->seconds) { + n = snprintf(out, out_len, "%04d%02d%02d %02d:%02d:%02d %s", + year, month, day, hour, minute, second, suffix); + } else { + n = snprintf(out, out_len, "%04d%02d%02d %02d:%02d %s", + year, month, day, hour, minute, suffix); + } + } else if (fmt->seconds) { + n = snprintf(out, out_len, "%04d%02d%02d %02d:%02d:%02d", + year, month, day, hour, minute, second); + } else { + n = snprintf(out, out_len, "%04d%02d%02d %02d:%02d", + year, month, day, hour, minute); + } + + if (n < 0 || (size_t)n >= out_len) { + out[0] = '\0'; + return HYBBX_ERR_INVALID; + } + + return HYBBX_OK; +} + +void hybbx_socket_nosigpipe(int fd) +{ +#ifdef SO_NOSIGPIPE + int on = 1; + + if (fd >= 0) { + (void)setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &on, sizeof(on)); + } +#else + (void)fd; +#endif +} + +void hybbx_socket_log_bind_failure(const char *component, const char *addr, + unsigned port) +{ + int err = errno; + + if (component == NULL || addr == NULL) { + return; + } + +#if !defined(HYBBX_CLIENT_BUILD) + hybbx_log_warn("[%s] failed to bind %s:%u (%s)", component, addr, port, + strerror(err)); + if (err == EADDRINUSE) { + hybbx_log_warn("[%s] port %u already in use — another HyBBX instance may " + "already be running (ss -tlnp | grep %u)", + component, port, port); + } +#else + fprintf(stderr, "[%s] failed to bind %s:%u (%s)\n", component, addr, port, + strerror(err)); + if (err == EADDRINUSE) { + fprintf(stderr, + "[%s] port %u already in use — another HyBBX instance may " + "already be running (ss -tlnp | grep %u)\n", + component, port, port); + } +#endif +} + +hybbx_result_t hybbx_socket_peer_name(int fd, char *buf, size_t buf_len) +{ +#if defined(_WIN32) || defined(__AMIGA__) + (void)fd; + (void)buf; + (void)buf_len; + return HYBBX_ERR_UNSUPPORTED; +#else + struct sockaddr_storage addr; + socklen_t addr_len = sizeof(addr); + const void *ip; + char ip_buf[INET6_ADDRSTRLEN]; + + if (buf == NULL || buf_len == 0) { + return HYBBX_ERR_INVALID; + } + + buf[0] = '\0'; + + if (fd < 0) { + return HYBBX_ERR_INVALID; + } + + if (getpeername(fd, (struct sockaddr *)&addr, &addr_len) != 0) { + return HYBBX_ERR_IO; + } + + if (addr.ss_family == AF_INET) { + const struct sockaddr_in *in4 = (const struct sockaddr_in *)&addr; + + ip = &in4->sin_addr; + } else if (addr.ss_family == AF_INET6) { + const struct sockaddr_in6 *in6 = (const struct sockaddr_in6 *)&addr; + + ip = &in6->sin6_addr; + } else { + return HYBBX_ERR_UNSUPPORTED; + } + + if (inet_ntop(addr.ss_family, ip, ip_buf, sizeof(ip_buf)) == NULL) { + return HYBBX_ERR_IO; + } + + hybbx_strlcpy(buf, ip_buf, buf_len); + return HYBBX_OK; +#endif +} |
