// Copyright 2022 Darwin Schuppan // SPDX license identifier: MIT #define GENERIC_IMPL_STATIC #include #include #include #include #define GENERIC_TYPE FmtPrintFunc #define GENERIC_NAME _PrintFuncMap #define GENERIC_PREFIX _print_func_map #include /* Contains all builtin and custom formatting functions. */ static _PrintFuncMap _print_funcs; /* Whether the fmt library was initialized. */ static bool initialized = false; /********************************\ |* Builtin fmt Functions *| \********************************/ static FmtPrintFuncRet _print_string_func(FmtContext *restrict ctx, FmtAttrs *restrict attrs, va_list v) { int pad = 0, padchar = ' '; if (attrs != NULL) { size_t i; for (i = 0; i < attrs->len; i++) { if (strcmp(attrs->names[i], "p") == 0) { pad = attrs->vals[i]; } else if (strcmp(attrs->names[i], "c") == 0) { padchar = attrs->vals[i]; } else { return FMT_PRINT_FUNC_RET_INVALID_ATTR(i); } } } const char *s = va_arg(v, const char *); if (s == NULL) s = "(null)"; size_t len = strlen(s); if (pad > len) { size_t j; for (j = 0; j < pad - len; j++) ctx->putc_func(ctx, padchar); } while (*s != 0) ctx->putc_func(ctx, *s++); return FMT_PRINT_FUNC_RET_OK(); } /* Helper function to print any integer. */ static FmtPrintFuncRet _print_integer(FmtContext *restrict ctx, FmtAttrs *restrict attrs, unsigned long long int val, bool negative) { int pad = 0, padchar = ' ', base = 10; bool uppercase = false; if (attrs != NULL) { size_t i; for (i = 0; i < attrs->len; i++) { if (strcmp(attrs->names[i], "X") == 0) { uppercase = true; base = 16; } else if (strcmp(attrs->names[i], "x") == 0) { uppercase = false; base = 16; } else if (strcmp(attrs->names[i], "o") == 0) { base = 8; } else if (strcmp(attrs->names[i], "p") == 0) { pad = attrs->vals[i]; } else if (strcmp(attrs->names[i], "c") == 0) { padchar = attrs->vals[i]; } else if (strcmp(attrs->names[i], "b") == 0) { base = attrs->vals[i]; } else { return FMT_PRINT_FUNC_RET_INVALID_ATTR(i); } } } if (base < 2 || base > 16) base = 10; size_t i = 0; char buf[65]; /* max: 64-bit binary number with negative sign */ if (val == 0) buf[i++] = '0'; else { char a_begin = uppercase ? 'A' : 'a'; while (val != 0) { unsigned long long int rem = val % base; buf[i++] = rem > 9 ? (rem - 10) + a_begin : rem + '0'; val /= base; } if (negative) buf[i++] = '-'; } if (pad > i) { size_t j; for (j = 0; j < pad - i; j++) ctx->putc_func(ctx, padchar); } while (i--) ctx->putc_func(ctx, buf[i]); return FMT_PRINT_FUNC_RET_OK(); } static FmtPrintFuncRet _print_int_func(FmtContext *restrict ctx, FmtAttrs *restrict attrs, va_list v) { int val = va_arg(v, int); if (val < 0) return _print_integer(ctx, attrs, -val, true); else return _print_integer(ctx, attrs, val, false); } static FmtPrintFuncRet _print_lint_func(FmtContext *restrict ctx, FmtAttrs *restrict attrs, va_list v) { long int val = va_arg(v, long int); if (val < 0) return _print_integer(ctx, attrs, -val, true); else return _print_integer(ctx, attrs, val, false); } static FmtPrintFuncRet _print_llint_func(FmtContext *restrict ctx, FmtAttrs *restrict attrs, va_list v) { long long int val = va_arg(v, long long int); if (val < 0) return _print_integer(ctx, attrs, -val, true); else return _print_integer(ctx, attrs, val, false); } static FmtPrintFuncRet _print_uint_func(FmtContext *restrict ctx, FmtAttrs *restrict attrs, va_list v) { return _print_integer(ctx, attrs, va_arg(v, unsigned int), false); } static FmtPrintFuncRet _print_ulint_func(FmtContext *restrict ctx, FmtAttrs *restrict attrs, va_list v) { return _print_integer(ctx, attrs, va_arg(v, unsigned long int), false); } static FmtPrintFuncRet _print_ullint_func(FmtContext *restrict ctx, FmtAttrs *restrict attrs, va_list v) { return _print_integer(ctx, attrs, va_arg(v, unsigned long long int), false); } static FmtPrintFuncRet _print_size_t_func(FmtContext *restrict ctx, FmtAttrs *restrict attrs, va_list v) { return _print_integer(ctx, attrs, va_arg(v, size_t), false); } static FmtPrintFuncRet _print_ssize_t_func(FmtContext *restrict ctx, FmtAttrs *restrict attrs, va_list v) { return _print_integer(ctx, attrs, va_arg(v, ssize_t), false); } static void _print_error(FmtContext *restrict ctx, Error val, bool destroy) { if (val.has_location) { fmtc(ctx, "%s:%zu: ", val.file, val.line); } switch (val.kind) { case ErrorNone: fmtc(ctx, "Success"); break; case ErrorOutOfMemory: fmtc(ctx, "Out of memory"); break; case ErrorString: fmtc(ctx, "%s", val.str); if (val.str_on_heap && destroy) free(val.str); break; } if (val.has_annex) { fmtc(ctx, ": "); _print_error(ctx, *val.annex, destroy); if (destroy) free(val.annex); } } static FmtPrintFuncRet _print_error_func(FmtContext *restrict ctx, FmtAttrs *restrict attrs, va_list v) { bool destroy = false; if (attrs != NULL) { size_t i; for (i = 0; i < attrs->len; i++) { if (strcmp(attrs->names[i], "destroy") == 0) { destroy = true; } else { return FMT_PRINT_FUNC_RET_INVALID_ATTR(i); } } } Error val = va_arg(v, Error); _print_error(ctx, val, destroy); return FMT_PRINT_FUNC_RET_OK(); } /********************************\ |* Helper Functions *| \********************************/ typedef enum { UnclosedCharLiteral, End, Colon, Equals, Comma, Asterisk, String, Number, /* also returned as a string, but guaranteed to be a valid number */ CharLiteral, /* char literal encased in '' */ } _Token; static _Token _next_token(const char **restrict fmt, char *restrict strbuf, size_t strbuf_size) { const char *c = *fmt; if (strbuf_size == 0) strbuf = NULL; switch (*c) { case '}': case 0: *fmt = ++c; return End; case ':': *fmt = ++c; return Colon; case '=': *fmt = ++c; return Equals; case ',': *fmt = ++c; return Comma; case '*': *fmt = ++c; return Asterisk; case '\'': c++; if (!(c[0] != 0 && c[1] == '\'')) return UnclosedCharLiteral; else if (strbuf != NULL) { *strbuf++ = *c++; *strbuf++ = 0; } *fmt = ++c; return CharLiteral; } _Token ret = (*c == '-') || (*c >= '0' && *c <= '9') ? Number : String; for (size_t i = 0;; c++, i++) { if (*c == 0 || *c == '}' || *c == ':' || *c == '=' || *c == ',' || *c == '*') break; if (ret == Number && !(*c >= '0' && *c <= '9')) ret = String; if (strbuf != NULL && i < strbuf_size - 1) *strbuf++ = *c; } if (strbuf != NULL) *strbuf++ = 0; *fmt = c; return ret; } static void _fmtf_putc_func(FmtContext *restrict ctx, char c) { FILE *f = ctx->ctx_data; fputc(c, f); } typedef struct _FmtsContext { char *buf; size_t size, written; } _FmtsContext; static void _fmts_putc_func(FmtContext *restrict ctx, char c) { _FmtsContext *ctxs = ctx->ctx_data; if (ctxs->size > 0 && ctxs->written < ctxs->size - 1) ctxs->buf[ctxs->written] = c; ctxs->written++; } /********************************\ |* The guts of any fmt function *| \********************************/ static Error _fmt_main(bool use_err_location, const char *file, size_t line, FmtContext *ctx, const char *restrict format, va_list args) { if (!initialized) { const char *err_str = "fmt: fmt_init() must be called at the beginning of the program before using any other fmt functions, and fmt_term() must be called when done\n"; return use_err_location ? ERROR_STRING_LOCATION(file, line, err_str) : ERROR_STRING(err_str); } const char *c = format; while (*c != 0) { if (*c == '%') { c++; if (*c == '{') { FmtAttrs attrs = {0}; c++; _Token t; char name[128]; t = _next_token(&c, name, 128); if (t != String) { const char *err_str = "fmt: Expected valid type name after %{"; return use_err_location ? ERROR_STRING_LOCATION(file, line, err_str) : ERROR_STRING(err_str); } FmtPrintFunc *print_func = _print_func_map_get(_print_funcs, name); if (print_func == NULL) { char errbuf[256]; snprintf(errbuf, 256, "fmt: Unrecognized type name: '%s'", name); return use_err_location ? ERROR_HEAP_STRING_LOCATION(file, line, strdup(errbuf)) : ERROR_HEAP_STRING(strdup(errbuf)); } t = _next_token(&c, NULL, 0); if (t == Colon) { while (attrs.len < FMT_MAX_ATTRS) { t = _next_token(&c, attrs.names[attrs.len], FMT_MAX_ATTR_LEN); if (t != String) { const char *err_str = "fmt: Expected valid attribute name after %{:"; return use_err_location ? ERROR_STRING_LOCATION(file, line, err_str) : ERROR_STRING(err_str); } t = _next_token(&c, NULL, 0); if (t == Equals) { char num[64]; t = _next_token(&c, num, 64); if (t != Number && t != CharLiteral && t != Asterisk) { if (t == UnclosedCharLiteral) { const char *err_str = "fmt: Char literal after %{:= unclosed or containing more than one character"; return use_err_location ? ERROR_STRING_LOCATION(file, line, err_str) : ERROR_STRING(err_str); } else { const char *err_str = "fmt: Expected number, char literal or asterisk after %{:="; return use_err_location ? ERROR_STRING_LOCATION(file, line, err_str) : ERROR_STRING(err_str); } } if (t == Number) attrs.vals[attrs.len] = atoi(num); else if (t == CharLiteral) attrs.vals[attrs.len] = num[0]; else if (t == Asterisk) attrs.vals[attrs.len] = va_arg(args, int); t = _next_token(&c, NULL, 0); } else attrs.vals[attrs.len] = -1; attrs.len++; if (t == End) break; if (t != Comma) { const char *err_str = "fmt: Expected ',' or '}' after %{:="; return use_err_location ? ERROR_STRING_LOCATION(file, line, err_str) : ERROR_STRING(err_str); } } } FmtPrintFuncRet res = (*print_func)(ctx, &attrs, args); if (res.invalid_attr) { char errbuf[256]; snprintf(errbuf, 256, "fmt: Invalid attribute: '%s'", attrs.names[res.invalid_attr_idx]); return use_err_location ? ERROR_HEAP_STRING_LOCATION(file, line, strdup(errbuf)) : ERROR_HEAP_STRING(strdup(errbuf)); } if (t != End) { const char *err_str = "fmt: Expected end"; return use_err_location ? ERROR_STRING_LOCATION(file, line, err_str) : ERROR_STRING(err_str); } } else { enum { MOD_NONE, MOD_Z, MOD_L, MOD_LL, MOD_H, MOD_HH, }; int mod = MOD_NONE; switch (*c) { case 'z': c++; mod = MOD_Z; break; case 'l': c++; if (*c == 'l') { c++; mod = MOD_LL; } else mod = MOD_L; break; case 'h': c++; if (*c == 'h') { c++; mod = MOD_HH; } else mod = MOD_H; break; } switch (*c) { case '%': c++; ctx->putc_func(ctx, '%'); break; case 's': { c++; _print_string_func(ctx, NULL, args); break; } case 'c': { c++; ctx->putc_func(ctx, va_arg(args, int)); break; } case 'i': case 'd': c++; switch (mod) { default: case MOD_H: case MOD_HH: _print_int_func(ctx, NULL, args); break; case MOD_Z: _print_ssize_t_func(ctx, NULL, args); break; case MOD_L: _print_lint_func(ctx, NULL, args); break; case MOD_LL: _print_llint_func(ctx, NULL, args); break; } break; case 'u': c++; switch (mod) { default: case MOD_H: case MOD_HH: _print_uint_func(ctx, NULL, args); break; case MOD_Z: _print_size_t_func(ctx, NULL, args); break; case MOD_L: _print_ulint_func(ctx, NULL, args); break; case MOD_LL: _print_ullint_func(ctx, NULL, args); break; } break; case 'x': c++; FmtAttrs custom_attrs = { .len = 1, .names = { { 'x' } }, }; switch (mod) { default: case MOD_H: case MOD_HH: _print_uint_func(ctx, &custom_attrs, args); break; case MOD_Z: _print_size_t_func(ctx, &custom_attrs, args); break; case MOD_L: _print_ulint_func(ctx, &custom_attrs, args); break; case MOD_LL: _print_ullint_func(ctx, &custom_attrs, args); break; } break; case 'X': { c++; FmtAttrs custom_attrs = { .len = 1, .names = { { 'X' } }, }; switch (mod) { default: case MOD_H: case MOD_HH: _print_uint_func(ctx, &custom_attrs, args); break; case MOD_Z: _print_size_t_func(ctx, &custom_attrs, args); break; case MOD_L: _print_ulint_func(ctx, &custom_attrs, args); break; case MOD_LL: _print_ullint_func(ctx, &custom_attrs, args); break; } break; } case 'p': { c++; ctx->putc_func(ctx, '0'); ctx->putc_func(ctx, 'x'); FmtAttrs custom_attrs = { .len = 3, .names = { { 'p' }, { 'b' }, { 'c' } }, .vals = { 12, 16, '0' }, }; _print_size_t_func(ctx, &custom_attrs, args); break; } } } } else ctx->putc_func(ctx, *c++); } return OK(); } /********************************\ |* Public functions *| \********************************/ void fmt_register(const char *restrict keyword, FmtPrintFunc print_func) { _print_func_map_set(&_print_funcs, keyword, print_func); } void _fmtv(const char *file, size_t line, const char *restrict format, va_list args) { FmtContext ctx = { .ctx_data = stdout, .putc_func = _fmtf_putc_func, }; _fmtcv(file, line, &ctx, format, args); } void _fmt(const char *file, size_t line, const char *restrict format, ...) { va_list args; va_start(args, format); _fmtv(file, line, format, args); va_end(args); } size_t _fmtsv(const char *restrict file, size_t line, char *restrict buf, size_t size, const char *restrict format, va_list args) { _FmtsContext ctxs = { .buf = buf, .size = size, }; FmtContext ctx = { .ctx_data = &ctxs, .putc_func = _fmts_putc_func, }; memset(buf, 0, size); /* TODO: Remove this without valgrind complaining. */ _fmtcv(file, line, &ctx, format, args); if (size > 0) buf[ctxs.written] = 0; return ctxs.written; } size_t _fmts(const char *restrict file, size_t line, char *restrict buf, size_t size, const char *restrict format, ...) { va_list args; va_start(args, format); size_t res = _fmtsv(file, line, buf, size, format, args); va_end(args); return res; } void _fmtcv(const char *restrict file, size_t line, FmtContext *ctx, const char *restrict format, va_list args) { ERROR_ASSERT(_fmt_main(true, file, line, ctx, format, args)); } void _fmtc(const char *restrict file, size_t line, FmtContext *ctx, const char *restrict format, ...) { va_list args; va_start(args, format); _fmtcv(file, line, ctx, format, args); va_end(args); } void fmt_init() { if (initialized) { ERROR_ASSERT(ERROR_STRING("fmt: fmt_init() can only be called once")); } initialized = true; _print_funcs = _print_func_map(); fmt_register("str", _print_string_func); fmt_register("int", _print_int_func); fmt_register("unsigned int", _print_uint_func); fmt_register("uint", _print_uint_func); fmt_register("size_t", _print_size_t_func); fmt_register("ssize_t", _print_ssize_t_func); fmt_register("Error", _print_error_func); } void fmt_term() { _print_func_map_term(_print_funcs); }