433 lines
13 KiB
C
433 lines
13 KiB
C
/* MIT License
|
|
*
|
|
* Copyright (c) 2023-2026 rom-p
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
* in the Software without restriction, including without limitation the rights
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in all
|
|
* copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
* SOFTWARE.
|
|
*/
|
|
|
|
#include "picoprintf.h"
|
|
|
|
#include <stdlib.h> // llabs()
|
|
|
|
#ifdef PICOFORMAT_HANDLE_FLOATS
|
|
#include <math.h> // fabs()
|
|
#endif
|
|
|
|
static void flip(char* pLeft, char* pRight)
|
|
{
|
|
pRight--;
|
|
while (pLeft < pRight) {
|
|
char tmp = *pLeft;
|
|
*pLeft = *pRight;
|
|
*pRight = tmp;
|
|
pLeft++;
|
|
pRight--;
|
|
}
|
|
}
|
|
|
|
// using #define over `inline` for enabling porting to old C
|
|
#if !defined(MIN)
|
|
#define MIN(left, right) (((left) < (right)) ? (left) : (right))
|
|
#endif
|
|
#if !defined(MAX)
|
|
#define MAX(left, right) (((left) > (right)) ? (left) : (right))
|
|
#endif
|
|
|
|
// returns the pointer to the null-terminating character of the filled string
|
|
int pico_vsnprintf(char* pDest, size_t cbDest, const char* pFormat, va_list vl)
|
|
{
|
|
#if defined(PICOFORMAT_HANDLE_BIN) || defined(PICOFORMAT_HANDLE_OCT) || \
|
|
defined(PICOFORMAT_HANDLE_HEX)
|
|
const char* pLowercaseNumberDigits = "0123456789abcdef";
|
|
#endif
|
|
#if defined(PICOFORMAT_HANDLE_HEX)
|
|
const char* pUppercaseNumberDigits = "0123456789ABCDEF";
|
|
#elif defined(PICOFORMAT_HANDLE_OCT)
|
|
const char* pOctalDigits = pLowercaseNumberDigits;
|
|
#elif defined(PICOFORMAT_HANDLE_BIN)
|
|
const char* pBinaryDigits = pLowercaseNumberDigits;
|
|
#endif
|
|
char* pStart = pDest;
|
|
for (char* pEnd = pDest + cbDest - 1; *pFormat && pDest < pEnd;) {
|
|
if (*pFormat != '%') {
|
|
*pDest++ = *pFormat++;
|
|
} else { // format starts here
|
|
pFormat++; // skipping the '%'
|
|
if (*pFormat == '%') { // unless it's indeed a '%'
|
|
*pDest++ = *pFormat++;
|
|
} else { // first, collect the format
|
|
char format = '\0';
|
|
#if defined(PICOFORMAT_HANDLE_BIN) || defined(PICOFORMAT_HANDLE_OCT) || \
|
|
defined(PICOFORMAT_HANDLE_HEX)
|
|
int bits_per_digit =
|
|
0; // valid in 'b', 'o', 'p' and 'x'/'X' modes only
|
|
#endif
|
|
int whole_chars =
|
|
0; // number of chars in the whole part of the number
|
|
int decimal_chars =
|
|
-1; // if not specified, %f are rendered with 6, while %g are rendered with 0
|
|
#ifdef __aarch64__ // these platforms benefit from packing flags into a single variable
|
|
struct {
|
|
unsigned force_sign : 1;
|
|
unsigned fill_zeros : 1;
|
|
unsigned left_align : 1;
|
|
unsigned seen_period : 1;
|
|
unsigned seen_numbers : 1;
|
|
unsigned treat_as_unsigned : 1;
|
|
unsigned treat_as_long : 1;
|
|
unsigned render_in_lowercase : 1;
|
|
} flags = {0};
|
|
|
|
#define FLAGS flags.
|
|
|
|
#else // other platforms do not benefit from struct packing
|
|
unsigned force_sign = 0;
|
|
unsigned fill_zeros = 0;
|
|
#ifdef PICOFORMAT_HANDLE_FILL
|
|
unsigned left_align = 0;
|
|
#endif
|
|
unsigned seen_period = 0;
|
|
unsigned seen_numbers = 0;
|
|
unsigned treat_as_unsigned = 0;
|
|
unsigned treat_as_long = 0;
|
|
#if defined(PICOFORMAT_HANDLE_HEX) || defined(PICOFORMAT_HANDLE_FLOATS)
|
|
unsigned render_in_lowercase = 0;
|
|
#endif
|
|
#define FLAGS
|
|
#endif // struct packing platforms
|
|
for (; *pFormat && '\0' == format; pFormat++) {
|
|
switch (*pFormat) {
|
|
#ifdef PICOFORMAT_HANDLE_FORCEDSIGN
|
|
case '+':
|
|
FLAGS force_sign = 1;
|
|
break;
|
|
#endif // PICOFORMAT_HANDLE_FORCEDSIGN
|
|
#ifdef PICOFORMAT_HANDLE_FILL
|
|
case '-': // left-align flag
|
|
FLAGS left_align = 1;
|
|
break;
|
|
#endif // PICOFORMAT_HANDLE_FILL
|
|
case '0':
|
|
if (!FLAGS seen_numbers) {
|
|
FLAGS fill_zeros = 1;
|
|
break;
|
|
}
|
|
// fall through
|
|
case '1':
|
|
case '2':
|
|
case '3':
|
|
case '4':
|
|
case '5':
|
|
case '6':
|
|
case '7':
|
|
case '8':
|
|
case '9':
|
|
*(FLAGS seen_period ? &decimal_chars :
|
|
&whole_chars) *= 10;
|
|
*(FLAGS seen_period ? &decimal_chars :
|
|
&whole_chars) +=
|
|
*pFormat - '0';
|
|
FLAGS seen_numbers = 1;
|
|
break;
|
|
case '.':
|
|
FLAGS seen_numbers = FLAGS seen_period =
|
|
1;
|
|
decimal_chars = 0;
|
|
break;
|
|
case '*': // dynamic width/precision: read value from arg list
|
|
*(FLAGS seen_period ? &decimal_chars :
|
|
&whole_chars) =
|
|
va_arg(vl, int);
|
|
FLAGS seen_numbers = 1;
|
|
break;
|
|
case 'l': // long modifier
|
|
FLAGS treat_as_long = 1;
|
|
break;
|
|
case 'u': // unsigned decimal integer
|
|
FLAGS treat_as_unsigned = 1;
|
|
format = 'd';
|
|
break;
|
|
#ifdef PICOFORMAT_HANDLE_BIN
|
|
case 'b':
|
|
bits_per_digit = 0;
|
|
FLAGS treat_as_unsigned = 1;
|
|
format = 'b';
|
|
break;
|
|
#endif // PICOFORMAT_HANDLE_BIN
|
|
#ifdef PICOFORMAT_HANDLE_OCT
|
|
case 'o': // octal integer
|
|
bits_per_digit = 3;
|
|
FLAGS treat_as_unsigned = 1;
|
|
format = 'b';
|
|
break;
|
|
#endif // PICOFORMAT_HANDLE_OCT
|
|
#ifdef PICOFORMAT_HANDLE_HEX
|
|
case 'x': // hexadecimal integer
|
|
case 'p': // pointer
|
|
bits_per_digit = 4;
|
|
FLAGS treat_as_unsigned = 1;
|
|
FLAGS render_in_lowercase = 1;
|
|
format = 'b';
|
|
break;
|
|
case 'X': // hexadecimal integer, uppercase
|
|
bits_per_digit = 4;
|
|
FLAGS treat_as_unsigned = 1;
|
|
format = 'b';
|
|
break;
|
|
#endif // PICOFORMAT_HANDLE_HEX
|
|
#ifdef PICOFORMAT_HANDLE_FLOATS
|
|
case 'a':
|
|
#ifdef PICOFORMAT_HANDLE_EXPONENTS
|
|
case 'e': // floating point, exponent format
|
|
#endif // PICOFORMAT_HANDLE_EXPONENTS
|
|
case 'f':
|
|
FLAGS render_in_lowercase = 1;
|
|
// fall through
|
|
case 'F':
|
|
#endif // PICOFORMAT_HANDLE_FLOATS
|
|
case 'c': // single char: always supported
|
|
case 'd': // integer: always supported
|
|
case 'i': // integer: always supported
|
|
case 's': // string of native `char`s: always supported
|
|
format = *pFormat;
|
|
break;
|
|
default:
|
|
FORMAT_ERROR_DELEGATE(
|
|
"detected unhandled format specifier: %c",
|
|
*pFormat);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// format is parsed, now render the value
|
|
char* pParamStarts = pDest;
|
|
switch (format) {
|
|
case 'c': // single char
|
|
*pDest++ = va_arg(vl, int) & 0xff;
|
|
break;
|
|
case 's': { // null-terminated string
|
|
#ifdef PICOFORMAT_HANDLE_FILL
|
|
int len =
|
|
0; // effective length, bounded by precision if set
|
|
const char* pStr = va_arg(vl, const char*);
|
|
for (; pStr[len] &&
|
|
(decimal_chars < 0 || len < decimal_chars);
|
|
len++)
|
|
;
|
|
if (!FLAGS left_align) { // right-align: pad on the left
|
|
#ifdef PICOFORMAT_CLANG_QUIRK // clang's non-standard: '0' flag zero-pads strings
|
|
char chFill =
|
|
FLAGS fill_zeros ? '0' : ' ';
|
|
#else // PICOFORMAT_CLANG_QUIRK // standard C: '0' flag is undefined for %s, use spaces
|
|
char chFill = ' ';
|
|
#endif // PICOFORMAT_CLANG_QUIRK
|
|
for (; pDest < pEnd && whole_chars > len;
|
|
whole_chars--) {
|
|
*pDest++ = chFill;
|
|
}
|
|
}
|
|
for (int ii = 0; ii < len && pDest < pEnd; ii++) {
|
|
*pDest++ = pStr[ii];
|
|
}
|
|
if (FLAGS left_align) { // left-align: pad on the right (always spaces; '-' flag overrides '0')
|
|
for (; pDest < pEnd && whole_chars > len;
|
|
whole_chars--) {
|
|
*pDest++ = ' ';
|
|
}
|
|
}
|
|
#else // PICOFORMAT_HANDLE_FILL
|
|
for (const char* pStr = va_arg(vl, const char*);
|
|
*pStr && pDest < pEnd;) {
|
|
*pDest++ = *pStr++;
|
|
}
|
|
#endif // PICOFORMAT_HANDLE_FILL
|
|
} break;
|
|
#if defined(PICOFORMAT_HANDLE_HEX) || defined(PICOFORMAT_HANDLE_OCT) || \
|
|
defined(PICOFORMAT_HANDLE_BIN)
|
|
case 'b': { // binary, oct, or hex integer, always unsigned
|
|
long long int val = 0;
|
|
if (FLAGS treat_as_long) {
|
|
val = va_arg(vl, unsigned long long int);
|
|
} else {
|
|
val = va_arg(vl, unsigned);
|
|
}
|
|
unsigned mask = bits_per_digit == 4 ? 0x0f :
|
|
bits_per_digit == 3 ? 0x07 :
|
|
0x01;
|
|
#if defined(PICOFORMAT_HANDLE_HEX)
|
|
const char* chars = FLAGS render_in_lowercase ?
|
|
pLowercaseNumberDigits :
|
|
pUppercaseNumberDigits;
|
|
#elif defined(PICOFORMAT_HANDLE_OCT)
|
|
const char* chars = pOctalDigits;
|
|
#elif defined(PICOFORMAT_HANDLE_BIN)
|
|
const char* chars = pBinaryDigits;
|
|
#endif // individual non-decimal formats
|
|
while (pDest < pEnd &&
|
|
(pDest == pParamStarts || val ||
|
|
pDest - pParamStarts < whole_chars)) {
|
|
char ch;
|
|
if (pDest == pParamStarts ||
|
|
0 != val) { // if first char or there's still meaningful digits
|
|
ch = chars[val & mask];
|
|
val >>= bits_per_digit;
|
|
} else {
|
|
ch = FLAGS fill_zeros ? '0' : ' ';
|
|
}
|
|
*pDest++ = ch;
|
|
}
|
|
flip(pParamStarts, pDest);
|
|
} break;
|
|
#endif // defined(PICOFORMAT_HANDLE_BIN) || defined(PICOFORMAT_HANDLE_OCT) || defined(PICOFORMAT_HANDLE_HEX)
|
|
case 'd': // decimal integer
|
|
case 'i': {
|
|
long long int val = 0;
|
|
if (FLAGS treat_as_long) {
|
|
if (FLAGS treat_as_unsigned) {
|
|
val = va_arg(
|
|
vl,
|
|
unsigned long long int);
|
|
} else {
|
|
val = va_arg(vl, long long int);
|
|
}
|
|
} else {
|
|
if (FLAGS treat_as_unsigned) {
|
|
val = va_arg(vl, unsigned);
|
|
} else {
|
|
val = va_arg(vl, int);
|
|
}
|
|
}
|
|
char chSign = '\0';
|
|
if ((FLAGS force_sign || val < 0) &&
|
|
pDest < pEnd) {
|
|
chSign = val < 0 ? '-' : '+';
|
|
val = llabs(val);
|
|
whole_chars--;
|
|
}
|
|
while (pDest < pEnd &&
|
|
(pDest == pParamStarts || val ||
|
|
pDest - pParamStarts < whole_chars)) {
|
|
char ch;
|
|
if (pDest == pParamStarts ||
|
|
0 != val) { // if first digit (i.e. zero) or there's still non-zero digits to write
|
|
ch = val % 10 + '0';
|
|
val /= 10;
|
|
} else {
|
|
if ('\0' != chSign &&
|
|
!FLAGS fill_zeros) { // write a sign if filling with spaces, not with zeros
|
|
*pDest++ = chSign;
|
|
whole_chars++;
|
|
chSign =
|
|
'\0'; // prevent the sign from being written twice
|
|
}
|
|
#ifdef PICOFORMAT_HANDLE_FILL
|
|
ch = FLAGS fill_zeros ? '0' : ' ';
|
|
#else // PICOFORMAT_HANDLE_FILL
|
|
break;
|
|
#endif // PICOFORMAT_HANDLE_FILL
|
|
}
|
|
*pDest++ = ch;
|
|
}
|
|
if ('\0' !=
|
|
chSign) { // write a sign if it wasn't written before
|
|
*pDest++ = chSign;
|
|
}
|
|
flip(pParamStarts, pDest);
|
|
} break;
|
|
#ifdef PICOFORMAT_HANDLE_FLOATS
|
|
case 'f':
|
|
case 'F': {
|
|
double val = va_arg(vl, double);
|
|
if (FLAGS force_sign || val < 0.f) {
|
|
*pDest++ = val < 0.f ? '-' : '+';
|
|
val = fabs(val);
|
|
pParamStarts = pDest;
|
|
}
|
|
if (val == INFINITY || val == -INFINITY ||
|
|
isnan(val)) {
|
|
for (const char* pszVal = isnan(val) ?
|
|
FLAGS render_in_lowercase ?
|
|
"nan" :
|
|
"NAN" :
|
|
FLAGS render_in_lowercase ?
|
|
"inf" :
|
|
"INF";
|
|
pDest < pEnd && *pszVal;
|
|
*pDest++ = *pszVal++)
|
|
;
|
|
} else {
|
|
if (decimal_chars == -1) {
|
|
if (format == 'g') {
|
|
decimal_chars = 0;
|
|
} else {
|
|
decimal_chars = 6;
|
|
}
|
|
}
|
|
whole_chars =
|
|
MAX(0,
|
|
whole_chars - decimal_chars);
|
|
whole_chars = MAX(
|
|
whole_chars,
|
|
pDest - pParamStarts +
|
|
1); // at least sign (if present) and first char
|
|
for (float digit = 1.f; pDest < pEnd &&
|
|
(val >= digit ||
|
|
pDest - pParamStarts < whole_chars);
|
|
digit *= 10.f) {
|
|
char ch =
|
|
(int) (val / digit) % 10 +
|
|
'0';
|
|
*pDest++ = ch;
|
|
}
|
|
flip(pParamStarts, pDest);
|
|
if (decimal_chars && pDest < pEnd) {
|
|
*pDest++ = '.';
|
|
val += .5f *
|
|
pow(10.f,
|
|
-decimal_chars); // compensating for rounding error
|
|
for (size_t digit = 0;
|
|
pDest < pEnd &&
|
|
digit < decimal_chars;
|
|
digit++) {
|
|
val = (val -
|
|
(int) (val)) *
|
|
10.f;
|
|
*pDest++ =
|
|
(int) (val) + '0';
|
|
}
|
|
}
|
|
}
|
|
} break;
|
|
#endif // PICOFORMAT_HANDLE_FLOATS
|
|
}
|
|
}
|
|
}
|
|
}
|
|
*pDest = '\0';
|
|
return pDest - pStart;
|
|
}
|
|
|
|
int pico_snprintf(char* pDest, size_t cbDest, const char* pFormat, ...)
|
|
{
|
|
va_list vl;
|
|
va_start(vl, pFormat);
|
|
int result = pico_vsnprintf(pDest, cbDest, pFormat, vl);
|
|
va_end(vl);
|
|
return result;
|
|
}
|