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/link.c | |
#2
Diffstat (limited to 'src/core/link.c')
| -rw-r--r-- | src/core/link.c | 427 |
1 files changed, 427 insertions, 0 deletions
diff --git a/src/core/link.c b/src/core/link.c new file mode 100644 index 0000000..b9a38cd --- /dev/null +++ b/src/core/link.c @@ -0,0 +1,427 @@ +/* + * Link/repeater edge registry: data/links/<id>.ini, [link.<id>], stale prune. + */ +#include "hybbx/link.h" +#include "hybbx/config.h" +#include "hybbx/util.h" +#include "hybbx/log.h" + +#include <dirent.h> +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <time.h> + +static char *link_strdup(const char *s) +{ + size_t len; + char *copy; + + if (s == NULL) { + return NULL; + } + + len = strlen(s) + 1; + copy = malloc(len); + if (copy != NULL) { + memcpy(copy, s, len); + } + return copy; +} + +static const char *LINK_CODE_CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; + +static int mkdir_p(const char *path) +{ + char buf[HYBBX_PATH_MAX]; + size_t len; + size_t i; + + if (path == NULL || path[0] == '\0') { + return -1; + } + + len = strlen(path); + if (len >= sizeof(buf)) { + return -1; + } + + memcpy(buf, path, len + 1); + for (i = 1; i < len; i++) { + if (buf[i] == '/') { + buf[i] = '\0'; + if (mkdir(buf, 0755) != 0 && errno != EEXIST) { + return -1; + } + buf[i] = '/'; + } + } + + if (mkdir(buf, 0755) != 0 && errno != EEXIST) { + return -1; + } + + return 0; +} + +void hybbx_link_generate_code(char *out, size_t out_cap) +{ + static unsigned seed; + size_t i; + size_t n; + + if (out == NULL || out_cap < 2) { + return; + } + + if (seed == 0) { + seed = (unsigned)time(NULL) ^ 0x48594242u; + } + + n = out_cap - 1; + if (n > HYBBX_LINK_CODE_MAX - 1) { + n = HYBBX_LINK_CODE_MAX - 1; + } + + for (i = 0; i < n; i++) { + seed = seed * 1103515245u + 12345u; + out[i] = LINK_CODE_CHARS[(seed >> 16) % 32]; + } + out[n] = '\0'; +} + +void hybbx_link_registry_init(hybbx_link_registry_t *reg, + const char *data_dir, + const char *config_path, + unsigned stale_days) +{ + if (reg == NULL) { + return; + } + + memset(reg, 0, sizeof(*reg)); + if (data_dir != NULL && data_dir[0] != '\0') { + snprintf(reg->dir, sizeof(reg->dir), "%s/links", data_dir); + } + if (config_path != NULL) { + hybbx_strlcpy(reg->config_path, config_path, sizeof(reg->config_path)); + } + reg->stale_days = stale_days > 0 ? stale_days : HYBBX_LINK_STALE_DAYS; +} + +static hybbx_result_t link_shard_path(const hybbx_link_registry_t *reg, + const char *id, + char *out, size_t out_cap) +{ + if (reg == NULL || id == NULL || id[0] == '\0' || out == NULL) { + return HYBBX_ERR_INVALID; + } + + if (snprintf(out, out_cap, "%s/%s.ini", reg->dir, id) >= (int)out_cap) { + return HYBBX_ERR_INVALID; + } + + return HYBBX_OK; +} + +static hybbx_result_t read_link_last_seen(const char *path, time_t *last_seen) +{ + FILE *fp; + char line[256]; + time_t value = 0; + + if (path == NULL || last_seen == NULL) { + return HYBBX_ERR_INVALID; + } + + fp = fopen(path, "r"); + if (fp == NULL) { + *last_seen = 0; + return HYBBX_OK; + } + + while (fgets(line, sizeof(line), fp) != NULL) { + if (strncmp(line, "last_seen=", 10) == 0) { + value = (time_t)strtol(line + 10, NULL, 10); + } + } + + fclose(fp); + *last_seen = value; + return HYBBX_OK; +} + +static hybbx_result_t write_link_shard(const char *path, + const char *id, + const char *role, + const char *link_code, + time_t now, + int is_new) +{ + FILE *fp; + time_t created = now; + char existing_code[HYBBX_LINK_CODE_MAX]; + + existing_code[0] = '\0'; + if (!is_new) { + FILE *old = fopen(path, "r"); + char line[256]; + + if (old != NULL) { + while (fgets(line, sizeof(line), old) != NULL) { + if (strncmp(line, "created=", 8) == 0) { + created = (time_t)strtol(line + 8, NULL, 10); + } else if (strncmp(line, "link_code=", 10) == 0) { + size_t n = strcspn(line + 10, "\r\n"); + if (n >= sizeof(existing_code)) { + n = sizeof(existing_code) - 1; + } + memcpy(existing_code, line + 10, n); + existing_code[n] = '\0'; + } + } + fclose(old); + } + } + + if (link_code != NULL && link_code[0] != '\0') { + hybbx_strlcpy(existing_code, link_code, sizeof(existing_code)); + } else if (existing_code[0] == '\0') { + hybbx_link_generate_code(existing_code, sizeof(existing_code)); + } + + fp = fopen(path, "w"); + if (fp == NULL) { + return HYBBX_ERR_IO; + } + + fprintf(fp, "id=%s\n", id); + fprintf(fp, "role=%s\n", role != NULL && role[0] != '\0' ? role : "link"); + fprintf(fp, "link_code=%s\n", existing_code); + fprintf(fp, "created=%ld\n", (long)created); + fprintf(fp, "last_seen=%ld\n", (long)now); + fclose(fp); + return HYBBX_OK; +} + +static hybbx_result_t config_upsert_link_section(const char *config_path, + const char *id, + const char *role, + const char *link_code, + time_t last_seen) +{ + hybbx_config_t cfg; + char section[HYBBX_CONFIG_SECTION_MAX]; + char value[64]; + size_t i; + hybbx_result_t rc; + int found_role = 0; + int found_code = 0; + int found_seen = 0; + + if (config_path == NULL || config_path[0] == '\0' || id == NULL) { + return HYBBX_OK; + } + + rc = hybbx_config_load(&cfg, config_path); + if (rc != HYBBX_OK) { + return rc; + } + + snprintf(section, sizeof(section), "link.%s", id); + snprintf(value, sizeof(value), "%ld", (long)last_seen); + + for (i = 0; i < cfg.count; i++) { + if (cfg.entries[i].section == NULL || cfg.entries[i].key == NULL) { + continue; + } + if (strcmp(cfg.entries[i].section, section) != 0) { + continue; + } + if (strcmp(cfg.entries[i].key, "role") == 0) { + free(cfg.entries[i].value); + cfg.entries[i].value = link_strdup(role != NULL ? role : "link"); + found_role = 1; + } else if (strcmp(cfg.entries[i].key, "link_code") == 0) { + free(cfg.entries[i].value); + cfg.entries[i].value = link_strdup(link_code != NULL ? link_code : ""); + found_code = 1; + } else if (strcmp(cfg.entries[i].key, "last_seen") == 0) { + free(cfg.entries[i].value); + cfg.entries[i].value = link_strdup(value); + found_seen = 1; + } + } + + if (!found_role) { + rc = hybbx_config_set(&cfg, section, "role", + role != NULL ? role : "link"); + if (rc != HYBBX_OK) { + hybbx_config_free(&cfg); + return rc; + } + } + if (!found_code && link_code != NULL) { + rc = hybbx_config_set(&cfg, section, "link_code", link_code); + if (rc != HYBBX_OK) { + hybbx_config_free(&cfg); + return rc; + } + } + if (!found_seen) { + rc = hybbx_config_set(&cfg, section, "last_seen", value); + if (rc != HYBBX_OK) { + hybbx_config_free(&cfg); + return rc; + } + } + + rc = hybbx_config_save(&cfg, config_path); + hybbx_config_free(&cfg); + return rc; +} + +hybbx_result_t hybbx_link_registry_touch(hybbx_link_registry_t *reg, + const char *id, + const char *role, + char *link_code_out, + size_t link_code_cap) +{ + char path[HYBBX_PATH_MAX]; + char code[HYBBX_LINK_CODE_MAX]; + time_t now = time(NULL); + struct stat st; + hybbx_result_t rc; + int is_new; + + if (reg == NULL || id == NULL || id[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + if (reg->dir[0] != '\0') { + mkdir_p(reg->dir); + } + + rc = link_shard_path(reg, id, path, sizeof(path)); + if (rc != HYBBX_OK) { + return rc; + } + + is_new = stat(path, &st) != 0; + code[0] = '\0'; + if (!is_new) { + FILE *fp = fopen(path, "r"); + char line[256]; + + if (fp != NULL) { + while (fgets(line, sizeof(line), fp) != NULL) { + if (strncmp(line, "link_code=", 10) == 0) { + size_t n = strcspn(line + 10, "\r\n"); + if (n >= sizeof(code)) { + n = sizeof(code) - 1; + } + memcpy(code, line + 10, n); + code[n] = '\0'; + } + } + fclose(fp); + } + } + if (code[0] == '\0') { + hybbx_link_generate_code(code, sizeof(code)); + } + + rc = write_link_shard(path, id, role, code, now, is_new); + if (rc != HYBBX_OK) { + return rc; + } + + if (link_code_out != NULL && link_code_cap > 0) { + hybbx_strlcpy(link_code_out, code, link_code_cap); + } + + return config_upsert_link_section(reg->config_path, id, role, code, now); +} + +static hybbx_result_t remove_link_files(const hybbx_link_registry_t *reg, + const char *id) +{ + char path[HYBBX_PATH_MAX]; + hybbx_result_t rc; + + rc = link_shard_path(reg, id, path, sizeof(path)); + if (rc == HYBBX_OK) { + remove(path); + } + + if (reg->config_path[0] != '\0') { + hybbx_config_t cfg; + char section[HYBBX_CONFIG_SECTION_MAX]; + + snprintf(section, sizeof(section), "link.%s", id); + if (hybbx_config_load(&cfg, reg->config_path) == HYBBX_OK) { + hybbx_config_remove_section(&cfg, section); + hybbx_config_save(&cfg, reg->config_path); + hybbx_config_free(&cfg); + } + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_link_registry_prune(hybbx_link_registry_t *reg) +{ + DIR *dir; + struct dirent *ent; + time_t now = time(NULL); + time_t cutoff; + char id[HYBBX_LINK_ID_MAX]; + + if (reg == NULL || reg->dir[0] == '\0') { + return HYBBX_OK; + } + + cutoff = now - (time_t)reg->stale_days * 24 * 60 * 60; + + dir = opendir(reg->dir); + if (dir == NULL) { + return HYBBX_OK; + } + + while ((ent = readdir(dir)) != NULL) { + const char *name = ent->d_name; + size_t nlen = strlen(name); + time_t last_seen; + char path[HYBBX_PATH_MAX]; + + if (nlen < 5 || strcmp(name + nlen - 4, ".ini") != 0) { + continue; + } + + nlen -= 4; + if (nlen >= sizeof(id)) { + continue; + } + memcpy(id, name, nlen); + id[nlen] = '\0'; + + if (snprintf(path, sizeof(path), "%s/%s", reg->dir, name) >= (int)sizeof(path)) { + continue; + } + + if (read_link_last_seen(path, &last_seen) != HYBBX_OK) { + continue; + } + + if (last_seen > 0 && last_seen < cutoff) { + hybbx_log_info("[links] removing stale link '%s' (last auth %ld days ago)", + id, (long)((now - last_seen) / (24 * 60 * 60))); + remove_link_files(reg, id); + } + } + + closedir(dir); + return HYBBX_OK; +} |
