feat(gen): add POSIX-sh emitter with quoting

This commit is contained in:
2026-08-28 20:12:41 -04:00
parent fa5d22af4b
commit 439aa0d856
3 changed files with 860 additions and 0 deletions
+162
View File
@@ -0,0 +1,162 @@
#include "sh_emit.h"
#include "error.h"
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
/*
* src/gen/sh_emit.c — POSIX-sh text emitter (todo 15).
*
* The quoting rules implemented here are the single-quote rules of the
* POSIX Shell Command Language (XBD 2.2.2 "Single-Quotes"): everything
* between two unescaped `'` characters is literal, and the ONLY way to
* embed a `'` is to close the quoted region, escape the quote with a
* backslash, and reopen it — the four-character sequence `'\''`.
*/
static struct st_error *
quote_error(const char *message)
{
return st_error_internal(message);
}
char *
st_sh_quote_n(const char *value, size_t len, struct st_error **err)
{
size_t i;
size_t out_len;
char *out;
if (err != NULL) {
*err = NULL;
}
if (value == NULL) {
if (err != NULL) {
*err = quote_error("st_sh_quote: value is NULL");
}
return NULL;
}
for (i = 0; i < len; i++) {
if (value[i] == '\0') {
if (err != NULL) {
*err = quote_error("st_sh_quote: value contains a NUL byte");
}
return NULL;
}
}
/* Worst case: every byte is a `'` and expands to 4. Guard the
* size computation against hostile (huge) lengths. */
if (len > (SIZE_MAX - 3) / 4) {
if (err != NULL) {
*err = quote_error("st_sh_quote: value too large to quote");
}
return NULL;
}
out_len = 1; /* opening quote */
for (i = 0; i < len; i++) {
out_len += (value[i] == '\'') ? 4 : 1;
}
out_len += 1; /* closing quote */
out = malloc(out_len + 1);
if (out == NULL) {
if (err != NULL) {
*err = quote_error("st_sh_quote: out of memory");
}
return NULL;
}
out_len = 0;
out[out_len++] = '\'';
for (i = 0; i < len; i++) {
if (value[i] == '\'') {
memcpy(out + out_len, "'\\''", 4);
out_len += 4;
} else {
out[out_len++] = value[i];
}
}
out[out_len++] = '\'';
out[out_len] = '\0';
return out;
}
char *
st_sh_quote_ex(const char *value, struct st_error **err)
{
if (value == NULL) {
if (err != NULL) {
*err = quote_error("st_sh_quote: value is NULL");
}
return NULL;
}
return st_sh_quote_n(value, strlen(value), err);
}
char *
st_sh_quote(const char *value)
{
return st_sh_quote_ex(value, NULL);
}
int
st_sh_emit_str(FILE *out, const char *s)
{
if (out == NULL || s == NULL) {
return -1;
}
return fputs(s, out) == EOF ? -1 : 0;
}
int
st_sh_emit_comment(FILE *out, const char *text)
{
const unsigned char *p;
if (out == NULL || text == NULL) {
return -1;
}
if (fputs("# ", out) == EOF) {
return -1;
}
for (p = (const unsigned char *)text; *p != '\0'; p++) {
if (putc(*p, out) == EOF) {
return -1;
}
if (*p == '\n' && fputs("# ", out) == EOF) {
return -1;
}
}
return putc('\n', out) == EOF ? -1 : 0;
}
int
st_sh_emit_assign(FILE *out, const char *var, const char *quoted_value)
{
if (out == NULL || var == NULL || quoted_value == NULL) {
return -1;
}
if (fputs(var, out) == EOF || putc('=', out) == EOF ||
fputs(quoted_value, out) == EOF || putc('\n', out) == EOF) {
return -1;
}
return 0;
}
int
st_sh_emit_assign_q(FILE *out, const char *var, const char *value)
{
char *quoted;
int rc;
if (out == NULL || var == NULL) {
return -1;
}
quoted = st_sh_quote(value);
if (quoted == NULL) {
return -1;
}
rc = st_sh_emit_assign(out, var, quoted);
free(quoted);
return rc;
}
+85
View File
@@ -0,0 +1,85 @@
#ifndef ST_GEN_SH_EMIT_H
#define ST_GEN_SH_EMIT_H
/*
* POSIX-sh text emitter (todo 15).
*
* Everything this module writes is POSIX Shell Command Language only:
* the generated configure scripts must run unmodified under dash, bash
* and zsh. Policy (AGENTS.md §4): no `[[ ]]`, no arrays, no `local`,
* no `==` (use `=` inside `[ ]`), no `<<<`, no `&>`; `set -e` is POSIX
* but is AVOIDED by policy in generated scripts because its failure
* semantics vary across shells.
*
* Trust boundaries (callers MUST honor these):
* - st_sh_quote() is the ONLY function here that makes arbitrary bytes
* shell-safe. Everything written via st_sh_emit_str() and
* st_sh_emit_assign() (3rd arg) is emitted verbatim — the caller
* guarantees it is already safe shell text.
* - `var` arguments to the assign helpers must be valid POSIX shell
* identifiers (name followed by [A-Za-z0-9_]); they are not
* validated or quoted.
*/
#include <stddef.h>
#include <stdio.h>
struct st_error;
/*
* st_sh_quote(value) — the load-bearing primitive.
*
* Returns a malloc'd single-quoted shell literal that reproduces
* `value` byte-for-byte when assigned inside a shell script
* (e.g. `v=<result>`), or NULL on error.
*
* Form: the value is wrapped in `'…'` and every embedded `'` becomes
* the four-character sequence `'\''` (close quote, escaped quote, open
* quote). Nothing else is touched: inside single quotes NO character
* is special to the shell, so `$`, backticks, `"`, `\`, newlines and
* metacharacters pass through untouched — no injection is possible.
* The empty string quotes as `''`.
*
* Errors (NULL result): `value` is NULL, `value` contains a NUL byte
* (detectable only through the length-bounded variant), the required
* size overflows size_t, or allocation fails.
*/
char *st_sh_quote(const char *value);
/* As st_sh_quote(), but on error and if `err` is non-NULL, *err
* receives a newly allocated st_error (category ST_ERR_INTERNAL) that
* the caller frees with st_error_free(). On success *err is left
* untouched; if the error itself cannot be allocated, *err is NULL. */
char *st_sh_quote_ex(const char *value, struct st_error **err);
/* Length-bounded core: quotes exactly `len` bytes starting at `value`.
* This is the only variant that can DETECT an embedded NUL byte (which
* is rejected with an error rather than silently truncating). Use it
* when the input is a counted buffer, not a C string. */
char *st_sh_quote_n(const char *value, size_t len, struct st_error **err);
/*
* Emitter helpers. All return 0 on success, -1 on failure (NULL stream
* or text, or an I/O error). They do not call exit()/abort().
*/
/* Writes `s` verbatim. The caller ensures `s` is safe shell text
* (usually it is hand-written code, or output of st_sh_quote()). */
int st_sh_emit_str(FILE *out, const char *s);
/* Writes a comment: `# `, then `text` with every embedded newline
* followed by `# `, then a terminating newline. `text` must not be
* NULL. */
int st_sh_emit_comment(FILE *out, const char *text);
/* Writes `var=<quoted_value>` plus a newline. `quoted_value` must be
* ALREADY shell-safe (typically the result of st_sh_quote()) — it is
* emitted verbatim. */
int st_sh_emit_assign(FILE *out, const char *var, const char *quoted_value);
/* Convenience: quotes `value` with st_sh_quote() and assigns it in one
* step. Returns -1 if quoting fails (NULL/NUL input, OOM) or on I/O
* error; the quoted literal is freed internally. */
int st_sh_emit_assign_q(FILE *out, const char *var, const char *value);
#endif