You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
595 lines
13 KiB
C
595 lines
13 KiB
C
/* Copyright (C) 2021,2022 fef <owo@fef.moe>. All rights reserved. */
|
|
|
|
/*
|
|
* Ok, i'm gonna be honest, this is one of those functions where i wish i was
|
|
* writing them in Rust rather than C. Anyway, the tricky part of this entire
|
|
* implementation is that we can't call kmalloc() because that uses kprintf()
|
|
* internally for debug messages and the such, so it could potentially end in an
|
|
* infinite loop. But, since format strings allow arbitrary padding, we need to
|
|
* resort to our good old friend alloca().
|
|
* Also, this code is probably buggy as shit and i only tested some basic format
|
|
* sequences, so don't push it too far. Be gentle.
|
|
* I hope i never have to touch this again.
|
|
*/
|
|
|
|
#include <alloca.h>
|
|
#include <stdarg.h>
|
|
|
|
#include <string.h>
|
|
|
|
#include <gay/cdefs.h>
|
|
#include <gay/kprintf.h>
|
|
#include <gay/types.h>
|
|
|
|
static struct kprintf_printer *printer = NULL;
|
|
|
|
int kprintf_set_printer(struct kprintf_printer *new)
|
|
{
|
|
int ret = 0;
|
|
|
|
if (printer != NULL)
|
|
ret = printer->flush(printer);
|
|
|
|
printer = new;
|
|
return ret;
|
|
}
|
|
|
|
enum length_modifier {
|
|
LENGTH_DEFAULT = 0,
|
|
LENGTH_H,
|
|
LENGTH_HH,
|
|
LENGTH_L,
|
|
LENGTH_LL,
|
|
LENGTH_J,
|
|
LENGTH_T,
|
|
LENGTH_Z,
|
|
};
|
|
|
|
struct fmt_sequence {
|
|
isize (*render)(const struct fmt_sequence *sequence, va_list *ap);
|
|
unsigned int min_width;
|
|
unsigned int max_precision;
|
|
enum length_modifier length_modifier;
|
|
struct {
|
|
bool hash;
|
|
bool zero;
|
|
bool minus;
|
|
bool space;
|
|
bool plus;
|
|
bool apos;
|
|
} flags __packed; /* save some bytes on the stack :) */
|
|
bool uppercase;
|
|
};
|
|
|
|
static void parse_fmt_sequence(struct fmt_sequence *sequence, const char **restrict posptr);
|
|
|
|
/** @brief Write a NUL terminated string using the current `printer`. */
|
|
static isize write_asciz(const char *s);
|
|
/** @brief Write a specific amount of bytes using the current `printer`. */
|
|
static isize write_bytes(const void *buf, usize len);
|
|
|
|
int kvprintf(const char *fmt, va_list _args)
|
|
{
|
|
isize ret = 0;
|
|
const char *tmp = fmt;
|
|
va_list args;
|
|
va_copy(args, _args);
|
|
|
|
while (*tmp != '\0') {
|
|
if (*tmp++ == '%') {
|
|
/* write out everything we have so far (minus one char for %) */
|
|
isize write_ret = write_bytes(fmt, (usize)tmp - (usize)fmt - 1);
|
|
if (write_ret < 0) {
|
|
ret = write_ret;
|
|
break;
|
|
}
|
|
ret += write_ret;
|
|
|
|
isize fmt_ret = 0;
|
|
struct fmt_sequence sequence;
|
|
parse_fmt_sequence(&sequence, &tmp);
|
|
if (sequence.render != NULL)
|
|
fmt_ret = sequence.render(&sequence, &args);
|
|
|
|
/*
|
|
* act as if the current position were the beginning in
|
|
* order to make the first step of this if block easier
|
|
*/
|
|
fmt = tmp;
|
|
|
|
if (fmt_ret < 0) {
|
|
ret = fmt_ret;
|
|
break;
|
|
}
|
|
ret += fmt_ret;
|
|
}
|
|
}
|
|
|
|
if (tmp != fmt && ret >= 0) {
|
|
isize render_ret = write_bytes(fmt, (usize)tmp - (usize)fmt);
|
|
if (render_ret < 0)
|
|
ret = render_ret;
|
|
else
|
|
ret += render_ret;
|
|
}
|
|
|
|
isize flush_ret = printer->flush(printer);
|
|
if (flush_ret < 0)
|
|
ret = flush_ret;
|
|
else
|
|
ret += flush_ret;
|
|
|
|
va_end(args);
|
|
return (int)ret;
|
|
}
|
|
|
|
int kprintf(const char *fmt, ...)
|
|
{
|
|
int ret;
|
|
va_list args;
|
|
|
|
va_start(args, fmt);
|
|
ret = kvprintf(fmt, args);
|
|
va_end(args);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static isize render_c(const struct fmt_sequence *sequence, va_list *ap);
|
|
static isize render_d(const struct fmt_sequence *sequence, va_list *ap);
|
|
static isize render_o(const struct fmt_sequence *sequence, va_list *ap);
|
|
static isize render_p(const struct fmt_sequence *sequence, va_list *ap);
|
|
static isize render_s(const struct fmt_sequence *sequence, va_list *ap);
|
|
static isize render_u(const struct fmt_sequence *sequence, va_list *ap);
|
|
static isize render_x(const struct fmt_sequence *sequence, va_list *ap);
|
|
static isize render_percent(const struct fmt_sequence *sequence, va_list *ap);
|
|
|
|
/*
|
|
* Oh boi this is gonna be fun.
|
|
* So, this is basically a step by step implementation of the FreeBSD manpage
|
|
* for printf(), except that there is no support for all the deprecated
|
|
* specifiers and the (imho insane) $ directive:
|
|
* <https://www.freebsd.org/cgi/man.cgi?query=printf&sektion=3&manpath=FreeBSD+13.0-RELEASE>
|
|
*/
|
|
void parse_fmt_sequence(struct fmt_sequence *sequence, const char **restrict posptr)
|
|
{
|
|
memset(sequence, 0, sizeof(*sequence));
|
|
|
|
if (**posptr == '%') { /* %% */
|
|
sequence->render = render_percent;
|
|
*posptr += 1;
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* parse optional flags
|
|
*/
|
|
bool continue_parse_flags = true;
|
|
while (continue_parse_flags) {
|
|
switch (**posptr) {
|
|
case '#':
|
|
sequence->flags.hash = true;
|
|
break;
|
|
case '0':
|
|
sequence->flags.zero = true;
|
|
break;
|
|
case '-':
|
|
sequence->flags.minus = true;
|
|
break;
|
|
case ' ':
|
|
/* the FreeBSD manpage says plus overrides space if both are used */
|
|
if (!sequence->flags.plus)
|
|
sequence->flags.space = true;
|
|
break;
|
|
case '+':
|
|
sequence->flags.plus = true;
|
|
sequence->flags.space = false;
|
|
break;
|
|
case '\'':
|
|
sequence->flags.apos = true;
|
|
break;
|
|
default:
|
|
continue_parse_flags = false;
|
|
break;
|
|
}
|
|
*posptr += 1;
|
|
}
|
|
*posptr -= 1;
|
|
|
|
/*
|
|
* parse optional minimum digits
|
|
*/
|
|
while (**posptr >= '0' && **posptr <= '9') {
|
|
sequence->min_width *= 10;
|
|
sequence->min_width += **posptr - '0';
|
|
if (sequence->max_precision > 128)
|
|
sequence->max_precision = 128;
|
|
*posptr += 1;
|
|
}
|
|
|
|
/*
|
|
* parse optional maximum precision
|
|
*/
|
|
if (**posptr == '.') {
|
|
*posptr += 1;
|
|
while (**posptr >= '0' && **posptr <= '9') {
|
|
sequence->max_precision *= 10;
|
|
sequence->max_precision += **posptr - '0';
|
|
/* sanitize length (prevents stack overflow) */
|
|
if (sequence->max_precision > 128)
|
|
sequence->max_precision = 128;
|
|
*posptr += 1;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* parse optional length modifier
|
|
*/
|
|
switch (**posptr) {
|
|
case 'h':
|
|
case 'H':
|
|
if ((*posptr)[1] == 'h' || (*posptr)[1] == 'H') {
|
|
sequence->length_modifier = LENGTH_HH;
|
|
*posptr += 2;
|
|
} else {
|
|
sequence->length_modifier = LENGTH_H;
|
|
*posptr += 1;
|
|
}
|
|
break;
|
|
case 'l':
|
|
case 'L':
|
|
if ((*posptr)[1] == 'l' || (*posptr)[1] == 'L') {
|
|
sequence->length_modifier = LENGTH_LL;
|
|
*posptr += 2;
|
|
} else {
|
|
sequence->length_modifier = LENGTH_L;
|
|
*posptr += 1;
|
|
}
|
|
break;
|
|
case 'j':
|
|
case 'J':
|
|
sequence->length_modifier = LENGTH_J;
|
|
*posptr += 1;
|
|
break;
|
|
case 't':
|
|
case 'T':
|
|
sequence->length_modifier = LENGTH_T;
|
|
*posptr += 1;
|
|
break;
|
|
case 'z':
|
|
case 'Z':
|
|
sequence->length_modifier = LENGTH_Z;
|
|
*posptr += 1;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
/*
|
|
* parse type specifier
|
|
*/
|
|
switch (**posptr) {
|
|
case 'C':
|
|
sequence->length_modifier = LENGTH_L;
|
|
/* fall through */
|
|
case 'c':
|
|
sequence->render = render_c;
|
|
break;
|
|
case 'd':
|
|
case 'i':
|
|
sequence->render = render_d;
|
|
break;
|
|
case 'o':
|
|
sequence->render = render_o;
|
|
break;
|
|
case 'P':
|
|
sequence->uppercase = true;
|
|
/* fall through */
|
|
case 'p':
|
|
sequence->render = render_p;
|
|
break;
|
|
case 'S':
|
|
sequence->length_modifier = LENGTH_L;
|
|
/* fall through */
|
|
case 's':
|
|
sequence->render = render_s;
|
|
break;
|
|
case 'u':
|
|
sequence->render = render_u;
|
|
break;
|
|
case 'X':
|
|
sequence->uppercase = true;
|
|
/* fall through */
|
|
case 'x':
|
|
sequence->render = render_x;
|
|
break;
|
|
default:
|
|
sequence->render = NULL;
|
|
break;
|
|
}
|
|
*posptr += 1;
|
|
}
|
|
|
|
static ssize_t render_c(const struct fmt_sequence *sequence, va_list *ap)
|
|
{
|
|
/* we don't support wchars until we have a UTF-8 encoder */
|
|
if (sequence->length_modifier != LENGTH_DEFAULT)
|
|
return -1;
|
|
|
|
char val = (char)va_arg(*ap, int);
|
|
return printer->write(printer, &val, sizeof(val));
|
|
}
|
|
|
|
static ssize_t render_s(const struct fmt_sequence *sequence, va_list *ap)
|
|
{
|
|
/*
|
|
* the string is a wchar_t if LENGTH_L is set, but that would require
|
|
* a full UTF-8 encoder which i won't write in the near future. Cope.
|
|
*/
|
|
if (sequence->length_modifier != LENGTH_DEFAULT)
|
|
return -1;
|
|
|
|
const char *s = va_arg(*ap, char *);
|
|
if (s == nil)
|
|
return write_asciz("(null)");
|
|
|
|
if (sequence->max_precision)
|
|
return write_bytes(s, strnlen(s, sequence->max_precision));
|
|
else
|
|
return write_asciz(s);
|
|
}
|
|
|
|
static inline intmax_t get_arg_signed(const struct fmt_sequence *sequence, va_list *ap)
|
|
{
|
|
switch (sequence->length_modifier) {
|
|
case LENGTH_H:
|
|
case LENGTH_HH:
|
|
case LENGTH_DEFAULT:
|
|
/* short and char will be promoted to int with parameter passing */
|
|
return va_arg(*ap, int);
|
|
case LENGTH_L:
|
|
return va_arg(*ap, long);
|
|
case LENGTH_LL:
|
|
return va_arg(*ap, long long);
|
|
case LENGTH_Z:
|
|
return va_arg(*ap, isize);
|
|
case LENGTH_J:
|
|
return va_arg(*ap, intmax_t);
|
|
case LENGTH_T:
|
|
return va_arg(*ap, intptr_t);
|
|
}
|
|
}
|
|
|
|
static inline uintmax_t get_arg_unsigned(const struct fmt_sequence *sequence, va_list *ap)
|
|
{
|
|
switch (sequence->length_modifier) {
|
|
case LENGTH_H:
|
|
case LENGTH_HH:
|
|
case LENGTH_DEFAULT:
|
|
/* short and char will be promoted to int with parameter passing */
|
|
return va_arg(*ap, unsigned int);
|
|
case LENGTH_L:
|
|
return va_arg(*ap, unsigned long);
|
|
case LENGTH_LL:
|
|
return va_arg(*ap, unsigned long long);
|
|
case LENGTH_Z:
|
|
return va_arg(*ap, usize);
|
|
case LENGTH_J:
|
|
return va_arg(*ap, uintmax_t);
|
|
case LENGTH_T:
|
|
return va_arg(*ap, uintptr_t);
|
|
}
|
|
}
|
|
|
|
static isize render_d(const struct fmt_sequence *sequence, va_list *ap)
|
|
{
|
|
isize ret = 0;
|
|
intmax_t val = get_arg_signed(sequence, ap);
|
|
|
|
if (val < 0) {
|
|
val = -val;
|
|
ret = write_asciz("-");
|
|
if (ret < 0)
|
|
return ret;
|
|
} else if (sequence->flags.plus) {
|
|
ret = write_asciz("+");
|
|
if (ret < 0)
|
|
return ret;
|
|
} else if (sequence->flags.space) {
|
|
ret = write_asciz(" ");
|
|
if (ret < 0)
|
|
return ret;
|
|
}
|
|
|
|
usize len = 20; /* 2**64 has 20 decimal digits, let's hope intmax_t isn't 128 bits */
|
|
if (sequence->min_width > len)
|
|
len = sequence->min_width;
|
|
char *buf = alloca(len);
|
|
char *pos = &buf[len - 1];
|
|
|
|
do {
|
|
*pos-- = (char)(val % 10) + '0'; /* NOLINT */
|
|
val /= 10;
|
|
} while (val > 0);
|
|
|
|
char fillchr;
|
|
if (sequence->flags.zero)
|
|
fillchr = '0';
|
|
else
|
|
fillchr = ' ';
|
|
while (sequence->min_width > len - (pos - buf) - 1)
|
|
*pos-- = fillchr;
|
|
|
|
pos += 1;
|
|
isize tmp = write_bytes(pos, len - (pos - buf));
|
|
if (tmp > 0)
|
|
ret += tmp;
|
|
else
|
|
ret = tmp;
|
|
return ret;
|
|
}
|
|
|
|
static isize render_o(const struct fmt_sequence *sequence, va_list *ap)
|
|
{
|
|
isize ret = 0;
|
|
|
|
if (sequence->flags.plus) {
|
|
ret = write_asciz("+");
|
|
if (ret < 0)
|
|
return ret;
|
|
} else if (sequence->flags.space) {
|
|
ret = write_asciz(" ");
|
|
if (ret < 0)
|
|
return ret;
|
|
}
|
|
|
|
uintmax_t val = get_arg_unsigned(sequence, ap);
|
|
|
|
usize len = 22; /* 2**64 has 22 octal digits, let's hope intmax_t isn't 128 bits */
|
|
if (sequence->min_width > len)
|
|
len = sequence->min_width;
|
|
char *buf = alloca(len);
|
|
char *pos = &buf[len - 1];
|
|
|
|
do {
|
|
*pos-- = (char)(val % 010) + '0'; /* NOLINT */
|
|
val /= 010;
|
|
} while (val > 0);
|
|
|
|
char fillchr;
|
|
if (sequence->flags.zero)
|
|
fillchr = '0';
|
|
else
|
|
fillchr = ' ';
|
|
while (sequence->min_width > len - (pos - buf) - 1)
|
|
*pos-- = fillchr;
|
|
|
|
pos++;
|
|
isize tmp = write_bytes(pos, len - (pos - buf));
|
|
if (tmp > 0)
|
|
ret += tmp;
|
|
else
|
|
ret = tmp;
|
|
return ret;
|
|
}
|
|
|
|
static const char *const digit_table_smol = "0123456789abcdef";
|
|
static const char *const digit_table_big = "0123456789ABCDEF";
|
|
|
|
static isize render_p(const struct fmt_sequence *sequence, va_list *ap)
|
|
{
|
|
/* 2 hex digits per byte + 2 for 0x prefix */
|
|
char buf[sizeof(uintptr_t) * 2 + 2];
|
|
char *pos = &buf[sizeof(uintptr_t) * 2 + 1];
|
|
const char *digit_table;
|
|
uintptr_t ptr = va_arg(*ap, uintptr_t);
|
|
|
|
buf[0] = '0';
|
|
buf[1] = 'x';
|
|
|
|
if (sequence->uppercase)
|
|
digit_table = digit_table_big;
|
|
else
|
|
digit_table = digit_table_smol;
|
|
|
|
while (pos > &buf[1]) {
|
|
*pos-- = digit_table[ptr % 0x10];
|
|
ptr /= 0x10;
|
|
}
|
|
|
|
return write_bytes(buf, sizeof(buf));
|
|
}
|
|
|
|
static isize render_u(const struct fmt_sequence *sequence, va_list *ap)
|
|
{
|
|
isize ret = 0;
|
|
|
|
if (sequence->flags.plus) {
|
|
ret = write_asciz("+");
|
|
if (ret < 0)
|
|
return ret;
|
|
} else if (sequence->flags.space) {
|
|
ret = write_asciz(" ");
|
|
if (ret < 0)
|
|
return ret;
|
|
}
|
|
|
|
uintmax_t val = get_arg_unsigned(sequence, ap);
|
|
|
|
usize len = 20; /* 2^64 has 20 decimal digits, let's hope intmax_t isn't 128 bits */
|
|
if (sequence->min_width > len)
|
|
len = sequence->min_width;
|
|
char *buf = alloca(len);
|
|
char *pos = &buf[len - 1];
|
|
|
|
do {
|
|
*pos-- = (char)(val % 10) + '0'; /* NOLINT */
|
|
val /= 10;
|
|
} while (val > 0);
|
|
|
|
char fillchr;
|
|
if (sequence->flags.zero)
|
|
fillchr = '0';
|
|
else
|
|
fillchr = ' ';
|
|
while (sequence->min_width > len - (pos - buf) - 1)
|
|
*pos-- = fillchr;
|
|
|
|
pos++;
|
|
isize tmp = write_bytes(pos, len - (pos - buf));
|
|
if (tmp > 0)
|
|
ret += tmp;
|
|
else
|
|
ret = tmp;
|
|
return ret;
|
|
}
|
|
|
|
static isize render_x(const struct fmt_sequence *sequence, va_list *ap)
|
|
{
|
|
char *buf;
|
|
usize len = sizeof(uintmax_t) * 2; /* 2 hex digits per byte */
|
|
if (len < sequence->min_width)
|
|
len = sequence->min_width;
|
|
buf = alloca(len);
|
|
char *pos = &buf[len - 1];
|
|
|
|
uintmax_t val = get_arg_unsigned(sequence, ap);
|
|
|
|
const char *digit_table;
|
|
if (sequence->uppercase)
|
|
digit_table = digit_table_big;
|
|
else
|
|
digit_table = digit_table_smol;
|
|
|
|
do {
|
|
*pos-- = digit_table[val % 0x10];
|
|
val /= 0x10;
|
|
} while (val > 0);
|
|
|
|
char fillchr;
|
|
if (sequence->flags.zero)
|
|
fillchr = '0';
|
|
else
|
|
fillchr = ' ';
|
|
while (sequence->min_width > len - (pos - buf) - 1)
|
|
*pos-- = fillchr;
|
|
|
|
pos++;
|
|
return write_bytes(pos, len - (pos - buf));
|
|
}
|
|
|
|
static isize render_percent(const struct fmt_sequence *sequence, va_list *ap)
|
|
{
|
|
return write_asciz("%");
|
|
}
|
|
|
|
static inline isize write_asciz(const char *s)
|
|
{
|
|
return printer->write(printer, s, strlen(s));
|
|
}
|
|
|
|
static inline isize write_bytes(const void *buf, usize len)
|
|
{
|
|
return printer->write(printer, buf, len);
|
|
}
|