deployd/script/metadata.c
2025-08-02 02:03:28 +03:00

627 lines
17 KiB
C

#include "include/conf.h"
#include <ctype.h>
#include <strings.h>
#include <pwd.h>
#include <grp.h>
#include "include/def.h"
#include "include/metadata.h"
static bool sh_meta_parse_name(char name[SH_META_NAME_MAX + 1],
const char *val,
size_t val_len);
static bool sh_meta_parse_extensions(
char exts[SH_META_EXTENSION_MAX + 1][SH_META_EXTENSIONS_MAX],
size_t *countp,
const char *val,
size_t val_len);
static bool sh_meta_parse_user(uid_t *userp, const char *val, size_t val_len);
static bool sh_meta_parse_groups(gid_t grps[SH_META_GROUPS_MAX],
size_t *countp,
const char *val,
size_t val_len);
static bool sh_meta_parse_target(char target[SH_META_PATH_MAX + 1],
const char *val,
size_t val_len);
static bool
sh_meta_parse_copy(bool *copy, bool copy_set, const char *val, size_t val_len);
static bool sh_meta_parse_key(char key[SH_META_KEY_MAX + 1],
const char *val,
size_t val_len);
static uid_t sh_get_uid_by_name(const char *value, size_t length);
static gid_t sh_get_gid_by_name(const char *value, size_t length);
static bool sh_meta_is_delim(const char **bufp);
bool sh_Metadata_read(sh_Metadata *metadata, const char *buf) {
if (!metadata || !buf) {
return false;
}
bool in_meta = false;
while (*buf) {
/* Find seperator */
in_meta = sh_meta_is_delim(&buf);
if (in_meta) {
break;
}
/* Skip to next line */
while (*buf && *buf != '\n' && *buf != '\r') {
++buf;
}
if (*buf == '\r') {
++buf;
}
if (*buf == '\n') {
++buf;
}
}
if (!in_meta) {
sh_print_error("No metadata block found (did you forget # -----...?)");
return NULL;
}
*metadata->name = '\0';
metadata->user = (uid_t)-1;
metadata->extensions_count = 0;
metadata->groups_count = 0;
*metadata->target = '\0';
bool copy_set = false;
while (*buf) {
if (*buf != '#') {
sh_print_error("Metadata lines must start with '#'");
return false;
}
if (sh_meta_is_delim(&buf)) {
in_meta = false;
break;
}
/* Skip '#' if not delimiter */
++buf;
/* Skip any whitespace after '#' */
if (!sh_str_lstrip(&buf)) {
return false;
}
const char *key = buf;
/* Locate ':' to separate key and value */
const char *colon = strchr(buf, ':');
if (!colon) {
sh_print_error(
"Metadata lines must have a ':' key-value separator");
return false;
}
/* Trim trailing whitespace before ':' */
const char *key_end = colon - 1;
while (key_end > key && (*key_end == ' ' || *key_end == '\t')) {
--key_end;
}
const size_t key_len =
(size_t)(key_end >= key ? (key_end - key + 1) : 0);
if (key_len == 0) {
sh_print_errorf("Metadata key '%.*s' must not be empty",
(int)key_len, key);
return false;
}
/* Start of value */
const char *val = colon + 1;
while (*val == ' ' || *val == '\t') {
++val;
}
/* Find end of value (until \n, \r or \0) */
const char *val_end = val;
while (*val_end && *val_end != '\n' && *val_end != '\r') {
++val_end;
}
/* Trim trailing whitespace from value */
const char *value_end = val_end - 1;
while (value_end >= val && (*value_end == ' ' || *value_end == '\t')) {
--value_end;
}
size_t val_len = (size_t)(value_end >= val ? (value_end - val + 1) : 0);
if (val_len == 0) {
sh_print_errorf("Metadata key '%.*s' value must not be empty",
(int)key_len, key);
return false;
}
/* Safety check for newlines in key/value */
if (sh_strnchr(key, '\n', key_len) || sh_strnchr(key, '\r', key_len) ||
sh_strnchr(val, '\n', val_len) || sh_strnchr(val, '\r', val_len)) {
sh_print_errorf(
"Metadata key '%.*s' or value contains an unexpected newline",
(int)key_len, key);
return false;
}
bool ret = false;
if (key_len == 4 && strncasecmp(key, "Name", 4) == 0) {
ret = sh_meta_parse_name(metadata->name, val, val_len);
} else if (key_len == 10 && strncasecmp(key, "Extensions", 10) == 0) {
ret = sh_meta_parse_extensions(metadata->extensions,
&metadata->extensions_count, val,
val_len);
} else if (key_len == 4 && strncasecmp(key, "User", 4) == 0) {
ret = sh_meta_parse_user(&metadata->user, val, val_len);
} else if (key_len == 6 && strncasecmp(key, "Groups", 6) == 0) {
ret = sh_meta_parse_groups(metadata->groups,
&metadata->groups_count, val, val_len);
} else if (key_len == 6 && strncasecmp(key, "Target", 6) == 0) {
ret = sh_meta_parse_target(metadata->target, val, val_len);
} else if (key_len == 4 && strncasecmp(key, "Copy", 4) == 0) {
ret = sh_meta_parse_copy(&metadata->copy, copy_set, val, val_len);
copy_set = ret;
} else if (key_len == 3 && strncasecmp(key, "Key", 3) == 0) {
ret = sh_meta_parse_key(metadata->key, val, val_len);
} else {
sh_print_errorf("Unknown metadata key '%.*s'", (int)key_len, key);
return false;
}
if (!ret) {
return false;
}
/* Advance buf to the next line */
buf = val_end;
if (*buf == '\r' && *(buf + 1) == '\n') {
buf += 2;
} else if (*buf == '\r' || *buf == '\n') {
++buf;
}
}
if (in_meta) {
sh_print_error(
"No closing metadata delimiter found (did you forget #-----...?)");
return false;
}
return true;
}
void sh_Metadata_print(const sh_Metadata *metadata) {
if (!metadata) {
return;
}
printf("Name: %s\n", metadata->name);
if (metadata->extensions_count > 0) {
printf("Extensions (%zu):", metadata->extensions_count);
for (size_t idx = 0; idx < metadata->extensions_count; ++idx) {
printf(" %s%s", metadata->extensions[idx],
idx + 1 == metadata->extensions_count ? "" : ",");
}
printf("\n");
} else {
printf("Extensions: (none)\n");
}
const struct passwd *pwd = getpwuid(metadata->user);
printf("User: %s (%d)\n", pwd ? pwd->pw_name : "unknown", metadata->user);
if (metadata->groups_count > 0) {
printf("groups (%zu):", metadata->groups_count);
for (size_t idx = 0; idx < metadata->groups_count; ++idx) {
const struct group *grp = getgrgid(metadata->groups[idx]);
printf(" %s (%u)%s", grp->gr_name, grp->gr_gid,
idx + 1 == metadata->groups_count ? "" : ",");
}
printf("\n");
} else {
printf("Groups: (none)\n");
}
printf("Target: %s\n", metadata->target);
printf("Copy: %s\n", metadata->copy ? "yes" : "no");
if (*metadata->key) {
printf("Key: %s\n", metadata->key);
}
}
bool sh_Metadata_is_full_script(const sh_Metadata *metadata) {
if (!metadata) {
return false;
}
if (*metadata->target == '\0') {
return false;
}
if (metadata->user == (uid_t)-1 || metadata->user == 0) {
return false;
}
if (metadata->extensions_count > 0) {
for (size_t idx = 0; idx < metadata->extensions_count; ++idx) {
if (!*metadata->extensions[idx]) {
return false;
}
if (!sh_is_lowstr(metadata->extensions[idx]) ||
!sh_is_valid_identifier(metadata->extensions[idx],
strlen(metadata->extensions[idx]))) {
return false;
}
}
}
if (metadata->groups_count > 0) {
for (size_t idx = 0; idx < metadata->groups_count; ++idx) {
if (metadata->groups[idx] == (gid_t)-1 ||
metadata->groups[idx] == 0) {
return false;
}
}
} else {
return false;
}
return true;
}
static bool sh_meta_parse_name(char name[SH_META_NAME_MAX + 1],
const char *val,
size_t val_len) {
if (val_len > SH_META_NAME_MAX) {
sh_print_errorf("Deployer name '%.*s' too long", (int)val_len, val);
return false;
}
if (*name != '\0') {
sh_print_errorf("Deployer name '%s' already set", name);
return false;
}
strncpy(name, val, val_len);
name[SH_META_NAME_MAX] = '\0';
return true;
}
static bool sh_meta_parse_extensions(
char exts[SH_META_EXTENSION_MAX + 1][SH_META_EXTENSIONS_MAX],
size_t *countp,
const char *val,
size_t val_len) {
if (*countp != 0) {
sh_print_error("Deployer extensions already set");
return false;
}
size_t count = 0;
const char *ptr = val;
const char *end = val + val_len;
while (ptr < end) {
/* Skip leading whitespace */
while (ptr < end && (*ptr == ' ' || *ptr == '\t')) {
++ptr;
}
if (ptr >= end) {
break;
}
const char *tok_start = ptr;
/* Find end of token */
while (ptr < end && *ptr != ' ' && *ptr != '\t') {
++ptr;
}
const char *tok_end = ptr;
const size_t len = (size_t)(tok_end - tok_start);
if (len == 0) {
sh_print_error("Length 0 extension is invalid");
return false;
}
if (count >= SH_META_EXTENSIONS_MAX) {
sh_print_error("Too many deployer extensions");
return false;
}
if (len > SH_META_EXTENSION_MAX) {
sh_print_errorf("Extension identifier '%.*s' too long", (int)len,
tok_start);
return false;
}
if (!sh_is_valid_identifier(tok_start, len)) {
sh_print_errorf("Invalid extension identifier '%.*s'", (int)len,
tok_start);
return false;
}
/* Lowercase into struct storage */
for (size_t idx = 0; idx < len; ++idx) {
exts[count][idx] = (char)tolower((unsigned char)tok_start[idx]);
}
exts[count][len] = '\0';
/* Check uniqueness */
for (size_t idx = 0; idx < count; ++idx) {
if (strcmp(exts[idx], exts[count]) == 0) {
sh_print_errorf("Duplicate extension identifier '%s'",
exts[idx]);
return false;
}
}
++count;
}
if (count == 0) {
sh_print_error(
"Deployer 'Extensions' must have at least one identifier");
return false;
}
*countp = count;
return true;
}
static bool sh_meta_parse_user(uid_t *userp, const char *val, size_t val_len) {
uid_t user = *userp;
if (user != (uid_t)-1 && user != 0) {
sh_print_errorf("Deployer user '%u' already set", *userp);
return false;
}
user = sh_get_uid_by_name(val, val_len);
if (user == (uid_t)-1 || user == 0) {
return false;
}
*userp = user;
return true;
}
static bool sh_meta_parse_groups(gid_t grps[SH_META_GROUPS_MAX],
size_t *countp,
const char *val,
size_t val_len) {
if (*countp != 0) {
sh_print_errorf("Deployer groups (%zu) already set", *countp);
return false;
}
size_t count = 0;
const char *ptr = val;
const char *end = val + val_len;
while (ptr < end) {
/* Skip leading whitespace */
while (ptr < end && (*ptr == ' ' || *ptr == '\t')) {
++ptr;
}
if (ptr >= end) {
break;
}
const char *tok_start = ptr;
/* Find end of token */
while (ptr < end && *ptr != ' ' && *ptr != '\t') {
++ptr;
}
const char *tok_end = ptr;
size_t len = (size_t)(tok_end - tok_start);
if (len == 0) {
sh_print_error("Length 0 group name is invalid");
return false;
}
if (count >= SH_META_GROUPS_MAX) {
sh_print_error("Too many deployer groups");
return false;
}
const gid_t gid = sh_get_gid_by_name(tok_start, len);
if (gid == (gid_t)-1 || gid == 0) {
return false;
}
/* Ensure uniqueness */
for (size_t idx = 0; idx < count; ++idx) {
if (grps[idx] == gid) {
sh_print_errorf("Duplicate group entry '%.*s'", (int)len,
tok_start);
return false;
}
}
grps[count++] = gid;
}
if (count == 0) {
sh_print_error("Deployer 'Groups' must have at least one group");
return false;
}
*countp = count;
return true;
}
static bool sh_meta_parse_target(char target[SH_META_PATH_MAX + 1],
const char *val,
size_t val_len) {
if (val_len > SH_META_PATH_MAX) {
sh_print_errorf("Deployer target '%.*s' too long", (int)val_len, val);
return false;
}
if (*target != '\0') {
sh_print_errorf("Deployer target '%s' already set", target);
return false;
}
strncpy(target, val, val_len);
target[SH_META_PATH_MAX] = '\0';
return target;
}
static bool
sh_meta_parse_copy(bool *copy, bool copy_set, const char *val, size_t val_len) {
if (copy_set) {
sh_print_error("Deployer copy flag already set");
return false;
}
if ((val_len == 4 && strncmp(val, "true", 4) == 0) ||
(val_len == 3 && strncmp(val, "yes", 3) == 0) ||
(val_len == 1 && *val == '1')) {
*copy = true;
} else if ((val_len == 5 && strncmp(val, "false", 5) == 0) ||
(val_len == 2 && strncmp(val, "no", 2) == 0) ||
(val_len == 1 && *val == '0')) {
*copy = false;
} else {
sh_print_error("Invalid value for deployer flag copy (allowed: "
"true|yes|1 or false|no|0)");
return false;
}
return true;
}
static bool sh_meta_parse_key(char key[SH_META_KEY_MAX + 1],
const char *val,
size_t val_len) {
if (val_len > SH_META_KEY_MAX) {
sh_print_errorf("Deployer key '%.*s' too long", (int)val_len, val);
return false;
}
if (*key != '\0') {
sh_print_errorf("Deployer key '%s' already set", key);
return false;
}
strncpy(key, val, val_len);
key[SH_META_KEY_MAX] = '\0';
return true;
}
/* Helpers */
static uid_t sh_get_uid_by_name(const char *value, size_t length) {
if (length > SH_META_USERNAME_MAX) {
sh_print_errorf("Username '%.*s' too long", (int)length, value);
return (uid_t)-1;
}
char name[SH_META_USERNAME_MAX + 1] = {0};
strncpy(name, value, length);
const struct passwd *pwd = getpwnam(name);
if (pwd) {
if (pwd->pw_uid == 0) {
sh_print_error(
"Deployers as the root user are strictly prohibited");
return (uid_t)-1;
}
return pwd->pw_uid;
}
sh_print_errorf("No user '%s' found", name);
return (uid_t)-1;
}
static gid_t sh_get_gid_by_name(const char *value, size_t length) {
if (length > SH_META_GROUP_MAX) {
sh_print_errorf("Group '%.*s' too long", (int)length, value);
return (gid_t)-1;
}
char name[SH_META_GROUP_MAX + 1] = {0};
strncpy(name, value, length);
const struct group *grp = getgrnam(name);
if (grp) {
if (grp->gr_gid == 0) {
sh_print_error(
"Deployers as the root group are strictly prohibited");
return (gid_t)-1;
}
return grp->gr_gid;
}
sh_print_errorf("No group '%s' found", name);
return (gid_t)-1;
}
static bool sh_meta_is_delim(const char **bufp) {
const char *buf = *bufp;
if (*buf != '#') {
return false;
}
/* Skip '#' */
++buf;
/* Skip whispace after '#' */
if (!sh_str_lstrip(&buf)) {
return false;
}
/* Check for delimiter line: at least 5 dashes */
size_t dash_count = 0;
while (*buf == '-') {
++dash_count;
++buf;
}
if (dash_count >= 5) {
/* Found delimiter line, now skip rest of line */
while (*buf && *buf != '\n' && *buf != '\r') {
++buf;
}
/* Skip newline(s) (handle CR, LF, CRLF) */
if (*buf == '\r') {
++buf;
}
if (*buf == '\n') {
++buf;
}
*bufp = buf;
return true;
}
return false;
}