deployd/script/stages.c
Arija A. f298495d59
Add extension-path helper, overhaul metadata parsing and integrate stages API
- dirs.c / dirs.h
  - add sh_dirs_get_ext_path() to build extension paths
  - define SH_DIRS_MAX_EXT_PATH for buffer sizing
- metadata.h / metadata.c
  - revamp sh_Metadata_read() to return bool, drop error‑string outparam
  - add SH_METADATA_NEW() and SH_METADATA_FREE()
  - split parsing into helpers (name, extensions, user, groups, target, copy)
  - tighten validation, trimming, uniqueness checks, and error messages
  - rename sh_Metadata_is_full() to sh_Metadata_is_full_script()
- stages.h / stages.c
  - extend sh_find_stages() to take sh_PtrRange* for sysadmin block range
  - refine brace/paren balance, in‑function tracking, EOF error checks
  - improve backtick‑in‑subsidiary parsing error handling
- main.c
  - add sh_load_script() to fread entire script into memory with error checks
  - add sh_read_meta() wrapper to call metadata and stages APIs, free buffer, return proper exit codes
  - include deploy.h and progress printf() calls
- server/main.c
  - insert “TODO: Cron jobs” placeholder

Signed-off-by: Arija A. <ari@ari.lt>
2025-07-27 13:08:39 +03:00

509 lines
15 KiB
C

#include "include/conf.h"
#include "include/def.h"
#include "include/stages.h"
#include <stdbool.h>
#include <stdint.h>
#include <ctype.h>
static bool sh_parse_subshell(const char **bufp, const char *start);
static bool sh_parse_param_expression(const char **bufp, const char *start);
static bool sh_is_name_or_func(const char **bufp,
const char *start,
uint8_t *stagesp,
bool *in_func);
static void sh_skip_comment(const char **bufp);
static bool sh_parse_double(const char **bufp, const char *start);
static bool sh_parse_dollar(const char **bufp, const char *start);
uint8_t sh_find_stages(const char *buf, sh_PtrRange *sysadm) {
if (!buf || !*buf) {
return SH_STAGE_ERROR;
}
const char *start = buf;
int code_level = 0;
int sub_level = 0;
/* { ... } is just a block. name() { ... } is a function. */
bool in_func = false;
uint8_t stages = 0;
sysadm->start = NULL;
sysadm->end = NULL;
while (*buf) {
const char chr = *buf;
++buf;
switch (chr) {
case '#': sh_skip_comment(&buf); break;
case '{':
if (!in_func) {
sh_print_errorf("Disallowed '{' at position %ld - not in "
"function context",
buf - start);
return SH_STAGE_ERROR;
}
++code_level;
break;
case '}':
if (code_level < 1) {
sh_print_errorf("Unbalanced '}' at position %ld",
buf - start);
return SH_STAGE_ERROR;
}
if (code_level == 1) {
if ((stages & SH_STAGE_SYSADMIN) && !sysadm->end &&
sysadm->start && *sysadm->start &&
sysadm->start < buf - 1 && *(buf - 1)) {
sysadm->end = buf - 1;
}
}
in_func = false;
--code_level;
break;
case '(':
if (code_level < 1) {
sh_print_errorf("Disallowed '(' at position %ld",
buf - start);
return SH_STAGE_ERROR;
}
++sub_level;
break;
case ')':
if (sub_level < 1 || code_level < 1) {
sh_print_errorf("Unbalanced/disallowed '(' at position %ld",
buf - start);
return SH_STAGE_ERROR;
}
--sub_level;
break;
case '\'':
if (code_level < 1) {
sh_print_errorf(
"Disallowed ' (single quote) at position %ld",
buf - start);
return SH_STAGE_ERROR;
}
while (*buf && *buf != '\'') {
++buf;
}
if (*buf != '\'') {
sh_print_errorf("Unclosed ' (single quote) at position %ld",
buf - start);
return SH_STAGE_ERROR;
}
++buf;
break;
case '"':
if (code_level < 1) {
sh_print_errorf(
"Disallowed \" (double quote) at position %ld",
buf - start);
return SH_STAGE_ERROR;
}
if (!sh_parse_double(&buf, start)) {
return SH_STAGE_ERROR;
}
break;
case '`':
/* Backtick command substitution outside quotes: This is an
* unsafe feature */
sh_print_errorf("Unsafe backtick at position %ld", buf - start);
return SH_STAGE_ERROR;
case '$':
if (code_level < 1) {
sh_print_errorf("Disallowed $ at position %ld",
buf - start);
return SH_STAGE_ERROR;
}
if (!sh_parse_dollar(&buf, start)) {
return SH_STAGE_ERROR;
}
break;
case ' ':
case '\t':
case '\r':
case '\n':
/* Whitespace allowed anywhere */
break;
default:
/* For any other chars, must be inside a brace or parens */
if (code_level < 1) {
--buf;
if (!sh_is_name_or_func(&buf, start, &stages, &in_func)) {
sh_print_errorf(
"Code outside of a block in position %ld",
buf - start);
return SH_STAGE_ERROR;
}
if ((stages & SH_STAGE_SYSADMIN) && !sysadm->start &&
*(buf + 1)) {
sysadm->start = buf + 1;
}
}
break;
}
}
if ((stages & SH_STAGE_PREPARE) == 0) {
sh_print_error("Deployer is missing a required prepare() stage");
return SH_STAGE_ERROR;
}
if (code_level != 0) {
sh_print_error("Unmatched {...} block in top level");
return SH_STAGE_ERROR;
}
if (sub_level != 0) {
sh_print_error("Unmatched (...) expression in top level");
return SH_STAGE_ERROR;
}
if (in_func) {
sh_print_error("Expected function body, got EOF");
return SH_STAGE_ERROR;
}
return stages;
}
static bool sh_parse_subshell(const char **bufp, const char *start) {
int paren_level = 1; /* Already consumed first '(' */
const char *buf = *bufp;
while (*buf) {
const char chr = *buf;
++buf;
if (chr == '\'') {
/* Inside single quotes: skip till next single quote */
while (*buf && *buf != '\'') {
++buf;
}
if (*buf != '\'') {
sh_print_errorf(
"Unclosed single quote at position %ld (subshell)",
buf - start);
return false;
}
++buf;
} else if (chr == '"') {
/* Inside double quotes: parse similar to main code */
if (!sh_parse_double(&buf, start)) {
return false;
}
} else if (chr == '`') {
/* Disallow backticks */
sh_print_errorf("Unsafe backtick at position %ld (subshell)",
buf - start);
return false;
} else if (chr == '(') {
++paren_level;
} else if (chr == ')') {
--paren_level;
if (paren_level == 0) {
*bufp = buf;
return true;
}
} else if (chr == '$') {
if (!sh_parse_dollar(&buf, start)) {
return false;
}
}
/* Other characters ok inside subshell */
}
sh_print_errorf("Unbalanced parentheses at position %ld (subshell)",
buf - start);
return false; /* unbalanced parentheses */
}
static bool sh_parse_param_expression(const char **bufp, const char *start) {
const char *buf = *bufp;
if (*buf == '\0') {
sh_print_errorf("Empty parameter substitution at position %ld "
"(parameter expression)",
buf - start);
return false;
}
/* Check variable name */
if (isalpha((unsigned char)*buf) || *buf == '_') {
while (isalnum((unsigned char)*buf) || *buf == '_') {
++buf;
}
} else {
sh_print_errorf("Invalid parameter start '%c' at position %ld "
"(parameter expression)",
*buf, buf - start);
return false;
}
/* Optional operator: :- := :? :+ or - = ? + */
if (*buf == ':' || *buf == '-' || *buf == '=' || *buf == '?' ||
*buf == '+') {
if (*buf == ':') {
++buf;
if (*buf != '-' && *buf != '=' && *buf != '?' && *buf != '+') {
sh_print_errorf("Invalid expression modifier at position %ld "
"(parameter expression)",
buf - start);
return false;
}
}
++buf;
/* Scan value until matching closing brace */
int nested = 0;
while (*buf && (*buf != '}' || nested > 0)) {
if (*buf == '$' && *(buf + 1) == '{') {
buf += 2;
if (!sh_parse_param_expression(&buf, start)) {
return false;
}
} else if (*buf == '{') {
++nested;
++buf;
} else if (*buf == '}') {
if (nested > 0) {
--nested;
}
++buf;
} else if (*buf == '"' || *buf == '\'') {
const char quote = *buf++;
while (*buf && *buf != quote) {
++buf;
}
if (*buf != quote) {
sh_print_errorf(
"Unclosed %c at position %ld (parameter expression)",
quote, buf - start);
return false;
}
++buf;
} else {
++buf;
}
}
}
if (*buf != '}') {
sh_print_errorf("Unclosed parameter expansion at position %ld "
"(parameter expression)",
buf - start);
return false;
}
++buf;
*bufp = buf;
return true;
}
struct sh_StageFlag {
const char *name;
const uint8_t flag;
};
static bool sh_is_name_or_func(const char **bufp,
const char *start,
uint8_t *stagesp,
bool *in_func) {
const char *buf = *bufp;
const char *buf_start = buf;
if (!isalpha((unsigned char)*buf) && *buf != '_') {
return false;
}
++buf;
while (isalnum((unsigned char)*buf) || *buf == '_') {
++buf;
}
if (*buf == '=') {
++buf;
while (*buf && *buf != ' ' && *buf != '\t' && *buf != '\r' &&
*buf != '\n') {
++buf;
}
*in_func = false;
*bufp = buf;
return true;
}
if (*buf == '(' && *(buf + 1) == ')') {
const size_t name_len = (size_t)(buf - buf_start);
if (*in_func) {
sh_print_errorf("Already in function in name '%.*s', re-function "
"position at %ld (global)",
(int)(name_len), buf_start, buf_start - start);
return false;
}
if (*buf_start != '_') {
/* Only internal functions */
static const struct sh_StageFlag valid_func_names[7] = {
{"prepare", SH_STAGE_PREPARE}, {"build", SH_STAGE_BUILD},
{"test", SH_STAGE_TEST}, {"cleanup", SH_STAGE_CLEANUP},
{"teardown", SH_STAGE_TEARDOWN}, {"deploy", SH_STAGE_DEPLOY},
{"sysadmin", SH_STAGE_SYSADMIN},
};
bool is_ok = false;
for (size_t idx = 0;
idx < sizeof(valid_func_names) / sizeof(valid_func_names[0]);
++idx) {
const size_t valid_name_len =
strlen(valid_func_names[idx].name);
if (name_len == valid_name_len &&
strncmp(buf_start, valid_func_names[idx].name,
valid_name_len) == 0) {
if ((*stagesp & valid_func_names[idx].flag) != 0) {
sh_print_errorf("Function '%.*s' already defined, "
"redefinition at position %ld (global)",
(int)(name_len), buf_start,
buf_start - start);
return false;
}
*stagesp |= valid_func_names[idx].flag;
is_ok = true;
break;
}
}
if (!is_ok) {
sh_print_errorf("Invalid internal function name '%.*s' at "
"position %ld. Did you mean _%.*s()? (global)",
(int)(name_len), buf_start, buf_start - start,
(int)(name_len), buf_start);
return false;
}
}
/* Private functions can be whatever */
buf += 2;
while (*buf == ' ' || *buf == '\t' || *buf == '\r' || *buf == '\n') {
++buf;
}
*in_func = true;
*bufp = buf;
return true;
}
return false;
}
static void sh_skip_comment(const char **bufp) {
const char *buf = *bufp;
while (*buf && *buf != '\r' && *buf != '\n') {
++buf;
}
if (*buf == '\n') {
++buf;
}
if (*buf == '\r') {
++buf;
}
*bufp = buf;
}
static bool sh_parse_double(const char **bufp, const char *start) {
const char *buf = *bufp;
/* Double quote string: process contents with substitutions
* allowed */
while (*buf && *buf != '"') {
const char dchr = *buf;
if (dchr == '\\') {
/* Skip escaped char */
++buf;
if (*buf) {
++buf;
}
} else if (dchr == '$') {
++buf;
if (!sh_parse_dollar(&buf, start)) {
return false;
}
} else if (dchr == '`') {
/* Backtick substitution inside double quotes: this is
* an unsafe feature */
sh_print_errorf("Unsafe backtick at position %ld", buf - start);
return false;
} else {
++buf;
}
}
if (*buf != '"') {
sh_print_errorf("Unclosed \" (double quote) at position %ld",
buf - start);
return false;
}
++buf;
*bufp = buf;
return true;
}
static bool sh_parse_dollar(const char **bufp, const char *start) {
const char *buf = *bufp;
/* Possible expansions outside quotes */
if (*buf == '(') {
++buf;
if (!sh_parse_subshell(&buf, start)) {
return false;
}
} else if (*buf == '{') {
++buf;
if (!sh_parse_param_expression(&buf, start)) {
return false;
}
} else if (*buf == '`') {
/* Unsafe backtick */
sh_print_errorf("Unsafe backtick at position %ld (in $)", buf - start);
return false;
} else if (isalpha((unsigned char)*buf) || *buf == '_') {
/* Simple variable substitution */
while (isalnum((unsigned char)*buf) || *buf == '_') {
++buf;
}
}
/* else { */
/* $ not followed by expansion - ok */
/* } */
*bufp = buf;
return true;
}