btrfs-progs/common/format-output.c
David Sterba ede538f98d btrfs-progs: print null value for empty uuid in json
This is a potentially breaking change to json output. An all zeros uuid
was printed as "-" but we can utilize native json type null for that.
Note the va_copy must be used as va_arg advances the pointer.

{
	"nulluuid": null
}

Signed-off-by: David Sterba <dsterba@suse.com>
2023-08-28 17:24:23 +02:00

410 lines
9.4 KiB
C

/*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
* License v2 as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program; if not, write to the
* Free Software Foundation, Inc., 59 Temple Place - Suite 330,
* Boston, MA 021110-1307, USA.
*/
#include "kerncompat.h"
#include <stdio.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <uuid/uuid.h>
#include "common/defs.h"
#include "common/format-output.h"
#include "common/utils.h"
#include "common/units.h"
#include "cmds/commands.h"
static void print_uuid(const u8 *uuid)
{
char uuidparse[BTRFS_UUID_UNPARSED_SIZE];
if (uuid_is_null(uuid)) {
printf("null");
} else {
uuid_unparse(uuid, uuidparse);
printf("%s", uuidparse);
}
}
static void print_escaped(const char *str)
{
while (*str) {
switch (*str) {
case '\b': /* 0x08 */
putchar('\\');
putchar('b');
break;
case '\t': /* 0x09 */
putchar('\\');
putchar('t');
break;
case '\n': /* 0x0a */
putchar('\\');
putchar('n');
break;
case '\f': /* 0x0c */
putchar('\\');
putchar('f');
break;
case '\r': /* 0x0d */
putchar('\\');
putchar('r');
break;
/* Other control characters from 0 .. 31 */
case '\v': /* 0x0b */
case 0x00 ... 0x07:
case 0x0e ... 0x1f:
printf("\\u%04x", *str);
break;
/* '/' (solidus) not escaped */
case '"':
case '\\':
putchar('\\');
fallthrough;
default:
putchar(*str);
}
str++;
}
}
static void fmt_indent1(int indent)
{
while (indent--)
putchar(' ');
}
static void fmt_indent2(int indent)
{
while (indent--) {
putchar(' ');
putchar(' ');
}
}
static void fmt_error(struct format_ctx *fctx)
{
internal_error("formatting json: depth=%d", fctx->depth);
exit(1);
}
static void fmt_inc_depth(struct format_ctx *fctx)
{
if (fctx->depth >= JSON_NESTING_LIMIT - 1) {
internal_error("nesting too deep, limit %d", JSON_NESTING_LIMIT);
exit(1);
}
fctx->depth++;
}
static void fmt_dec_depth(struct format_ctx *fctx)
{
if (fctx->depth < 1) {
internal_error("nesting below first level");
exit(1);
}
fctx->depth--;
}
static void fmt_separator(struct format_ctx *fctx)
{
if (bconf.output_format == CMD_FORMAT_JSON) {
/* Check current depth */
if (fctx->memb[fctx->depth] == 0) {
/* First member, only indent */
putchar('\n');
fmt_indent2(fctx->depth);
fctx->memb[fctx->depth] = 1;
} else if (fctx->memb[fctx->depth] == 1) {
/* Something has been printed already */
printf(",\n");
fmt_indent2(fctx->depth);
fctx->memb[fctx->depth] = 2;
} else {
/* N-th member */
printf(",\n");
fmt_indent2(fctx->depth);
}
}
}
/* Detect formats or values that must not be quoted (null, bool) */
static bool fmt_set_unquoted(struct format_ctx *fctx, const struct rowspec *row,
va_list args)
{
static const char *types[] = { "%llu", "bool" };
for (int i = 0; i < sizeof(types) / sizeof(types[0]); i++)
if (strcmp(types[i], row->fmt) == 0)
return true;
/* Null value */
if (strcmp("uuid", row->fmt) == 0) {
va_list tmpargs;
const u8 *uuid;
va_copy(tmpargs, args);
uuid = va_arg(tmpargs, const u8 *);
if (uuid_is_null(uuid))
return true;
}
return false;
}
void fmt_start(struct format_ctx *fctx, const struct rowspec *spec, int width,
int indent)
{
memset(fctx, 0, sizeof(*fctx));
fctx->width = width;
fctx->indent = indent;
fctx->rowspec = spec;
fctx->depth = 1;
if (bconf.output_format & CMD_FORMAT_JSON) {
putchar('{');
/* The top level is a map and is the first one */
fctx->jtype[fctx->depth] = JSON_TYPE_MAP;
fctx->memb[fctx->depth] = 0;
fmt_print_start_group(fctx, "__header", JSON_TYPE_MAP);
fmt_separator(fctx);
printf("\"version\": \"1\"");
fctx->memb[fctx->depth] = 1;
fmt_print_end_group(fctx, "__header");
}
}
void fmt_end(struct format_ctx *fctx)
{
if (fctx->depth != 1)
fprintf(stderr, "WARNING: wrong nesting\n");
/* Close, no continuation to print */
if (bconf.output_format & CMD_FORMAT_JSON) {
fmt_dec_depth(fctx);
fmt_separator(fctx);
printf("}\n");
}
}
void fmt_start_list_value(struct format_ctx *fctx)
{
if (bconf.output_format == CMD_FORMAT_TEXT) {
fmt_indent1(fctx->indent);
} else if (bconf.output_format == CMD_FORMAT_JSON) {
fmt_separator(fctx);
fmt_indent2(fctx->depth);
putchar('"');
}
}
void fmt_end_list_value(struct format_ctx *fctx)
{
if (bconf.output_format == CMD_FORMAT_TEXT)
putchar('\n');
else if (bconf.output_format == CMD_FORMAT_JSON)
putchar('"');
}
void fmt_start_value(struct format_ctx *fctx, const struct rowspec *row)
{
if (bconf.output_format == CMD_FORMAT_TEXT) {
if (strcmp(row->fmt, "list") == 0)
putchar('\n');
else if (strcmp(row->fmt, "map") == 0)
putchar('\n');
} else if (bconf.output_format == CMD_FORMAT_JSON) {
if (strcmp(row->fmt, "list") == 0) {
} else if (strcmp(row->fmt, "map") == 0) {
} else if (fctx->unquoted) {
} else {
putchar('"');
}
}
}
/*
* Newline depends on format type:
* - json does delayed continuation "," in case there's a following object
* - plain text always ends with a newline
*/
void fmt_end_value(struct format_ctx *fctx, const struct rowspec *row)
{
if (bconf.output_format == CMD_FORMAT_TEXT)
putchar('\n');
if (bconf.output_format == CMD_FORMAT_JSON) {
if (strcmp(row->fmt, "list") == 0) {
} else if (strcmp(row->fmt, "map") == 0) {
} else if (fctx->unquoted) {
} else {
putchar('"');
}
}
}
void fmt_print_start_group(struct format_ctx *fctx, const char *name,
enum json_type jtype)
{
if (bconf.output_format == CMD_FORMAT_JSON) {
fmt_separator(fctx);
fmt_inc_depth(fctx);
fctx->jtype[fctx->depth] = jtype;
fctx->memb[fctx->depth] = 0;
if (name)
printf("\"%s\": ", name);
if (jtype == JSON_TYPE_MAP)
putchar('{');
else if (jtype == JSON_TYPE_ARRAY)
putchar('[');
else
fmt_error(fctx);
}
}
void fmt_print_end_group(struct format_ctx *fctx, const char *name)
{
if (bconf.output_format == CMD_FORMAT_JSON) {
/* Whatever was on previous line won't continue with "," */
const enum json_type jtype = fctx->jtype[fctx->depth];
fmt_dec_depth(fctx);
putchar('\n');
fmt_indent2(fctx->depth);
if (jtype == JSON_TYPE_MAP)
putchar('}');
else if (jtype == JSON_TYPE_ARRAY)
putchar(']');
else
fmt_error(fctx);
}
}
/* Use rowspec to print according to currently set output format */
void fmt_print(struct format_ctx *fctx, const char* key, ...)
{
va_list args;
const struct rowspec *row;
bool found = false;
va_start(args, key);
row = &fctx->rowspec[0];
while (row->key) {
if (strcmp(key, row->key) == 0) {
found = true;
break;
}
row++;
}
if (!found) {
internal_error("unknown key: %s", key);
exit(1);
}
if (bconf.output_format == CMD_FORMAT_TEXT) {
const bool print_colon = row->out_text[0];
int len;
/* Print indented key name */
fmt_indent1(fctx->indent);
len = strlen(row->out_text);
printf("%s", row->out_text);
if (print_colon) {
putchar(':');
len++;
}
/* Align start for the value */
fmt_indent1(fctx->width - len);
} else if (bconf.output_format == CMD_FORMAT_JSON) {
if (strcmp(row->fmt, "list") == 0) {
fmt_print_start_group(fctx, row->out_json,
JSON_TYPE_ARRAY);
} else if (strcmp(row->fmt, "map") == 0) {
fmt_print_start_group(fctx, row->out_json,
JSON_TYPE_MAP);
} else {
/* Simple key/values */
fmt_separator(fctx);
if (row->out_json)
printf("\"%s\": ", row->out_json);
}
}
fctx->unquoted = fmt_set_unquoted(fctx, row, args);
fmt_start_value(fctx, row);
if (row->fmt[0] == '%') {
vprintf(row->fmt, args);
} else if (strcmp(row->fmt, "bool") == 0) {
/* Bool is passed as int to varargs */
bool value = va_arg(args, int);
printf("%s", value ? "true" : "false");
} else if (strcmp(row->fmt, "str") == 0) {
const char *str = va_arg(args, const char *);
print_escaped(str);
} else if (strcmp(row->fmt, "uuid") == 0) {
const u8 *uuid = va_arg(args, const u8*);
print_uuid(uuid);
} else if (strcmp(row->fmt, "date-time") == 0) {
const time_t ts = va_arg(args, time_t);
if (ts) {
char tstr[256];
struct tm tm;
localtime_r(&ts, &tm);
strftime(tstr, 256, "%Y-%m-%d %X %z", &tm);
printf("%s", tstr);
} else {
putchar('-');
}
} else if (strcmp(row->fmt, "list") == 0) {
} else if (strcmp(row->fmt, "map") == 0) {
} else if (strcmp(row->fmt, "qgroupid") == 0) {
/*
* Level is u16 but promoted to int when it's a vararg, callers
* should add explicit cast.
*/
const int level = va_arg(args, int);
const u64 id = va_arg(args, u64);
printf("%hu/%llu", level, id);
} else if (strcmp(row->fmt, "size-or-none") == 0) {
const u64 size = va_arg(args, u64);
const unsigned int unit_mode = va_arg(args, unsigned int);
if (size)
printf("%s", pretty_size_mode(size, unit_mode));
else
putchar('-');
} else if (strcmp(row->fmt, "size") == 0) {
const u64 size = va_arg(args, u64);
const unsigned int unit_mode = va_arg(args, unsigned int);
printf("%s", pretty_size_mode(size, unit_mode));
} else {
internal_error("unknown format %s", row->fmt);
}
fmt_end_value(fctx, row);
va_end(args);
}