Files
s390-tools/libutil/util_fmt.c
Niklas Schnelle 8a4a4e0557 libutil/util_fmt: Add util_fmt_type_to_name()
Add a function to get the textual name of an enum util_fmt_t value. To
make this robust to changes in the order of elements in the format array
initialize this using named indices.

Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
Signed-off-by: Niklas Schnelle <schnelle@linux.ibm.com>
Signed-off-by: Jan Höppner <hoeppner@linux.ibm.com>
2026-07-24 18:21:50 +02:00

821 lines
17 KiB
C

/*
* util - Utility function library
*
* Format structured data as key-value pairs, JSON, or CSV
*
* Copyright IBM Corp. 2024
*
* s390-tools is free software; you can redistribute it and/or modify
* it under the terms of the MIT license. See LICENSE for details.
*/
#include <ctype.h>
#include <err.h>
#include <errno.h>
#include <limits.h>
#include <locale.h>
#include <signal.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <time.h>
#include <unistd.h>
#include "lib/util_base.h"
#include "lib/util_fmt.h"
#include "lib/util_libc.h"
#include "lib/util_rec.h"
#include "lib/zt_common.h"
struct obj_t {
char *name;
bool is_list;
bool is_row;
bool is_prefix;
unsigned int index;
};
struct key_t {
char *name;
bool persist;
};
static struct {
enum util_fmt_t type;
FILE *fd;
int fileno;
/* Format control. */
bool hide_prefix;
bool hide_inval;
bool quote_all;
bool do_filter;
bool do_warn;
bool hide_meta;
bool handle_int;
int api_level;
const char *nl;
/* JSON specifics. */
unsigned int ind_base;
unsigned int ind_width;
char ind_char;
bool meta_done;
/* CSV specifics. */
struct util_rec *csv_rec;
bool csv_hdr;
bool csv_data;
/* State. */
unsigned int lvl;
struct obj_t *objs;
unsigned int num_objs;
struct key_t *keys;
unsigned int num_keys;
struct sigaction old_int;
struct sigaction old_term;
/* Methods. */
void (*obj_start)(struct obj_t *parent, struct obj_t *obj);
void (*obj_end)(struct obj_t *parent, struct obj_t *obj);
void (*map)(struct obj_t *parent, unsigned int mflags, const char *key,
const char *val);
void (*term)(void);
} f;
#define fwarn(fmt, ...) \
do { if (f.do_warn) warnx(fmt, ##__VA_ARGS__); } while (0)
/* Map format name to format ID. */
static const struct {
const char *name;
enum util_fmt_t fmt;
} formats[] = {
[FMT_JSON] = {
.name = "json",
.fmt = FMT_JSON
},
[FMT_JSONSEQ] = {
.name = "json-seq",
.fmt = FMT_JSONSEQ
},
[FMT_JSONL] = {
.name = "jsonl",
.fmt = FMT_JSONL
},
[FMT_PAIRS] = {
.name = "pairs",
.fmt = FMT_PAIRS
},
[FMT_CSV] = {
.name = "csv",
.fmt = FMT_CSV
},
};
/* Signal mask for blocking INT and TERM signals. */
static sigset_t no_int_mask;
bool util_fmt_name_to_type(const char *name, enum util_fmt_t *type)
{
unsigned int i;
for (i = 0; i < ARRAY_SIZE(formats); i++) {
if (strcasecmp(name, formats[i].name) == 0) {
*type = formats[i].fmt;
return true;
}
}
return false;
}
const char *util_fmt_type_to_name(enum util_fmt_t type)
{
return formats[type].name;
}
bool util_fmt_is_json(enum util_fmt_t type)
{
switch (type) {
case FMT_JSON:
case FMT_JSONSEQ:
case FMT_JSONL:
return true;
default:
return false;
}
}
bool util_fmt_is_json_stream(enum util_fmt_t type)
{
switch (type) {
case FMT_JSONSEQ:
case FMT_JSONL:
return true;
default:
return false;
}
}
static void safe_write(const char *str)
{
size_t done, todo;
ssize_t rc;
if (f.fileno < 0)
return;
for (done = 0; (todo = strlen(&str[done])) > 0; done += (size_t)rc) {
rc = write(f.fileno, &str[done], todo);
if (rc <= 0)
return;
}
}
static void _indent(unsigned int off, bool safe)
{
unsigned int num, i;
if (util_fmt_is_json_stream(f.type))
return;
num = f.ind_base + off;
if (f.type == FMT_JSON && f.lvl > 0)
num += f.lvl - 1;
for (i = 0; i < num * f.ind_width; i++) {
if (!safe) {
fputc(f.ind_char, f.fd);
} else if (f.fileno >= 0) {
if (write(f.fileno, &f.ind_char, 1) <= 0)
return;
}
}
}
#define indent(x) _indent(x, false)
static void obj_free(struct obj_t *obj)
{
free(obj->name);
memset(obj, 0, sizeof(*obj));
}
static void disable_int(sigset_t *saved)
{
if (f.handle_int)
sigprocmask(SIG_BLOCK, &no_int_mask, saved);
}
static void enable_int(sigset_t *saved)
{
if (f.handle_int) {
/* Ensure latest updates are flushed to file descriptor. */
fflush(f.fd);
sigprocmask(SIG_SETMASK, saved, NULL);
}
}
static void int_handler(int signum)
{
struct sigaction *old;
if (f.term)
f.term();
/* Re-install and call original handler. */
old = (signum == SIGINT) ? &f.old_int : &f.old_term;
sigaction(signum, old, NULL);
raise(signum);
}
static void setup_int_handler(void)
{
struct sigaction act;
memset(&act, 0, sizeof(act));
act.sa_handler = &int_handler;
sigaction(SIGINT, &act, &f.old_int);
sigaction(SIGTERM, &act, &f.old_term);
}
static void remove_int_handler(void)
{
sigaction(SIGINT, &f.old_int, NULL);
sigaction(SIGTERM, &f.old_term, NULL);
}
void util_fmt_exit(void)
{
unsigned int i;
if (f.handle_int)
remove_int_handler();
if (f.lvl > 0)
fwarn("%s before remaining %d util_obj_end()", __func__, f.lvl);
for (i = 0; i < f.num_keys; i++)
free(f.keys[i].name);
free(f.keys);
for (i = 0; i < f.num_objs; i++)
obj_free(&f.objs[i]);
free(f.objs);
if (f.type == FMT_CSV)
util_rec_free(f.csv_rec);
}
void util_fmt_set_indent(unsigned int base, unsigned int width, char ind_char)
{
f.ind_base = base;
f.ind_width = width;
f.ind_char = ind_char;
}
static unsigned int to_hex(char *str, int val, unsigned int num_digits)
{
int digit;
char *c;
for (c = str + num_digits - 1; c >= str; c--) {
digit = (val & 0xf);
val >>= 4;
*c = (char)((digit >= 10) ? digit - 10 + 'a' : digit + '0');
}
return num_digits;
}
static char get_escape(const char *map, char c)
{
int i;
for (i = 0; map[i] && map[i + 1]; i += 2) {
if (map[i] == c)
return map[i + 1];
}
return 0;
}
struct quote_params {
const char *double_chars;
const char *esc_map;
char hex_char;
unsigned int hex_digits;
unsigned int max_width_per_char;
};
static char *do_quote(const char *str, const struct quote_params *p)
{
unsigned int from, to;
char *q, esc, c;
/* Start with worst-case length assuming every char is replaced. */
q = util_zalloc(strlen(str) * p->max_width_per_char + /* "" nul */ 3);
to = 0;
q[to++] = '"';
for (from = 0; (c = str[from]); from++) {
if (p->double_chars && strchr(p->double_chars, c)) {
/* Escape characters by doubling them ("" in CSV). */
q[to++] = c;
q[to++] = c;
} else if (p->esc_map && (esc = get_escape(p->esc_map, c))) {
/* Escape characters with backslash + letter. */
q[to++] = '\\';
q[to++] = esc;
} else if (p->hex_char && !isprint(c)) {
/* Escape characters with backslash + hex code. */
q[to++] = '\\';
q[to++] = p->hex_char;
to += to_hex(&q[to], c, p->hex_digits);
} else {
q[to++] = c;
}
}
q[to++] = '"';
return util_realloc(q, (size_t)to + 1);
}
static char *csv_quote(const char *str)
{
static const struct quote_params csv_quote_params = {
.double_chars = "\"",
.esc_map = NULL,
.hex_char = 0,
.hex_digits = 0,
.max_width_per_char = 2 /* " => "" */,
};
return do_quote(str, &csv_quote_params);
}
static void add_key(const char *name, bool persist)
{
struct key_t key;
char *hdr;
key.name = util_strdup(name);
key.persist = persist;
util_add_array(&f.keys, &f.num_keys, key);
if (f.type == FMT_CSV) {
if (f.quote_all) {
hdr = csv_quote(name);
util_rec_def(f.csv_rec, name, UTIL_REC_ALIGN_LEFT, 0, hdr);
free(hdr);
} else {
util_rec_def(f.csv_rec, name, UTIL_REC_ALIGN_LEFT, 0, name);
}
util_rec_set(f.csv_rec, name, "\"\"");
f.csv_hdr = true;
}
}
static struct key_t *get_key(const char *name)
{
unsigned int i;
for (i = 0; i < f.num_keys; i++) {
if (strcmp(name, f.keys[i].name) == 0)
return &f.keys[i];
}
return NULL;
}
void util_fmt_add_key(const char *fmt, ...)
{
va_list args;
char *key;
va_start(args, fmt);
util_vasprintf(&key, fmt, args);
va_end(args);
/* Only add unique keys. */
if (!get_key(key))
add_key(key, true);
free(key);
}
static bool update_key(const char *name, bool persist)
{
struct key_t *key;
bool rc = true;
key = get_key(name);
if (key) {
key->persist = persist;
} else if (!f.do_filter) {
add_key(name, persist);
} else {
fwarn("util_fmt_pair for key '%s' without util_fmt_add_key()",
name);
rc = false;
}
return rc;
}
static struct obj_t *curr_obj(int off)
{
int lvl = (int)f.lvl - 1 + off;
return lvl < 0 ? NULL : &f.objs[lvl];
}
static void _util_fmt_obj_end(void);
/*
* By s390-tools convention, all tool output must be contained in an extra
* top-level object that includes tool-invocation meta-data.
*/
static void emit_meta_object(void)
{
unsigned int quoted = FMT_PERSIST | FMT_QUOTE, unquoted = FMT_PERSIST;
char hostname[HOST_NAME_MAX + 1] = { 0 }, date[30];
struct timeval tv;
struct tm *tm;
f.meta_done = true;
util_fmt_obj_start(FMT_DEFAULT, NULL);
util_fmt_obj_start(FMT_PREFIX, "meta");
/*
* "meta": {
* "api_level": 1,
* "version": "2.32.0",
* "host": "localhost",
* "time_epoch": 1714392976,
* "time": "2024-04-29 14:16:16+0200",
* }
*/
util_fmt_pair(unquoted, "api_level", "%d", f.api_level);
util_fmt_pair(quoted, "version", "%s", RELEASE_STRING);
gethostname(hostname, sizeof(hostname) - 1);
util_fmt_pair(quoted, "host", "%s", hostname);
gettimeofday(&tv, NULL);
util_fmt_pair(unquoted, "time_epoch", "%llu", tv.tv_sec);
tm = localtime(&tv.tv_sec);
if (!strftime(date, sizeof(date), "%F %T%z", tm))
date[0] = 0;
util_fmt_pair(quoted, "time", "%s", date);
_util_fmt_obj_end();
if (util_fmt_is_json_stream(f.type)) {
/* Tool meta-data is a separate object for JSON streams. */
util_fmt_obj_end();
}
}
void util_fmt_obj_start(unsigned int oflags, const char *fmt, ...)
{
struct obj_t *parent, *obj;
char *name = NULL;
sigset_t set;
va_list args;
if (!f.hide_meta && !f.meta_done && f.lvl == 0) {
emit_meta_object();
/*
* Allow override of top-level key name for supplementary
* output formats.
*/
if (!fmt)
name = util_strdup(program_invocation_short_name);
}
if (fmt) {
va_start(args, fmt);
util_vasprintf(&name, fmt, args);
va_end(args);
}
f.lvl++;
if (f.lvl > f.num_objs)
util_expand_array(&f.objs, &f.num_objs);
parent = curr_obj(-1);
obj = curr_obj(0);
obj->name = name;
obj->is_list = (oflags & FMT_LIST);
obj->is_row = (oflags & FMT_ROW);
obj->is_prefix = (oflags & FMT_PREFIX);
obj->index = 0;
if (f.obj_start) {
disable_int(&set);
f.obj_start(parent, obj);
enable_int(&set);
}
if (parent)
parent->index++;
}
static void _util_fmt_obj_end(void)
{
struct obj_t *obj, *parent;
sigset_t set;
if (f.lvl == 0) {
fwarn("%s without util_fmt_obj_start", __func__);
return;
}
parent = curr_obj(-1);
obj = curr_obj(0);
if (f.obj_end) {
disable_int(&set);
f.obj_end(parent, obj);
enable_int(&set);
}
f.lvl--;
obj_free(obj);
}
void util_fmt_obj_end(void)
{
_util_fmt_obj_end();
if (f.lvl == 1 && f.meta_done && !util_fmt_is_json_stream(f.type)) {
/* Emit closure for top-level meta-container object. */
util_fmt_obj_end();
}
}
static char *add_prefix(const char *str, bool full)
{
struct obj_t *obj;
unsigned int i;
char *prefix;
prefix = util_strdup("");
for (i = 0; i < f.lvl; i++) {
obj = &f.objs[i];
if (!full && !obj->is_prefix)
continue;
if (obj->name) {
if (*prefix)
util_concatf(&prefix, ".");
util_concatf(&prefix, "%s", obj->name);
}
if (obj->is_list && full)
util_concatf(&prefix, "[%d]", obj->index - 1);
}
if (*prefix)
util_concatf(&prefix, ".");
util_concatf(&prefix, "%s", str);
return prefix;
}
void util_fmt_pair(unsigned int mflags, const char *key, const char *fmt, ...)
{
char *val, *prefixed_key;
struct obj_t *obj;
bool is_filtered;
sigset_t set;
va_list args;
obj = curr_obj(0);
if (!obj) {
fwarn("%s before util_fmt_obj_start", __func__);
return;
}
/* Filter by key. */
if (f.do_filter) {
prefixed_key = add_prefix(key, false);
is_filtered = !get_key(prefixed_key);
free(prefixed_key);
if (is_filtered)
return;
}
/* Filter by validity. */
if (f.hide_inval && (mflags & FMT_INVAL))
return;
va_start(args, fmt);
util_vasprintf(&val, fmt, args);
va_end(args);
if (f.map) {
disable_int(&set);
f.map(obj, mflags, key, val);
enable_int(&set);
}
obj->index++;
free(val);
}
static char *pairs_quote(const char *str)
{
static const struct quote_params pairs_quote_params = {
.double_chars = NULL,
.esc_map = "\"\"$$``\\\\\aa\bb\ee\ff\nn\rr\tt\vv",
.hex_char = 'x',
.hex_digits = 2,
.max_width_per_char = 4 /* '\x' + 2 hex_digits */,
};
return do_quote(str, &pairs_quote_params);
}
static void pairs_map(struct obj_t *UNUSED(obj), unsigned int mflags,
const char *key, const char *val)
{
char *full_key, *qval = NULL;
if (mflags & FMT_INVAL)
val = "";
indent(0);
if (f.quote_all || (mflags & FMT_QUOTE))
qval = pairs_quote(val);
if (f.hide_prefix) {
fprintf(f.fd, "%s=%s\n", key, qval ?: val);
} else {
full_key = add_prefix(key, true);
fprintf(f.fd, "%s=%s\n", full_key, qval ?: val);
free(full_key);
}
free(qval);
}
static char *json_quote(const char *str)
{
static const struct quote_params json_quote_params = {
.double_chars = NULL,
.esc_map = "\"\"\\\\\bb\ff\nn\rr\tt",
.hex_char = 'u',
.hex_digits = 4,
.max_width_per_char = 6 /* '\u' + 4 hex_digits */,
};
return do_quote(str, &json_quote_params);
}
static void json_obj_start(struct obj_t *parent, struct obj_t *obj)
{
char *key;
if (!parent && f.type == FMT_JSONSEQ) {
/* Emit leading record separator according to RFC 7464. */
fprintf(f.fd, "\x1e");
}
if (parent && parent->index > 0)
fprintf(f.fd, ",%s", f.nl);
indent(0);
if (parent && !parent->is_list && obj->name) {
key = json_quote(obj->name);
fprintf(f.fd, "%s: ", key);
free(key);
}
fprintf(f.fd, obj->is_list ? "[%s" : "{%s", f.nl);
}
static void json_obj_end(struct obj_t *parent, struct obj_t *obj)
{
if (obj->index > 0)
fprintf(f.fd, "%s", f.nl);
indent(0);
fprintf(f.fd, obj->is_list ? "]" : "}");
if (!parent)
fprintf(f.fd, "\n");
}
/*
* Ensure syntactically correct JSON by emitting all pending closure elements.
* Called in signal context - only use signal-safe functions.
*/
static void json_term(void)
{
struct obj_t *obj, *parent;
for (; f.lvl > 0; f.lvl--) {
obj = curr_obj(0);
parent = curr_obj(-1);
if (obj->index > 0)
safe_write(f.nl);
_indent(0, true);
safe_write(obj->is_list ? "]" : "}");
if (!parent)
safe_write(f.nl);
}
}
static void json_map(struct obj_t *parent, unsigned int mflags,
const char *key, const char *val)
{
char *qkey, *qval = NULL;
qkey = json_quote(key);
if (mflags & FMT_INVAL)
qval = util_strdup("null");
else if (f.quote_all || (mflags & FMT_QUOTE))
qval = json_quote(val);
if (parent->index > 0)
fprintf(f.fd, ",%s", f.nl);
indent(1);
fprintf(f.fd, "%s: %s", qkey, qval ?: val);
free(qval);
free(qkey);
}
static void csv_obj_start(struct obj_t *UNUSED(parent), struct obj_t *obj)
{
if (!obj->is_row)
return;
}
static void csv_obj_end(struct obj_t *UNUSED(parent), struct obj_t *obj)
{
unsigned int i;
if (!(obj->is_row || (f.lvl == 1 && f.csv_data)))
return;
if (f.csv_hdr) {
/* Print row with CSV header. */
indent(0);
util_rec_print_hdr(f.csv_rec);
f.csv_hdr = false;
}
/* Print row with CSV data. */
indent(0);
util_rec_print(f.csv_rec);
f.csv_data = false;
/* Reset non-persistent fields. */
for (i = 0; i < f.num_keys; i++) {
if (!f.keys[i].persist)
util_rec_set(f.csv_rec, f.keys[i].name, "\"\"");
}
}
static void csv_map(struct obj_t *UNUSED(obj), unsigned int mflags,
const char *key, const char *val)
{
char *qval = NULL, *prefixed_key;
/* Use empty string for invalid values. */
if (mflags & FMT_INVAL)
val = "";
/* Quote value if requested. */
if (f.quote_all || (mflags & FMT_QUOTE))
qval = csv_quote(val);
/* Process key and value. */
prefixed_key = add_prefix(key, false);
if (update_key(prefixed_key, mflags & FMT_PERSIST)) {
util_rec_set(f.csv_rec, prefixed_key, "%s", qval ?: val);
f.csv_data = true;
}
free(prefixed_key);
free(qval);
}
static bool hide_meta_env(void)
{
char *v;
v = secure_getenv("FMT_NOMETA");
return (v && strcmp(v, "1") == 0);
}
void util_fmt_init(FILE *fd, enum util_fmt_t type, unsigned int flags,
int api_level)
{
memset(&f, 0, sizeof(f));
f.type = type;
f.fd = fd;
f.fileno = fileno(fd);
f.hide_prefix = (flags & FMT_NOPREFIX);
f.hide_inval = !(flags & FMT_KEEPINVAL);
f.hide_meta = (flags & FMT_NOMETA) || hide_meta_env();
f.quote_all = (flags & FMT_QUOTEALL);
f.do_filter = (flags & FMT_FILTER);
f.do_warn = (flags & FMT_WARN);
f.handle_int = (flags & FMT_HANDLEINT);
f.api_level = api_level;
if (util_fmt_is_json_stream(type))
f.nl = "";
else
f.nl = "\n";
f.ind_width = 2;
f.ind_char = ' ';
f.meta_done = false;
switch (type) {
case FMT_PAIRS:
f.map = &pairs_map;
break;
case FMT_JSON:
case FMT_JSONSEQ:
case FMT_JSONL:
f.obj_start = &json_obj_start;
f.obj_end = &json_obj_end;
f.map = &json_map;
f.term = &json_term;
break;
case FMT_CSV:
f.obj_start = &csv_obj_start;
f.obj_end = &csv_obj_end;
f.map = &csv_map;
f.csv_rec = util_rec_new_csv(",");
f.csv_hdr = true;
f.csv_data = false;
break;
}
/* Ensure consistent number format for callers that use setlocale(). */
setlocale(LC_NUMERIC, "C");
if (f.handle_int) {
setup_int_handler();
sigemptyset(&no_int_mask);
sigaddset(&no_int_mask, SIGINT);
sigaddset(&no_int_mask, SIGTERM);
}
}