Template
feat(gen): add POSIX-sh emitter with quoting
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user