/* * pkg - MAKEPKG file parser * https://rawnix.org * * Copyright (c) 2025 zorz@gmx.com * SPDX-License-Identifier: BSD-2-Clause * * Mirrors the variable-parsing semantics of mkpkg's own parser * (mkpkg >= 1.5.0): * * - bodies of the known functions (shell, extract, patch, build, * post_build, signify) are SKIPPED with the same quote- and * comment-aware brace counting mkpkg uses, so assignment-looking * lines inside them can't corrupt the package metadata. Variables * are read anywhere OUTSIDE function bodies, including between * and after functions — not only above build(). * * - $var and ${var} are expanded in values, with \$ as a literal * escape. name/version/release plus any custom top-level * variables seen so far are available, matching mkpkg. * (The exotic {var[N]} indexing used in source=() URLs is not * needed here — pkg never reads source=().) */ #include "pkg.h" #include /* ---- Custom variable table (for $var expansion) ---- */ #define MAX_CUSTOM_VARS 64 typedef struct { char name[64]; char *value; } custom_var_t; typedef struct { package_t *pkg; custom_var_t vars[MAX_CUSTOM_VARS]; int var_count; } parse_state_t; static const char *lookup_var(parse_state_t *st, const char *name) { int i; if (strcmp(name, "name") == 0) return st->pkg->name; if (strcmp(name, "version") == 0) return st->pkg->version; if (strcmp(name, "release") == 0) return st->pkg->release; for (i = 0; i < st->var_count; i++) if (strcmp(st->vars[i].name, name) == 0) return st->vars[i].value; return NULL; } static void store_custom_var(parse_state_t *st, const char *name, const char *value) { int i; for (i = 0; i < st->var_count; i++) { if (strcmp(st->vars[i].name, name) == 0) { free(st->vars[i].value); st->vars[i].value = xstrdup(value); return; } } if (st->var_count >= MAX_CUSTOM_VARS) return; /* silently drop — expansion just won't resolve it */ snprintf(st->vars[st->var_count].name, sizeof(st->vars[st->var_count].name), "%s", name); st->vars[st->var_count].value = xstrdup(value); st->var_count++; } static void free_custom_vars(parse_state_t *st) { int i; for (i = 0; i < st->var_count; i++) free(st->vars[i].value); st->var_count = 0; } /* * Expand $var / ${var} in 'in', with \$ as a literal '$'. * Unknown variables expand to the empty string (shell semantics). * Returns a malloc'd string the caller owns. */ static char *expand_vars(parse_state_t *st, const char *in) { size_t cap = strlen(in) + 64; size_t len = 0; char *out = xmalloc(cap); const char *p = in; #define APPEND(c) \ do { \ if (len + 1 >= cap) { \ cap *= 2; \ out = xrealloc(out, cap); \ } \ out[len++] = (c); \ } while (0) while (*p) { if (*p == '\\' && *(p + 1) == '$') { APPEND('$'); p += 2; continue; } if (*p == '$') { char varname[64]; size_t vi = 0; int braced; const char *val; p++; braced = (*p == '{'); if (braced) p++; while (*p && vi < sizeof(varname) - 1) { if (braced && *p == '}') { p++; break; } if (!braced && !isalnum((unsigned char)*p) && *p != '_') break; varname[vi++] = *p++; } varname[vi] = '\0'; if (vi == 0) { /* lone '$' — keep it literal */ APPEND('$'); continue; } val = lookup_var(st, varname); if (val) while (*val) APPEND(*val++); continue; } APPEND(*p++); } APPEND('\0'); #undef APPEND return out; } /* ---- Function body detection (matches mkpkg's parser) ---- */ /* * The functions mkpkg's parser recognises. Only these enter a * body-skipping state; anything else on a line falls through to * variable parsing, exactly like mkpkg. */ static const char *mkpkg_funcs[] = { "shell", "extract", "patch", "build", "post_build", "signify" }; #define MKPKG_FUNC_COUNT (sizeof(mkpkg_funcs) / sizeof(mkpkg_funcs[0])) static bool is_func_header(const char *trimmed) { size_t i, n; for (i = 0; i < MKPKG_FUNC_COUNT; i++) { n = strlen(mkpkg_funcs[i]); if (strncmp(trimmed, mkpkg_funcs[i], n) != 0) continue; /* accept "build()" and "build ()" like mkpkg's * str_starts_with checks */ if (strncmp(trimmed + n, "()", 2) == 0 || strncmp(trimmed + n, " ()", 3) == 0) return true; } return false; } /* * Quote- and comment-aware brace delta of a line — the same logic * mkpkg uses so both parsers always agree on where a body ends. * `echo "}"` and sed expressions like s/${VAR}/x/ inside quotes * must not count. */ static int brace_delta(const char *trimmed) { const char *p; int in_quote = 0; char qchar = 0; int delta = 0; for (p = trimmed; *p; p++) { if (in_quote) { if (qchar == '"' && *p == '\\' && *(p + 1)) { p++; /* skip escaped char */ continue; } if (*p == qchar) in_quote = 0; continue; } if (*p == '"' || *p == '\'') { in_quote = 1; qchar = *p; continue; } if (*p == '#') /* rest of line is a comment */ break; if (*p == '{') delta++; if (*p == '}') delta--; } return delta; } /* ---- Value parsing ---- */ /* Parse bash-style array: depends=(foo bar baz) or depends=('foo' 'bar') */ static void parse_array(parse_state_t *st, const char *value, strarray_t *arr) { const char *p = value; char item[256]; int i; while (*p && *p != '(') p++; if (*p == '(') p++; while (*p && *p != ')') { while (*p && isspace((unsigned char)*p)) p++; if (*p == ')' || !*p) break; if (*p == '\'' || *p == '"') { char quote = *p++; i = 0; while (*p && *p != quote && i < (int)sizeof(item) - 1) item[i++] = *p++; item[i] = '\0'; if (*p == quote) p++; } else { i = 0; while (*p && !isspace((unsigned char)*p) && *p != ')' && i < (int)sizeof(item) - 1) item[i++] = *p++; item[i] = '\0'; } if (item[0]) { char *expanded = expand_vars(st, item); if (expanded[0]) strarray_add(arr, expanded); free(expanded); } } } /* * Return true if the line begins (after optional leading whitespace) * with `var=` or `var =`. * * This is an anchored prefix match — it does NOT find `var=` as a * substring elsewhere on the line. Without anchoring, `makedepends=` * would accidentally match the `depends=` field. */ static bool match_array_assignment(const char *line, const char *var) { size_t varlen = strlen(var); const char *p = line; while (*p == ' ' || *p == '\t') p++; if (strncmp(p, var, varlen) != 0) return false; p += varlen; while (*p == ' ' || *p == '\t') p++; return *p == '='; } /* Extract value from: variable="value" or variable=value (no expansion) */ static char *extract_value(const char *line, const char *var) { const char *p; char *result; size_t varlen = strlen(var); const char *start, *end; p = line; while (*p && isspace((unsigned char)*p)) p++; if (strncmp(p, var, varlen) != 0) return NULL; p += varlen; while (*p && isspace((unsigned char)*p)) p++; if (*p != '=') return NULL; p++; while (*p && isspace((unsigned char)*p)) p++; if (*p == '"' || *p == '\'') { char quote = *p++; start = p; while (*p && *p != quote) p++; end = p; } else { start = p; while (*p && !isspace((unsigned char)*p) && *p != '#') p++; end = p; } if (end <= start) return NULL; result = xmalloc(end - start + 1); memcpy(result, start, end - start); result[end - start] = '\0'; return result; } /* * Try to read the line as ANY scalar assignment `ident=value` and * return the identifier in name_buf and the (unexpanded) value. * Used to capture custom variables for later $expansion. * Returns NULL when the line is not a scalar assignment. */ static char *extract_any_assignment(const char *line, char *name_buf, size_t name_buf_len) { const char *p = line; size_t ni = 0; while (*p == ' ' || *p == '\t') p++; if (!isalpha((unsigned char)*p) && *p != '_') return NULL; while ((isalnum((unsigned char)*p) || *p == '_') && ni < name_buf_len - 1) name_buf[ni++] = *p++; name_buf[ni] = '\0'; while (*p == ' ' || *p == '\t') p++; if (*p != '=') return NULL; return extract_value(line, name_buf); } /* * Read a complete array value, handling multi-line arrays. * Accumulates lines into buf until ')' is found. */ static bool read_full_array(FILE *fp, const char *first_line, char *buf, size_t bufsz) { size_t len; char cont[MAX_LINE]; len = strlen(first_line); if (len >= bufsz) len = bufsz - 1; memcpy(buf, first_line, len); buf[len] = '\0'; if (strchr(buf, ')')) return true; while (fgets(cont, sizeof(cont), fp)) { cont[strcspn(cont, "\n")] = '\0'; if (len + 1 + strlen(cont) < bufsz) { size_t clen = strlen(cont); buf[len++] = ' '; memcpy(buf + len, cont, clen + 1); len += clen; } if (strchr(cont, ')')) return true; } return true; /* EOF, parse what we have */ } /* Replace a scalar pkg field with the expanded value, freeing both */ static void set_field(parse_state_t *st, char **field, char *raw) { char *expanded = expand_vars(st, raw); free(raw); free(*field); *field = expanded; } /* Re-parse an array field: clear, then fill (last assignment wins) */ static void set_array(parse_state_t *st, FILE *fp, const char *line, char *buf, size_t bufsz, strarray_t *arr) { read_full_array(fp, line, buf, bufsz); strarray_free(arr); strarray_init(arr); parse_array(st, buf, arr); } int makepkg_parse(const char *path, package_t *pkg) { FILE *fp; char line[MAX_LINE]; char buf[MAX_LINE * 4]; char *trimmed, *val; char varname[64]; parse_state_t st; bool in_func = false; int brace_count = 0; fp = fopen(path, "r"); if (!fp) return -1; st.pkg = pkg; st.var_count = 0; while (fgets(line, sizeof(line), fp)) { line[strcspn(line, "\n")] = '\0'; trimmed = str_trim(line); if (!trimmed || trimmed[0] == '\0' || trimmed[0] == '#') continue; /* ---- inside a function body: skip with brace counting ---- */ if (in_func) { brace_count += brace_delta(trimmed); if (strcmp(trimmed, "{") == 0) continue; if (brace_count < 0 || strcmp(trimmed, "}") == 0) { in_func = false; brace_count = 0; } continue; } /* ---- function header: enter skip state ---- */ if (is_func_header(trimmed)) { in_func = true; brace_count = 0; continue; } /* ---- top-level variables ---- */ if ((val = extract_value(trimmed, "name")) != NULL) { set_field(&st, &pkg->name, val); } else if ((val = extract_value(trimmed, "version")) != NULL) { set_field(&st, &pkg->version, val); } else if ((val = extract_value(trimmed, "release")) != NULL) { set_field(&st, &pkg->release, val); } else if ((val = extract_value(trimmed, "description")) != NULL) { set_field(&st, &pkg->description, val); } else if ((val = extract_value(trimmed, "url")) != NULL) { set_field(&st, &pkg->url, val); } else if ((val = extract_value(trimmed, "maintainer")) != NULL) { set_field(&st, &pkg->maintainer, val); } else if (match_array_assignment(trimmed, "makedeps")) { set_array(&st, fp, trimmed, buf, sizeof(buf), pkg->makedeps); } else if (match_array_assignment(trimmed, "depends")) { set_array(&st, fp, trimmed, buf, sizeof(buf), pkg->depends); } else if (match_array_assignment(trimmed, "provides")) { set_array(&st, fp, trimmed, buf, sizeof(buf), pkg->provides); } else if (match_array_assignment(trimmed, "variant")) { set_array(&st, fp, trimmed, buf, sizeof(buf), pkg->variant); } else if ((val = extract_any_assignment(trimmed, varname, sizeof(varname))) != NULL) { /* custom variable — keep for $expansion */ char *expanded = expand_vars(&st, val); store_custom_var(&st, varname, expanded); free(expanded); free(val); } } fclose(fp); free_custom_vars(&st); return 0; }