/** * @author (c) Julius Pfrommer * 2023, Fraunhofer IOSB, Germany * @author (c) Eyal Rozenberg * 2021-2023, Haifa, Palestine/Israel * @author (c) Marco Paland (info@paland.com) * 2014-2019, PALANDesign Hannover, Germany * * @note Others have made smaller contributions to this file: see the * contributors page at https://github.com/eyalroz/printf/graphs/contributors * or ask one of the authors. The original code for exponential specifiers was * contributed by Martijn Jasperse . * * @brief Small stand-alone implementation of the printf family of functions * (`(v)printf`, `(v)s(n)printf` etc., geared towards use on embedded systems with * limited resources. * * @note the implementations are thread-safe; re-entrant; use no functions from * the standard library; and do not dynamically allocate any memory. * * @license The MIT License (MIT) * * 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 "mp_printf.h" #include "dtoa.h" #include #include // 'ntoa' conversion buffer size, this must be big enough to hold one converted // numeric number including padded zeros (dynamically created on stack) #define PRINTF_INTEGER_BUFFER_SIZE 32 // size of the fixed (on-stack) buffer for printing individual decimal numbers. // this must be big enough to hold one converted floating-point value including // padded zeros. #define PRINTF_DECIMAL_BUFFER_SIZE 32 // Default precision for the floating point conversion specifiers (the C // standard sets this at 6) #define PRINTF_DEFAULT_FLOAT_PRECISION 6 // internal flag definitions #define FLAGS_ZEROPAD (1U << 0U) #define FLAGS_LEFT (1U << 1U) #define FLAGS_PLUS (1U << 2U) #define FLAGS_SPACE (1U << 3U) #define FLAGS_HASH (1U << 4U) #define FLAGS_UPPERCASE (1U << 5U) #define FLAGS_CHAR (1U << 6U) #define FLAGS_SHORT (1U << 7U) // Only used with PRINTF_SUPPORT_MSVC_STYLE_INTEGER_SPECIFIERS #define FLAGS_LONG (1U << 9U) #define FLAGS_LONG_LONG (1U << 10U) #define FLAGS_PRECISION (1U << 11U) #define FLAGS_ADAPT_EXP (1U << 12U) #define FLAGS_POINTER (1U << 13U) // Note: Similar, but not identical, effect as FLAGS_HASH #define FLAGS_SIGNED (1U << 14U) #define BASE_BINARY 2 #define BASE_OCTAL 8 #define BASE_DECIMAL 10 #define BASE_HEX 16 typedef unsigned int printf_flags_t; typedef uint8_t numeric_base_t; typedef unsigned long long printf_unsigned_value_t; typedef long long printf_signed_value_t; // Note in particular the behavior here on LONG_MIN or LLONG_MIN; it is valid // and well-defined, but if you're not careful you can easily trigger undefined // behavior with -LONG_MIN or -LLONG_MIN #define ABS_FOR_PRINTING(_x) \ ((printf_unsigned_value_t)((_x) > 0 ? (_x) : -((printf_signed_value_t)_x))) // internal secure strlen @return The length of the string (excluding the // terminating 0) limited by 'maxsize' @note strlen uses size_t, but wes only // use this function with size_t variables - hence the signature. static size_t strnlen_s_(const char *str, size_t maxsize) { for(size_t i = 0; i < maxsize; i++) { if(!str[i]) return i; } return maxsize; } // internal test if char is a digit (0-9) // @return true if char is a digit static bool is_digit_(char ch) { return (ch >= '0') && (ch <= '9'); } // internal ASCII string to size_t conversion static size_t atou_(const char **str) { size_t i = 0U; while(is_digit_(**str)) { i = i * 10U + (size_t)(*((*str)++) - '0'); } return i; } // Output buffer typedef struct { char *buffer; size_t pos; size_t max_chars; } output_t; static void putchar_(output_t *out, char c) { size_t write_pos = out->pos++; // We're _always_ increasing pos, so as to count how may characters // _would_ have been written if not for the max_chars limitation if(write_pos >= out->max_chars) return; // it must be the case that out->buffer != NULL , due to the constraint // on output_t ; and note we're relying on write_pos being non-negative. out->buffer[write_pos] = c; } static void out_(output_t *out, const char *buf, size_t len) { if(out->pos < out->max_chars) { size_t write_len = len; if(out->pos + len > out->max_chars) write_len = out->max_chars - out->pos; for(size_t i = 0; i < write_len; i++) out->buffer[out->pos + i] = buf[i]; } out->pos += len; // Always increase pos by len } // output the specified string in reverse, taking care of any zero-padding static void out_rev_(output_t *output, const char *buf, size_t len, size_t width, printf_flags_t flags) { const size_t start_pos = output->pos; // pad spaces up to given width if(!(flags & FLAGS_LEFT) && !(flags & FLAGS_ZEROPAD)) { for(size_t i = len; i < width; i++) { putchar_(output, ' '); } } // reverse string while(len) { putchar_(output, buf[--len]); } // append pad spaces up to given width if(flags & FLAGS_LEFT) { while(output->pos - start_pos < width) { putchar_(output, ' '); } } } // Invoked by print_integer after the actual number has been printed, performing // necessary work on the number's prefix (as the number is initially printed in // reverse order) static void print_integer_finalization(output_t *output, char *buf, size_t len, bool negative, numeric_base_t base, size_t precision, size_t width, printf_flags_t flags) { size_t unpadded_len = len; // pad with leading zeros if(!(flags & FLAGS_LEFT)) { if(width && (flags & FLAGS_ZEROPAD) && (negative || (flags & (FLAGS_PLUS | FLAGS_SPACE)))) { width--; } while((flags & FLAGS_ZEROPAD) && (len < width) && (len < PRINTF_INTEGER_BUFFER_SIZE)) { buf[len++] = '0'; } } while((len < precision) && (len < PRINTF_INTEGER_BUFFER_SIZE)) { buf[len++] = '0'; } if(base == BASE_OCTAL && (len > unpadded_len)) { // Since we've written some zeros, we've satisfied the alternative format // leading space requirement flags &= ~FLAGS_HASH; } // handle hash if(flags & (FLAGS_HASH | FLAGS_POINTER)) { if(!(flags & FLAGS_PRECISION) && len && ((len == precision) || (len == width))) { // Let's take back some padding digits to fit in what will eventually be // the format-specific prefix if(unpadded_len < len) { len--; // This should suffice for BASE_OCTAL } if(len && (base == BASE_HEX || base == BASE_BINARY) && (unpadded_len < len)) { len--; // ... and an extra one for 0x or 0b } } if((base == BASE_HEX) && !(flags & FLAGS_UPPERCASE) && (len < PRINTF_INTEGER_BUFFER_SIZE)) { buf[len++] = 'x'; } else if((base == BASE_HEX) && (flags & FLAGS_UPPERCASE) && (len < PRINTF_INTEGER_BUFFER_SIZE)) { buf[len++] = 'X'; } else if((base == BASE_BINARY) && (len < PRINTF_INTEGER_BUFFER_SIZE)) { buf[len++] = 'b'; } if(len < PRINTF_INTEGER_BUFFER_SIZE) { buf[len++] = '0'; } } if(len < PRINTF_INTEGER_BUFFER_SIZE) { if(negative) { buf[len++] = '-'; } else if(flags & FLAGS_PLUS) { buf[len++] = '+'; // ignore the space if the '+' exists } else if(flags & FLAGS_SPACE) { buf[len++] = ' '; } } out_rev_(output, buf, len, width, flags); } // An internal itoa-like function static void print_integer(output_t *output, printf_unsigned_value_t value, bool negative, numeric_base_t base, size_t precision, size_t width, printf_flags_t flags) { char buf[PRINTF_INTEGER_BUFFER_SIZE]; size_t len = 0U; if(!value) { if(!(flags & FLAGS_PRECISION)) { buf[len++] = '0'; flags &= ~FLAGS_HASH; // We drop this flag this since either the alternative and regular modes // of the specifier don't differ on 0 values, or (in the case of octal) // we've already provided the special handling for this mode. } else if(base == BASE_HEX) { flags &= ~FLAGS_HASH; // We drop this flag this since either the alternative and regular modes // of the specifier don't differ on 0 values } } else { do { const char digit = (char)(value % base); buf[len++] = (char)(digit < 10 ? '0' + digit : (flags & FLAGS_UPPERCASE ? 'A' : 'a') + digit - 10); value /= base; } while(value && (len < PRINTF_INTEGER_BUFFER_SIZE)); } print_integer_finalization(output, buf, len, negative, base, precision, width, flags); } static void print_floating_point(output_t *output, double value, size_t precision, size_t width, printf_flags_t flags) { if((flags & FLAGS_PLUS) && value > 0.0) putchar_(output, '+'); // set default precision, if not set explicitly //if(!(flags & FLAGS_PRECISION) || precision > PRINTF_DECIMAL_BUFFER_SIZE - 5) // precision = PRINTF_DEFAULT_FLOAT_PRECISION; char buf[PRINTF_DECIMAL_BUFFER_SIZE]; unsigned len = dtoa(value, buf); // Fill the buffer (TODO: Consider precision) out_(output, buf, len); // Print the buffer } // Advances the format pointer past the flags, and returns the parsed flags // due to the characters passed static printf_flags_t parse_flags(const char **format) { printf_flags_t flags = 0U; do { switch(**format) { case '0': flags |= FLAGS_ZEROPAD; break; case '-': flags |= FLAGS_LEFT; break; case '+': flags |= FLAGS_PLUS; break; case ' ': flags |= FLAGS_SPACE; break; case '#': flags |= FLAGS_HASH; break; default: return flags; } (*format)++; } while(true); } #define ADVANCE_IN_FORMAT_STRING(cptr_) \ do { \ (cptr_)++; \ if(!*(cptr_)) \ return; \ } while(0) static void format_string_loop(output_t *output, const char *format, va_list args) { while(*format) { if(*format != '%') { // A regular content character putchar_(output, *format); format++; continue; } // We're parsing a format specifier: %[flags][width][.precision][length] ADVANCE_IN_FORMAT_STRING(format); printf_flags_t flags = parse_flags(&format); // evaluate width field size_t width = 0U; if(is_digit_(*format)) { width = atou_(&format); } else if(*format == '*') { const int w = va_arg(args, int); if(w < 0) { flags |= FLAGS_LEFT; // reverse padding width = (size_t)-w; } else { width = (size_t)w; } ADVANCE_IN_FORMAT_STRING(format); } // evaluate precision field size_t precision = 0U; if(*format == '.') { flags |= FLAGS_PRECISION; ADVANCE_IN_FORMAT_STRING(format); if(is_digit_(*format)) { precision = atou_(&format); } else if(*format == '*') { const int precision_ = va_arg(args, int); precision = precision_ > 0 ? (size_t)precision_ : 0U; ADVANCE_IN_FORMAT_STRING(format); } } // evaluate length field switch(*format) { case 'l': flags |= FLAGS_LONG; ADVANCE_IN_FORMAT_STRING(format); if(*format == 'l') { flags |= FLAGS_LONG_LONG; ADVANCE_IN_FORMAT_STRING(format); } break; case 'h': flags |= FLAGS_SHORT; ADVANCE_IN_FORMAT_STRING(format); if(*format == 'h') { flags |= FLAGS_CHAR; ADVANCE_IN_FORMAT_STRING(format); } break; case 't': flags |= (sizeof(ptrdiff_t) == sizeof(long) ? FLAGS_LONG : FLAGS_LONG_LONG); ADVANCE_IN_FORMAT_STRING(format); break; case 'j': flags |= (sizeof(intmax_t) == sizeof(long) ? FLAGS_LONG : FLAGS_LONG_LONG); ADVANCE_IN_FORMAT_STRING(format); break; case 'z': flags |= (sizeof(size_t) == sizeof(long) ? FLAGS_LONG : FLAGS_LONG_LONG); ADVANCE_IN_FORMAT_STRING(format); break; default: break; } // evaluate specifier switch(*format) { case 'd': case 'i': case 'u': case 'x': case 'X': case 'o': case 'b': { if(*format == 'd' || *format == 'i') { flags |= FLAGS_SIGNED; } numeric_base_t base; if(*format == 'x' || *format == 'X') { base = BASE_HEX; } else if(*format == 'o') { base = BASE_OCTAL; } else if(*format == 'b') { base = BASE_BINARY; } else { base = BASE_DECIMAL; flags &= ~FLAGS_HASH; // decimal integers have no alternative presentation } if(*format == 'X') { flags |= FLAGS_UPPERCASE; } format++; // ignore '0' flag when precision is given if(flags & FLAGS_PRECISION) { flags &= ~FLAGS_ZEROPAD; } if(flags & FLAGS_SIGNED) { // A signed specifier: d, i or possibly I + bit size if enabled if(flags & FLAGS_LONG_LONG) { const long long value = va_arg(args, long long); print_integer(output, ABS_FOR_PRINTING(value), value < 0, base, precision, width, flags); } else if(flags & FLAGS_LONG) { const long value = va_arg(args, long); print_integer(output, ABS_FOR_PRINTING(value), value < 0, base, precision, width, flags); } else { // We never try to interpret the argument as something // potentially-smaller than int, due to integer promotion rules: // Even if the user passed a short int, short unsigned etc. - // these will come in after promotion, as int's (or unsigned for // the case of short unsigned when it has the same size as int) const int value = (flags & FLAGS_CHAR) ? (signed char)va_arg(args, int) : (flags & FLAGS_SHORT) ? (short int)va_arg(args, int) : va_arg(args, int); print_integer(output, ABS_FOR_PRINTING(value), value < 0, base, precision, width, flags); } } else { // An unsigned specifier: u, x, X, o, b flags &= ~(FLAGS_PLUS | FLAGS_SPACE); if(flags & FLAGS_LONG_LONG) { print_integer(output, (printf_unsigned_value_t) va_arg(args, unsigned long long), false, base, precision, width, flags); } else if(flags & FLAGS_LONG) { print_integer(output, (printf_unsigned_value_t) va_arg(args, unsigned long), false, base, precision, width, flags); } else { const unsigned int value = (flags & FLAGS_CHAR) ? (unsigned char)va_arg(args, unsigned int) : (flags & FLAGS_SHORT) ? (unsigned short int)va_arg(args, unsigned int) : va_arg(args, unsigned int); print_integer(output, (printf_unsigned_value_t)value, false, base, precision, width, flags); } } break; } case 'f': case 'F': if(*format == 'F') flags |= FLAGS_UPPERCASE; print_floating_point(output, (double)va_arg(args, double), precision, width, flags); format++; break; case 'c': { size_t l = 1U; // pre padding if(!(flags & FLAGS_LEFT)) { while(l++ < width) { putchar_(output, ' '); } } // char output putchar_(output, (char)va_arg(args, int)); // post padding if(flags & FLAGS_LEFT) { while(l++ < width) { putchar_(output, ' '); } } format++; break; } case 's': { const char *p = va_arg(args, char *); if(p == NULL) { out_rev_(output, ")llun(", 6, width, flags); } else { // string length size_t l = strnlen_s_(p, precision ? precision : INT32_MAX); if(flags & FLAGS_PRECISION) { l = (l < precision ? l : precision); } // pre padding if(!(flags & FLAGS_LEFT)) { for(size_t i = 0; l + i < width; i++) { putchar_(output, ' '); } } // string output out_(output, p, l); // post padding if(flags & FLAGS_LEFT) { for(size_t i = 0; l + i < width; i++) { putchar_(output, ' '); } } } format++; break; } case 'p': { width = sizeof(void *) * 2U + 2; // 2 hex chars per byte + the "0x" prefix flags |= FLAGS_ZEROPAD | FLAGS_POINTER; uintptr_t value = (uintptr_t)va_arg(args, void *); (value == (uintptr_t)NULL) ? out_rev_(output, ")lin(", 5, width, flags) : print_integer(output, (printf_unsigned_value_t)value, false, BASE_HEX, precision, width, flags); format++; break; } case '%': putchar_(output, '%'); format++; break; default: putchar_(output, *format); format++; break; } } } int mp_vsnprintf(char *s, size_t n, const char *format, va_list arg) { // Check that the inputs are sane if(!s || n < 1) return -1; // Format the string output_t out = {s, 0, n}; format_string_loop(&out, format, arg); // Write the string-terminating '\0' character size_t null_char_pos = out.pos < out.max_chars ? out.pos : out.max_chars - 1; out.buffer[null_char_pos] = '\0'; // Return written chars without terminating \0 return (int)out.pos; } int mp_snprintf(char *s, size_t n, const char *format, ...) { va_list args; va_start(args, format); const int ret = mp_vsnprintf(s, n, format, args); va_end(args); return ret; }