255 lines
8.1 KiB
C
255 lines
8.1 KiB
C
|
|
/*----------------------------------------------------------------------+
|
|
| |
|
|
| vsnprintf.c -- the heart of the fprintf/sprintf family |
|
|
| |
|
|
+----------------------------------------------------------------------*/
|
|
|
|
/*
|
|
* Copyright (C) 2019, Marcel van Kervinck
|
|
* All rights reserved
|
|
*
|
|
* Please read the enclosed file `LICENSE' or retrieve this document
|
|
* from https://github.com/kervinck/gigatron-rom for terms and conditions.
|
|
*/
|
|
|
|
// TODO Handle size, flags, width and precision
|
|
// TODO Implement %o %x %X %p
|
|
// TODO Cross-check with C89 standard instead of K&R2
|
|
|
|
/*----------------------------------------------------------------------+
|
|
| Includes |
|
|
+----------------------------------------------------------------------*/
|
|
|
|
// C standard
|
|
#include <ctype.h>
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
|
|
/*----------------------------------------------------------------------+
|
|
| Definitions |
|
|
+----------------------------------------------------------------------*/
|
|
|
|
/*
|
|
* Our vsnprintf can work with string outputs and streams. This way
|
|
* we don't need to duplicate the implementation for the formatting
|
|
* logic, or work with intermediate buffers in memory. Strings are
|
|
* char pointers into memory. Streams are passed through the "backdoor"
|
|
* as `size' and marked with a NULL value for `str'.
|
|
*/
|
|
|
|
/*----------------------------------------------------------------------+
|
|
| Data |
|
|
+----------------------------------------------------------------------*/
|
|
|
|
static const unsigned radixtable[] = { 1, 10, 100, 1000, 10000 };
|
|
|
|
/*----------------------------------------------------------------------+
|
|
| Functions |
|
|
+----------------------------------------------------------------------*/
|
|
|
|
// Helpers
|
|
static const char *_parsenum(const char *str, int *num);
|
|
static int _discard(int c, FILE *stream);
|
|
|
|
/*----------------------------------------------------------------------+
|
|
| vsnprintf |
|
|
+----------------------------------------------------------------------*/
|
|
|
|
int vsnprintf(char *str, size_t size, const char *format, va_list ap)
|
|
{
|
|
enum { minus = 1, plus = 2, zero = 4, space = 8, hash = 16 };
|
|
|
|
int count = 0; // Return value is the number of chars produced
|
|
int flags, width, precision;
|
|
|
|
// For conversion
|
|
int sign, i, digit, m, radix;
|
|
const char *s;
|
|
|
|
struct _iobuf strbuf, *stream;
|
|
|
|
// Write out strings through a dummy FILE structure instead of str
|
|
#define charout(c) do{\
|
|
if (putc(c, stream) < 0)\
|
|
return EOF;\
|
|
count++;\
|
|
}while(0)
|
|
|
|
// String or stream?
|
|
if (str) { // String
|
|
if (size == 0)
|
|
return 0; // No space for anything
|
|
strbuf._ptr = str;
|
|
strbuf._n = size;
|
|
strbuf._flush = _discard;
|
|
stream = &strbuf;
|
|
} else // Stream
|
|
stream = (FILE*)size;
|
|
|
|
for (; *format; format++) {
|
|
if (*format != '%') {
|
|
charout(*format); // Verbatim
|
|
continue;
|
|
}
|
|
// Process formatter in the remainder of body
|
|
|
|
// Parse flags
|
|
flags = 0;
|
|
while (1) {
|
|
switch (*++format) {
|
|
case '-': flags |= minus; continue;
|
|
case '+': flags |= plus; continue;
|
|
case '0': flags |= zero; continue;
|
|
case ' ': flags |= space; continue;
|
|
case '#': flags |= hash; continue;
|
|
}
|
|
break;
|
|
}
|
|
|
|
// Parse minimum width
|
|
width = -1;
|
|
if (*format == '*') {
|
|
width = va_arg(ap, int);
|
|
format++;
|
|
} else
|
|
format = _parsenum(format, &width);
|
|
|
|
// Parse floating point precision or maximum string width
|
|
precision = -1;
|
|
if (*format == '.') {
|
|
format++;
|
|
if (*format == '*') {
|
|
precision = va_arg(ap, int);
|
|
format++;
|
|
} else
|
|
format = _parsenum(format, &precision);
|
|
}
|
|
|
|
// Parse length modifier
|
|
switch (*format) {
|
|
case 'h': // short or unsigned short
|
|
case 'l': // long or unsigned long
|
|
case 'L': // long double
|
|
format++;
|
|
break;
|
|
}
|
|
|
|
// Process conversion
|
|
switch (*format) {
|
|
case 'd': case 'i': // int: signed decimal
|
|
case 'u': // int: unsigned decimal
|
|
m = va_arg(ap, int);
|
|
|
|
// Split magnitude from sign
|
|
digit = sign = 0;
|
|
if (m < 0) {
|
|
if (*format == 'u') { // Large unsigned int
|
|
m -= 30000; // This can have signed overflow
|
|
digit = 3; // Make first digit safe for radix loop below
|
|
} else { // Negative signed int
|
|
m = -m; // Don't worry about -INT_MIN overflow
|
|
sign = '-';
|
|
width--;
|
|
}
|
|
}
|
|
|
|
if (!sign && (flags & plus)) {
|
|
sign = '+'; // Force plus sign
|
|
width--;
|
|
}
|
|
|
|
if (sign && (flags & zero)) {
|
|
charout(sign); // Emit sign first when padding with zeroes
|
|
sign = 0;
|
|
digit += '0'; // digit != 0 causes padding with zeroes
|
|
}
|
|
|
|
// Emit digits
|
|
for (i=4; i>=0; i--) {
|
|
radix = radixtable[i]; // Simpler and faster than division/modulo
|
|
while (1) {
|
|
m -= radix;
|
|
if (m < 0) // This relies on signed overflow for %u
|
|
break;
|
|
digit++;
|
|
}
|
|
m += radix;
|
|
|
|
if (digit || radix == 1U) {// Non-zero or last digit?
|
|
while (--width > i) // Padding with spaces or zeroes
|
|
charout(flags & zero ? '0' : ' ');
|
|
if (sign) {
|
|
charout(sign); // Emit sign before first digit
|
|
sign = 0;
|
|
}
|
|
charout(digit|'0'); // Emit digit as printable ASCII
|
|
digit = '0'; // Enable all following digits
|
|
}
|
|
}
|
|
break;
|
|
case 'c': // int: single character, cast to unsigned char
|
|
charout(va_arg(ap, unsigned char));
|
|
break;
|
|
case 's': // char*s: string
|
|
for (s=va_arg(ap, const char*); *s; s++)
|
|
charout(*s);
|
|
break;
|
|
case 'n': // int*n: count write-back, no conversion
|
|
*va_arg(ap, int*) = count;
|
|
break;
|
|
case '%': // literal %-sign, no conversion
|
|
charout('%');
|
|
break;
|
|
#if 0
|
|
case 'o': // int: unsigned octal (without a leading zero)
|
|
case 'x': case 'X': // int: unsigned hex (without leading 0x or 0X)
|
|
case 'p': // void*: implementation-specific pointer
|
|
case 'f': // double: [-]mmm.ddd, default precision 6, no decimal point for 0
|
|
case 'e': case 'E': // double: [-]m.dddddde+/-xx or [-]m.ddddddE+/-xx
|
|
case 'g': case 'G': // double: %e/%E if xx<-4 or xx >= precision, otherwise %f
|
|
#endif
|
|
default: // Unknown or not implemented conversion
|
|
charout('@');
|
|
(void) va_arg(ap, int); // Swallow an argument and hope for the best
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (str)
|
|
*stream->_ptr = '\0'; // Terminating zero for strings
|
|
|
|
return count;
|
|
}
|
|
|
|
/*----------------------------------------------------------------------+
|
|
| Helpers |
|
|
+----------------------------------------------------------------------*/
|
|
|
|
/*
|
|
* Parse optional number from string, returns advanced pointer.
|
|
* Only writes back the result through *num in case a number is present.
|
|
*/
|
|
static const char *_parsenum(const char *s, int *num)
|
|
{
|
|
int n = 0;
|
|
while (isdigit(*s)) {
|
|
n = n * 10 + *s++ - '0';
|
|
*num = n;
|
|
}
|
|
return s;
|
|
}
|
|
|
|
/*
|
|
* Throw away chars that don't fit in the caller's string space
|
|
*/
|
|
static int _discard(int c, FILE *stream)
|
|
{
|
|
return (stream->_n = 1);
|
|
}
|
|
|
|
/*----------------------------------------------------------------------+
|
|
| |
|
|
+----------------------------------------------------------------------*/
|
|
|