- 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>
509 lines
15 KiB
C
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;
|
|
}
|