From 4908a60a58023f03c253fc54536c56319eb1d4f2 Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Thu, 26 Feb 2026 09:26:10 -0700 Subject: [PATCH 01/21] Fix the qprintf() % bug (with changes copied from the apos_autoscale branch). Fix a bug in qprintf() that caused % and & characters inside conditional printing areas to always print, regardless of the condition. Improve documentation for qpfPrintf_va_internal() and qpf_grow_fn_t(). Clean up. --- centrallix-lib/include/qprintf.h | 9 +++++++++ centrallix-lib/src/qprintf.c | 20 ++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/centrallix-lib/include/qprintf.h b/centrallix-lib/include/qprintf.h index d638ef684..c7edbf2fc 100644 --- a/centrallix-lib/include/qprintf.h +++ b/centrallix-lib/include/qprintf.h @@ -32,6 +32,15 @@ #include +/*** A function to grow a string buffer. + *** + *** @param str The string buffer being grown. + *** @param size A pointer to the current size of the string buffer. + *** @param offset An offset up to which data must be preserved. + *** @param args Arguments for growing the buffer. + *** @param req The requested size. + ***/ +// typedef int (*qpf_grow_fn_t)(char** str, size_t* size, size_t offset, void* args, size_t req); typedef int (*qpf_grow_fn_t)(char**, size_t*, size_t, void*, size_t); typedef struct _QPS diff --git a/centrallix-lib/src/qprintf.c b/centrallix-lib/src/qprintf.c index 6827cc358..af1b16212 100644 --- a/centrallix-lib/src/qprintf.c +++ b/centrallix-lib/src/qprintf.c @@ -877,6 +877,14 @@ qpf_internal_Translate(pQPSession s, const char* srcbuf, size_t srcsize, char** *** change out from under this function to a new buffer if a realloc is *** done by the grow_fn function. Do not store pointers to 'str'. Go *** solely by offsets. + *** + *** NULL, &(s->Tmpbuf), &(s->TmpbufSize), htr_internal_GrowFn, (void*)s, fmt, va + *** @param s Optional session struct. + *** @param str Pointer to a string buffer where data will be written. + *** @param size Pointer to the current size of the string buffer. + *** @param grow_fn A function to grow the string buffer. + *** @param format The format of data which should be written. + *** @param ap The arguments list to fulfill the provided format. ***/ int qpfPrintf_va_internal(pQPSession s, char** str, size_t* size, qpf_grow_fn_t grow_fn, void* grow_arg, const char* format, va_list ap) @@ -972,6 +980,12 @@ qpfPrintf_va_internal(pQPSession s, char** str, size_t* size, qpf_grow_fn_t grow /** Simple specifiers **/ if (UNLIKELY(format[0] == '%')) { + if (ignore) + { + format++; + continue; + } + if (LIKELY(!nogrow) && (LIKELY(cpoffset+2 <= *size) || (grow_fn(str, size, cpoffset, grow_arg, cpoffset+2)))) (*str)[cpoffset++] = '%'; else @@ -984,6 +998,12 @@ qpfPrintf_va_internal(pQPSession s, char** str, size_t* size, qpf_grow_fn_t grow } else if (UNLIKELY(format[0] == '&')) { + if (ignore) + { + format++; + continue; + } + if (LIKELY(!nogrow) && (LIKELY(cpoffset+2 <= *size) || (grow_fn(str, size, cpoffset, grow_arg, cpoffset+2)))) (*str)[cpoffset++] = '&'; else From a0441676e1215d1c7cb02ca7c8281a798f52aec6 Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Mon, 6 Apr 2026 12:14:49 -0600 Subject: [PATCH 02/21] Improve documentation of qprintf(). Reorder some code to improve clarity. Add various comments. Fix spacing mistakes. Add void to function signatures that do not take parameters. --- centrallix-lib/include/qprintf.h | 2 +- centrallix-lib/src/qprintf.c | 379 ++++++++++++++++++++----------- 2 files changed, 246 insertions(+), 135 deletions(-) diff --git a/centrallix-lib/include/qprintf.h b/centrallix-lib/include/qprintf.h index c7edbf2fc..0b85412f4 100644 --- a/centrallix-lib/include/qprintf.h +++ b/centrallix-lib/include/qprintf.h @@ -67,7 +67,7 @@ typedef struct _QPS #define QPERR(x) (s->Errors |= (x)) /*** QPrintf methods ***/ -pQPSession qpfOpenSession(); +pQPSession qpfOpenSession(void); int qpfCloseSession(pQPSession s); int qpfClearErrors(pQPSession s); unsigned int qpfErrors(pQPSession s); diff --git a/centrallix-lib/src/qprintf.c b/centrallix-lib/src/qprintf.c index af1b16212..1702309cc 100644 --- a/centrallix-lib/src/qprintf.c +++ b/centrallix-lib/src/qprintf.c @@ -10,6 +10,7 @@ #include #include #include + #include "qprintf.h" #include "mtask.h" #include "newmalloc.h" @@ -56,7 +57,7 @@ #define QPF_SPEC_T_DBL (4) #define QPF_SPEC_T_NSTR (5) #define QPF_SPEC_T_CHR (6) -#define QPF_SPEC_T_LL (7) +#define QPF_SPEC_T_LL (7) #define QPF_SPEC_T_ENDSRC (7) /*** builtin filtering specifiers ***/ @@ -105,42 +106,42 @@ qpf_spec_names[] = { NULL, /* 0 */ "INT", /* 1 */ - "STR", - "POS", - "DBL", - "nSTR", - "CHR", - "LL", - "QUOT", - "DQUOT", - "SYM", - "JSSTR", - "nLEN", - "WS", - "ESCWS", - "ESCSP", - "UNESC", - "SSYB", - "DSYB", - "FILE", - "PATH", - "HEX", - "DHEX", - "B64", - "DB64", - "RF", - "RR", - "HTENLBR", - "DHTE", - "URL", - "DURL", - "nLSET", - "nRSET", - "nZRSET", - "SQLARG", - "SQLSYM", + "STR", /* 2 */ + "POS", /* 3 */ + "DBL", /* 4 */ + "nSTR", /* 5 */ + "CHR", /* 6 */ + "LL", /* 7 */ + "QUOT", /* 8 */ + "DQUOT", /* 9 */ + "SYM", /* 10 */ + "JSSTR", /* 11 */ + "nLEN", /* 12 */ + "WS", /* 13 */ + "ESCWS", /* 14 */ + "ESCSP", /* 15 */ + "UNESC", /* 16 */ + "SSYB", /* 17 */ + "DSYB", /* 18 */ + "FILE", /* 19 */ + "PATH", /* 20 */ + "HEX", /* 21 */ + "DHEX", /* 22 */ + "B64", /* 23 */ + "DB64", /* 24 */ + "RF", /* 25 */ + "RR", /* 26 */ + "HTENLBR", /* 27 */ + "DHTE", /* 28 */ + "URL", /* 29 */ + "DURL", /* 30 */ + "nLSET", /* 31 */ + "nRSET", /* 32 */ + "nZRSET", /* 33 */ + "SQLARG", /* 34 */ + "SQLSYM", /* 35 */ "HTDATA", /* 36 */ - "HTE", + "HTE", /* 37 */ "ESCQWS", /* 38 */ "ESCQ", /* 39 */ "CSSVAL", /* 40 */ @@ -166,24 +167,38 @@ typedef struct char* ext_specs[QPF_MAX_EXTS]; int (*ext_fns[QPF_MAX_EXTS])(); char is_source[QPF_MAX_EXTS]; - QPConvTable quote_matrix; - QPConvTable quote_ws_matrix; - QPConvTable ws_matrix; - QPConvTable hte_matrix; - QPConvTable htenlbr_matrix; - QPConvTable hex_matrix; - QPConvTable url_matrix; - QPConvTable jsstr_matrix; - QPConvTable jsonstr_matrix; - QPConvTable cssval_matrix; - QPConvTable cssurl_matrix; - QPConvTable dsyb_matrix; - QPConvTable ssyb_matrix; + + /** Conversion matrices for various tasks. **/ + QPConvTable quote_matrix; /* Escapes quotes. */ + QPConvTable quote_ws_matrix; /* Escapes quotes & whitespace. */ + QPConvTable ws_matrix; /* Escapes whitespace. */ + QPConvTable hte_matrix; /* Escapes characters for HTML. */ + QPConvTable htenlbr_matrix; /* Escapes characters and line breaks for HTML. */ + QPConvTable cssval_matrix; /* Escapes CSS values. */ + QPConvTable cssurl_matrix; /* Escapes CSS URLs. */ + QPConvTable jsstr_matrix; /* Escapes JavaScript strings. */ + QPConvTable jsonstr_matrix; /* Escapes JSON strings. */ + QPConvTable hex_matrix; /* Encodes strings into HEX. */ + QPConvTable url_matrix; /* Encodes URLs. */ + QPConvTable dsyb_matrix; /* Escapes double quotes sybase-style: " -> "" */ + QPConvTable ssyb_matrix; /* Escapes single quotes sybase-style: ' -> '' */ } QPF_t; static QPF_t QPF = { n_ext:0, is_init:0 }; +/*** Searches for a substring within a buffer. + *** Uses an optimized two-step approach: first locate the first character + *** using `memchr()`, then verify the full match with `memcmp()`. + *** + *** @param haystack The buffer to search. + *** @param haystacklen The length of the haystack buffer (in bytes). + *** @param needle The substring for which to search. + *** @param needlelen The length of the needle substring (in bytes). + *** @return The byte offset to the first instance of `needle` in `haystack`, + *** or -1 if not found (including when needlelen exceeds haystacklen). + *** Returns 0 if needlelen is 0 (empty needle matches at haystack). + ***/ int qpf_internal_FindStr(const char* haystack, size_t haystacklen, const char* needle, size_t needlelen) { @@ -191,6 +206,10 @@ qpf_internal_FindStr(const char* haystack, size_t haystacklen, const char* needl char* ptr; if (needlelen > haystacklen) return -1; if (needlelen == 0) return 0; + + /** Edge cases. **/ + + /** Search for the needle. **/ pos = 0; while(pos <= haystacklen - needlelen) { @@ -200,10 +219,17 @@ qpf_internal_FindStr(const char* haystack, size_t haystacklen, const char* needl return (ptr - haystack); pos = (ptr - haystack) + 1; } + + /** Not found. **/ return -1; } - +/*** Set up data in a conversion matrix (aka. table) data structure, called + *** after the matrix values have been set. + *** + *** @param table The conversion matrix table data structure to set up. + *** @returns 0 if successful, or -1 if an error occurs. + ***/ int qpf_internal_SetupTable(pQPConvTable table) { @@ -226,21 +252,25 @@ qpf_internal_SetupTable(pQPConvTable table) } -/*** qpfInitialize() - inits the QPF suite. +/*** Initialize internal data structures for the QPF module. + *** + *** @returns 0 if successful, or -1 if an error occurs. ***/ int -qpfInitialize() +qpfInitialize(void) { int i; char buf[4]; char hex[] = "0123456789abcdef"; + /** Initialize matrix that: Escapes quotes. **/ memset(&QPF.quote_matrix, 0, sizeof(QPF.quote_matrix)); QPF.quote_matrix.Matrix['\''] = "\\'"; QPF.quote_matrix.Matrix['"'] = "\\\""; QPF.quote_matrix.Matrix['\\'] = "\\\\"; qpf_internal_SetupTable(&QPF.quote_matrix); + /** Initialize matrix that: Escapes quotes & whitespace. **/ memset(&QPF.quote_ws_matrix, 0, sizeof(QPF.quote_ws_matrix)); QPF.quote_ws_matrix.Matrix['\''] = "\\'"; QPF.quote_ws_matrix.Matrix['"'] = "\\\""; @@ -250,58 +280,14 @@ qpfInitialize() QPF.quote_ws_matrix.Matrix['\r'] = "\\r"; qpf_internal_SetupTable(&QPF.quote_ws_matrix); - memset(&QPF.jsstr_matrix, 0, sizeof(QPF.jsstr_matrix)); - QPF.jsstr_matrix.Matrix['\''] = "\\'"; - QPF.jsstr_matrix.Matrix['"'] = "\\\""; - QPF.jsstr_matrix.Matrix['\\'] = "\\\\"; - QPF.jsstr_matrix.Matrix['/'] = "\\/"; - QPF.jsstr_matrix.Matrix['\n'] = "\\n"; - QPF.jsstr_matrix.Matrix['\t'] = "\\t"; - QPF.jsstr_matrix.Matrix['\r'] = "\\r"; - QPF.jsstr_matrix.Matrix['\b'] = "\\b"; - QPF.jsstr_matrix.Matrix['\f'] = "\\f"; - for(i=0;i<=31;i++) - { - if (!QPF.jsstr_matrix.Matrix[i]) - { - QPF.jsstr_matrix.Matrix[i] = nmSysMalloc(7); - snprintf(QPF.jsstr_matrix.Matrix[i], 7, "\\u%4.4X", i); - } - } - qpf_internal_SetupTable(&QPF.jsstr_matrix); - - memset(&QPF.jsonstr_matrix, 0, sizeof(QPF.jsonstr_matrix)); - QPF.jsonstr_matrix.Matrix['"'] = "\\\""; - QPF.jsonstr_matrix.Matrix['\\'] = "\\\\"; - QPF.jsonstr_matrix.Matrix['\n'] = "\\n"; - QPF.jsonstr_matrix.Matrix['\t'] = "\\t"; - QPF.jsonstr_matrix.Matrix['\r'] = "\\r"; - QPF.jsonstr_matrix.Matrix['\b'] = "\\b"; - QPF.jsonstr_matrix.Matrix['\f'] = "\\f"; - for(i=0;i<=31;i++) - { - if (!QPF.jsonstr_matrix.Matrix[i]) - { - QPF.jsonstr_matrix.Matrix[i] = nmSysMalloc(7); - snprintf(QPF.jsonstr_matrix.Matrix[i], 7, "\\u%4.4X", i); - } - } - qpf_internal_SetupTable(&QPF.jsonstr_matrix); - + /** Initialize matrix that: Escapes whitespace. **/ memset(&QPF.ws_matrix, 0, sizeof(QPF.ws_matrix)); QPF.ws_matrix.Matrix['\n'] = "\\n"; QPF.ws_matrix.Matrix['\t'] = "\\t"; QPF.ws_matrix.Matrix['\r'] = "\\r"; qpf_internal_SetupTable(&QPF.ws_matrix); - memset(&QPF.dsyb_matrix, 0, sizeof(QPF.dsyb_matrix)); - QPF.dsyb_matrix.Matrix['"'] = "\"\""; - qpf_internal_SetupTable(&QPF.dsyb_matrix); - - memset(&QPF.ssyb_matrix, 0, sizeof(QPF.ssyb_matrix)); - QPF.ssyb_matrix.Matrix['\''] = "''"; - qpf_internal_SetupTable(&QPF.ssyb_matrix); - + /** Initialize matrix that: Escapes characters for HTML. **/ memset(&QPF.hte_matrix, 0, sizeof(QPF.hte_matrix)); QPF.hte_matrix.Matrix['<'] = "<"; QPF.hte_matrix.Matrix['>'] = ">"; @@ -313,6 +299,7 @@ qpfInitialize() QPF.hte_matrix.Matrix['\0'] = "�"; qpf_internal_SetupTable(&QPF.hte_matrix); + /** Initialize matrix that: Escapes characters and line breaks for HTML. **/ memset(&QPF.htenlbr_matrix, 0, sizeof(QPF.htenlbr_matrix)); QPF.htenlbr_matrix.Matrix['<'] = "<"; QPF.htenlbr_matrix.Matrix['>'] = ">"; @@ -325,6 +312,7 @@ qpfInitialize() QPF.htenlbr_matrix.Matrix['\n'] = "
"; qpf_internal_SetupTable(&QPF.htenlbr_matrix); + /** Initialize matrix that: Escapes CSS values. **/ memset(&QPF.cssval_matrix, 0, sizeof(QPF.cssval_matrix)); QPF.cssval_matrix.Matrix[';'] = "\\;"; QPF.cssval_matrix.Matrix['}'] = "\\}"; @@ -337,6 +325,7 @@ qpfInitialize() QPF.cssval_matrix.Matrix['\''] = "\\'"; qpf_internal_SetupTable(&QPF.cssval_matrix); + /** Initialize matrix that: Escapes CSS URLs. **/ memset(&QPF.cssurl_matrix, 0, sizeof(QPF.cssurl_matrix)); QPF.cssurl_matrix.Matrix[';'] = "\\;"; QPF.cssurl_matrix.Matrix['}'] = "\\}"; @@ -356,6 +345,47 @@ qpfInitialize() QPF.cssurl_matrix.Matrix['\r'] = "\\\r"; qpf_internal_SetupTable(&QPF.cssurl_matrix); + /** Initialize matrix that: Escapes JavaScript strings. **/ + memset(&QPF.jsstr_matrix, 0, sizeof(QPF.jsstr_matrix)); + QPF.jsstr_matrix.Matrix['\''] = "\\'"; + QPF.jsstr_matrix.Matrix['"'] = "\\\""; + QPF.jsstr_matrix.Matrix['\\'] = "\\\\"; + QPF.jsstr_matrix.Matrix['/'] = "\\/"; + QPF.jsstr_matrix.Matrix['\n'] = "\\n"; + QPF.jsstr_matrix.Matrix['\t'] = "\\t"; + QPF.jsstr_matrix.Matrix['\r'] = "\\r"; + QPF.jsstr_matrix.Matrix['\b'] = "\\b"; + QPF.jsstr_matrix.Matrix['\f'] = "\\f"; + for(i=0;i<=31;i++) + { + if (!QPF.jsstr_matrix.Matrix[i]) + { + QPF.jsstr_matrix.Matrix[i] = nmSysMalloc(7); + snprintf(QPF.jsstr_matrix.Matrix[i], 7, "\\u%4.4X", i); + } + } + qpf_internal_SetupTable(&QPF.jsstr_matrix); + + /** Initialize matrix that: Escapes JSON strings. **/ + memset(&QPF.jsonstr_matrix, 0, sizeof(QPF.jsonstr_matrix)); + QPF.jsonstr_matrix.Matrix['"'] = "\\\""; + QPF.jsonstr_matrix.Matrix['\\'] = "\\\\"; + QPF.jsonstr_matrix.Matrix['\n'] = "\\n"; + QPF.jsonstr_matrix.Matrix['\t'] = "\\t"; + QPF.jsonstr_matrix.Matrix['\r'] = "\\r"; + QPF.jsonstr_matrix.Matrix['\b'] = "\\b"; + QPF.jsonstr_matrix.Matrix['\f'] = "\\f"; + for(i=0;i<=31;i++) + { + if (!QPF.jsonstr_matrix.Matrix[i]) + { + QPF.jsonstr_matrix.Matrix[i] = nmSysMalloc(7); + snprintf(QPF.jsonstr_matrix.Matrix[i], 7, "\\u%4.4X", i); + } + } + qpf_internal_SetupTable(&QPF.jsonstr_matrix); + + /** Initialize matrix that: Encodes strings into HEX. **/ for(i=0;i>4)&0x0F]; @@ -365,7 +395,7 @@ qpfInitialize() } qpf_internal_SetupTable(&QPF.hex_matrix); - /* set up table for url encoding everything except 0-9, A-Z, and a-z */ + /** Initialize matrix that: Encodes URLs (everything except 0-9, A-Z, and a-z). **/ memset(&QPF.url_matrix, 0, sizeof(QPF.url_matrix)); for(i=0;i<48;i++) /* escape until 0-9 */ { @@ -401,25 +431,38 @@ qpfInitialize() } qpf_internal_SetupTable(&QPF.url_matrix); - + /** Initialize matrix that: Escapes double quotes sybase-style: " -> "". **/ + memset(&QPF.dsyb_matrix, 0, sizeof(QPF.dsyb_matrix)); + QPF.dsyb_matrix.Matrix['"'] = "\"\""; + qpf_internal_SetupTable(&QPF.dsyb_matrix); + + /** Initialize matrix that: Escapes single quotes sybase-style: ' -> ''. **/ + memset(&QPF.ssyb_matrix, 0, sizeof(QPF.ssyb_matrix)); + QPF.ssyb_matrix.Matrix['\''] = "''"; + qpf_internal_SetupTable(&QPF.ssyb_matrix); + + /** Initialize the qpf_spec_len array. **/ for(i=0;i<=QPF_SPEC_T_MAXSPEC;i++) { if (qpf_spec_names[i]) qpf_spec_len[i] = strlen(qpf_spec_names[i]); } + /** Done. **/ QPF.is_init = 1; return 0; } -/*** qpfOpenSession() - open a new qprintf session. The session is used for - *** storing cumulative error information, to make error handling much cleaner - *** for any caller that wants to do so. +/*** Open a new qprintf session. + *** The session is used for storing cumulative error information which makes + *** error handling much cleaner for callers that use this feature. + *** + *** @returns A new, initialized pQPSession object, or NULL if an error occurs. ***/ pQPSession -qpfOpenSession() +qpfOpenSession(void) { pQPSession s = NULL; @@ -436,7 +479,12 @@ qpfOpenSession() } -/*** qpfErrors() - return the current error mask. +/*** Queries the current error mask, indicating errors that have occurred since + *** when the session was initialized or the last time that `qpfClearErrors()` + *** was called. + *** + *** @param s The session to be queried. + *** @returns The current error mask (does not fail). ***/ unsigned int qpfErrors(pQPSession s) @@ -445,7 +493,10 @@ qpfErrors(pQPSession s) } -/*** qpfClearErrors() - reset the errors mask. +/*** Reset the errors mask, clearing all errors that have occurred. + *** + *** @param s The session holding the error mask to be cleared. + *** @returns 0 if successful, or -1 if an error occurs. ***/ int qpfClearErrors(pQPSession s) @@ -455,7 +506,10 @@ qpfClearErrors(pQPSession s) } -/*** qpfCloseSession() - close a qprintf session. +/*** Closes a qprintf session and deallocates associated resources. + *** + *** @param s The session to close. + *** @returns 0 if successful, or -1 if an error occurs. ***/ int qpfCloseSession(pQPSession s) @@ -467,9 +521,12 @@ qpfCloseSession(pQPSession s) } -/*** qpf_internal_itoa() - convert an integer into a string representation. - *** this seems to perform better than snprintf(%d), even without - *** optimization enabled. +/*** Convert an integer into a string representation. Seems to perform better + *** than `snprintf("%d")`, even without optimization enabled. + *** + *** @param dst The destination string buffer. + *** @param dstlen The allocated length of the string buffer, used to avoid + *** buffer overflows. ***/ static inline int qpf_internal_itoa(char* dst, size_t dstlen, int i) @@ -504,7 +561,21 @@ qpf_internal_itoa(char* dst, size_t dstlen, int i) } -/*** qpf_internal_base64encode() - convert string to base 64 representation +/*** Convert string to its base 64 representation. + *** This function is the reverse of `qpf_internal_base64decode()`. + *** + *** @param s The qprintf session in use. + *** @param src The source string to convert. + *** @param src_size The length of `src` in bytes. + *** @param dst A pointer to a location where the resulting string pointer + *** should be saved. + *** @param dst_size The size of the currently allocated string at `dst`. + *** @param dst_offset The number of bytes to skip at the start of `dst` + *** before writing the result. + *** @param grow_fn An optional grow function, used to grow the dst string if + *** more space is needed. + *** @param grow_arg The argument to be passed to the grow function. + *** @returns The number of bytes written. ***/ int qpf_internal_base64encode(pQPSession s, const char* src, size_t src_size, char** dst, size_t* dst_size, size_t* dst_offset, qpf_grow_fn_t grow_fn, void* grow_arg) @@ -570,7 +641,22 @@ qpf_internal_base64encode(pQPSession s, const char* src, size_t src_size, char** } -/*** qpf_internal_base64decode() - convert base 64 to a string representation +/*** Convert the base 64 representation of a string back to the original + *** string. + *** This function is the reverse of `qpf_internal_base64encode()`. + *** + *** @param s The qprintf session in use. + *** @param src The source string representation to convert. + *** @param src_size The length of `src` in bytes. + *** @param dst A pointer to a location where the resulting string pointer + *** should be saved. + *** @param dst_size The size of the currently allocated string at `dst`. + *** @param dst_offset The number of bytes to skip at the start of `dst` + *** before writing the result. + *** @param grow_fn An optional grow function, used to grow the dst string if + *** more space is needed. + *** @param grow_arg The argument to be passed to the grow function. + *** @returns The number of bytes written. ***/ static inline int qpf_internal_base64decode(pQPSession s, const char* src, size_t src_size, char** dst, size_t* dst_size, size_t* dst_offset, qpf_grow_fn_t grow_fn, void* grow_arg) @@ -664,7 +750,21 @@ qpf_internal_base64decode(pQPSession s, const char* src, size_t src_size, char** } -/*** qpf_internal_hexdecode() - convert base 64 to a string representation +/*** Convert the base 16 (hex) representation of a string back to the + *** original string. + *** + *** @param s The qprintf session in use. + *** @param src The source string representation to convert. + *** @param src_size The length of `src` in bytes. + *** @param dst A pointer to a location where the resulting string pointer + *** should be saved. + *** @param dst_size The size of the currently allocated string at `dst`. + *** @param dst_offset The number of bytes to skip at the start of `dst` + *** before writing the result. + *** @param grow_fn An optional grow function, used to grow the dst string if + *** more space is needed. + *** @param grow_arg The argument to be passed to the grow function. + *** @returns The number of bytes written. ***/ static inline int qpf_internal_hexdecode(pQPSession s, const char* src, size_t src_size, char** dst, size_t* dst_size, size_t* dst_offset, qpf_grow_fn_t grow_fn, void* grow_arg) @@ -730,6 +830,16 @@ qpf_internal_hexdecode(pQPSession s, const char* src, size_t src_size, char** ds } +/*** Returns the amount of additional size that will fit, but does not + *** reallocate the string to grow it to a larger size. + ***/ +int +qpfPrintf_grow(char** str, size_t* size, size_t offs, void* arg, size_t req_size) + { + return (*size) >= req_size; + } + + /*** qpfPrintf() - do the quoting printf operation, given a standard vararg *** function call. ***/ @@ -748,15 +858,6 @@ qpfPrintf(pQPSession s, char* str, size_t size, const char* format, ...) } -/*** qpfPrintf_grow() - returns whether the additional size will fit. - ***/ -int -qpfPrintf_grow(char** str, size_t* size, size_t offs, void* arg, size_t req_size) - { - return (*size) >= req_size; - } - - /*** qpfPrintf_va() - same as qpfPrintf(), but takes a va_list instead of *** a list of arguments. ***/ @@ -870,13 +971,12 @@ qpf_internal_Translate(pQPSession s, const char* srcbuf, size_t srcsize, char** } -/*** qpfPrintf_va_internal() - does all of the guts work of the qpfPrintf - *** family of functions. - *** - *** A warning to those who would modify this: the 'str' parameter may - *** change out from under this function to a new buffer if a realloc is - *** done by the grow_fn function. Do not store pointers to 'str'. Go - *** solely by offsets. +/*** Parse the provided format to apply all of the qprintf() format specifier + *** rules while printing the indicated characters. + *** + *** Warning: The 'str' parameter may change during the execution of this + *** function if grow_fn reallocates it. Do not store pointers to 'str'! + *** Instead, use offsets. *** *** NULL, &(s->Tmpbuf), &(s->TmpbufSize), htr_internal_GrowFn, (void*)s, fmt, va *** @param s Optional session struct. @@ -1443,21 +1543,32 @@ qpfPrintf_va_internal(pQPSession s, char** str, size_t* size, qpf_grow_fn_t grow } -/*** qpfRegisterExt() - registers an extension with the QPF module, allowing - *** outside modules to provide extra specifiers for processing data or for - *** data sources. If is_source is nonzero, the extension is treated as a - *** source specifier, otherwise it is a filtering specifier. +/*** Registers a new extension with the QPF module. This allows outside + *** modules to provide extra specifiers for processing data or for data + *** sources. + *** + *** Warning: It appears that extensions are not currently implemented. + *** TODO: This comment should document the expected signature for ext_fn() + *** more clearly once it is used somewhere. + *** + *** @param ext_spec The name of the extension (e.g. "STR" for `%str`). + *** @param ext_fn The function to call on data that meets the extension. + *** @param is_source If nonzero, the extension is treated as a source + *** specifier (also known as a format specifier, e.g. `%STR`), otherwise + *** it is a treated as a filtering specifier (e.g. `"`). ***/ void qpfRegisterExt(char* ext_spec, int (*ext_fn)(), int is_source) { - /** Add to list of extensions **/ + /** Check if extension max has been reached. **/ if (QPF.n_ext >= QPF_MAX_EXTS) { fprintf(stderr, "warning: qpfRegisterExt: QPF_MAX_EXTS exceeded\n"); return; } + + /** Add to list of extensions **/ QPF.ext_specs[QPF.n_ext] = nmSysStrdup(ext_spec); QPF.ext_fns[QPF.n_ext] = ext_fn; QPF.is_source[QPF.n_ext] = is_source?1:0; From a5ff3ab6a66b1404b780f0793db41a94e98a79de Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Mon, 6 Apr 2026 12:33:17 -0600 Subject: [PATCH 03/21] Add LIKELY() and UNLIKELY() where it is obvious. Clean up some overly cleaver code. --- centrallix-lib/src/qprintf.c | 95 +++++++++++++++++++----------------- 1 file changed, 49 insertions(+), 46 deletions(-) diff --git a/centrallix-lib/src/qprintf.c b/centrallix-lib/src/qprintf.c index 1702309cc..0d0ed9f37 100644 --- a/centrallix-lib/src/qprintf.c +++ b/centrallix-lib/src/qprintf.c @@ -204,10 +204,10 @@ qpf_internal_FindStr(const char* haystack, size_t haystacklen, const char* needl { int pos; char* ptr; - if (needlelen > haystacklen) return -1; - if (needlelen == 0) return 0; /** Edge cases. **/ + if (UNLIKELY(needlelen > haystacklen)) return -1; + if (UNLIKELY(needlelen == 0)) return 0; /** Search for the needle. **/ pos = 0; @@ -466,13 +466,13 @@ qpfOpenSession(void) { pQPSession s = NULL; - if (!QPF.is_init) + if (UNLIKELY(!QPF.is_init)) { - if (qpfInitialize() < 0) return NULL; + if (UNLIKELY(qpfInitialize() < 0)) return NULL; } s = (pQPSession)nmMalloc(sizeof(QPSession)); - if (!s) return NULL; + if (UNLIKELY(!s)) return NULL; s->Errors = 0; return s; @@ -668,7 +668,7 @@ qpf_internal_base64decode(pQPSession s, const char* src, size_t src_size, char** int req_size = (.75 * src_size) + *dst_offset + 1; /** fmul could truncate when cast to int hence +1 **/ /** Verify source data is correct length for base 64 **/ - if (src_size % 4 != 0) + if (UNLIKELY(src_size % 4 != 0)) { QPERR(QPF_ERR_T_BADCHAR); return -1; @@ -691,7 +691,7 @@ qpf_internal_base64decode(pQPSession s, const char* src, size_t src_size, char** { /** First 6 bits. **/ ptr = strchr(b64,src[0]); - if (!ptr || !*ptr) + if (UNLIKELY(!ptr || !*ptr)) { QPERR(QPF_ERR_T_BADCHAR); return -1; @@ -701,7 +701,7 @@ qpf_internal_base64decode(pQPSession s, const char* src, size_t src_size, char** /** Second six bits are split between cursor[0] and cursor[1] **/ ptr = strchr(b64,src[1]); - if (!ptr || !*ptr) + if (UNLIKELY(!ptr || !*ptr)) { QPERR(QPF_ERR_T_BADCHAR); return -1; @@ -717,7 +717,7 @@ qpf_internal_base64decode(pQPSession s, const char* src, size_t src_size, char** break; } ptr = strchr(b64,src[2]); - if (!ptr || !*ptr) + if (UNLIKELY(!ptr || !*ptr)) { QPERR(QPF_ERR_T_BADCHAR); return -1; @@ -733,7 +733,7 @@ qpf_internal_base64decode(pQPSession s, const char* src, size_t src_size, char** break; } ptr = strchr(b64,src[3]); - if (!ptr || !*ptr) + if (UNLIKELY(!ptr || !*ptr)) { QPERR(QPF_ERR_T_BADCHAR); return -1; @@ -778,7 +778,7 @@ qpf_internal_hexdecode(pQPSession s, const char* src, size_t src_size, char** ds char* orig_src = src; /** Required size **/ - if (src_size%2 == 1) + if (UNLIKELY(src_size%2 == 1)) { QPERR(QPF_ERR_T_BADLENGTH); return -1; @@ -802,7 +802,7 @@ qpf_internal_hexdecode(pQPSession s, const char* src, size_t src_size, char** ds { /** First 4 bits. **/ ptr = strchr(hex, src[0]); - if (!ptr) + if (UNLIKELY(!ptr)) { QPERR(QPF_ERR_T_BADCHAR); return -1; @@ -812,7 +812,7 @@ qpf_internal_hexdecode(pQPSession s, const char* src, size_t src_size, char** ds /** Second four bits **/ ptr = strchr(hex, src[1]); - if (!ptr) + if (UNLIKELY(!ptr)) { QPERR(QPF_ERR_T_BADCHAR); return -1; @@ -889,10 +889,10 @@ qpf_internal_Translate(pQPSession s, const char* srcbuf, size_t srcsize, char** char* trans; int nogrow = (grow_fn == NULL); - if (srcsize >= SIZE_MAX/2/table->MaxExpand) + if (UNLIKELY(srcsize >= SIZE_MAX/2/table->MaxExpand)) return -1; - if (srcsize) + if (LIKELY(srcsize)) { rval += srcsize; if ((srcsize*table->MaxExpand) <= limit && (srcsize*table->MaxExpand + min_room) <= (*dstsize - *dstoffs)) @@ -1019,12 +1019,12 @@ qpfPrintf_va_internal(pQPSession s, char** str, size_t* size, qpf_grow_fn_t grow size_t min_room; char quote; - if (!QPF.is_init) + if (UNLIKELY(!QPF.is_init)) { if (qpfInitialize() < 0) return -ENOMEM; } - if (!s) + if (UNLIKELY(!s)) { null_session.Errors = 0; s=&null_session; @@ -1033,7 +1033,7 @@ qpfPrintf_va_internal(pQPSession s, char** str, size_t* size, qpf_grow_fn_t grow /** this all falls apart if there isn't at least room for the ** null terminator! **/ - if ((!*str || *size < 1) && !grow_fn(str, size, cpoffset, grow_arg, 1)) + if (UNLIKELY((!*str || *size < 1) && !grow_fn(str, size, cpoffset, grow_arg, 1))) { rval = -EINVAL; QPERR(QPF_ERR_T_BUFOVERFLOW); goto error; } /** search for %this-and-that (specifiers), copy everything else **/ @@ -1274,7 +1274,7 @@ qpfPrintf_va_internal(pQPSession s, char** str, size_t* size, qpf_grow_fn_t grow switch(specchain[i]) { case QPF_SPEC_T_NLEN: - if (cplen > specchain_n[i]) + if (UNLIKELY(cplen > specchain_n[i])) { QPERR(QPF_ERR_T_INSOVERFLOW); cplen = specchain_n[i]; @@ -1284,61 +1284,64 @@ qpfPrintf_va_internal(pQPSession s, char** str, size_t* size, qpf_grow_fn_t grow case QPF_SPEC_T_SYM: if (n_specs-i == 2 && specchain[i+1] == QPF_SPEC_T_NLEN && cplen > specchain_n[i+1]) cplen = specchain_n[i+1]; - if (cxsecVerifySymbol_n(strval, cplen) < 0) + if (UNLIKELY(cxsecVerifySymbol_n(strval, cplen) < 0)) { rval = -EINVAL; QPERR(QPF_ERR_T_BADSYMBOL); goto error; } break; case QPF_SPEC_T_FILE: if (n_specs-i == 2 && specchain[i+1] == QPF_SPEC_T_NLEN && cplen > specchain_n[i+1]) cplen = specchain_n[i+1]; - if ((cplen == 1 && strval[0] == '.') || + if (UNLIKELY((cplen == 1 && strval[0] == '.') || (cplen == 2 && strval[0] == '.' && strval[1] == '.') || memchr(strval, '/', cplen) || memchr(strval, '\0', cplen) || - cplen == 0) + cplen == 0)) { rval = -EINVAL; QPERR(QPF_ERR_T_BADFILE); goto error; } break; case QPF_SPEC_T_PATH: if (n_specs-i == 2 && specchain[i+1] == QPF_SPEC_T_NLEN && cplen > specchain_n[i+1]) cplen = specchain_n[i+1]; - if ((cplen == 2 && strval[0] == '.' && strval[1] == '.') || + if (UNLIKELY((cplen == 2 && strval[0] == '.' && strval[1] == '.') || (cplen > 2 && strval[0] == '.' && strval[1] == '.' && strval[2] == '/') || memchr(strval, '\0', cplen) || cplen == 0 || (cplen > 2 && strval[cplen-1] == '.' && strval[cplen-2] == '.' && strval[cplen-3] == '/') || - qpf_internal_FindStr(strval, cplen, "/../", 4) >= 0) + qpf_internal_FindStr(strval, cplen, "/../", 4) >= 0)) { rval = -EINVAL; QPERR(QPF_ERR_T_BADPATH); goto error; } break; case QPF_SPEC_T_B64: - if((n=qpf_internal_base64encode(s, strval, cplen, str, size, &cpoffset, grow_fn, grow_arg))<0) - { rval = -EINVAL; goto error; } - else + n = qpf_internal_base64encode(s, strval, cplen, str, size, &cpoffset, grow_fn, grow_arg); + if (UNLIKELY(n < 0)) { - copied+=n; - cplen=0; + rval = -EINVAL; + goto error; } + copied += n; + cplen = 0; break; case QPF_SPEC_T_DB64: - if((n=qpf_internal_base64decode(s, strval, cplen, str, size, &cpoffset, grow_fn, grow_arg))<0) - { rval = -EINVAL; goto error; } - else + n = qpf_internal_base64decode(s, strval, cplen, str, size, &cpoffset, grow_fn, grow_arg); + if (UNLIKELY(n < 0)) { - copied+=n; - cplen=0; + rval = -EINVAL; + goto error; } + copied += n; + cplen = 0; break; case QPF_SPEC_T_DHEX: - if((n=qpf_internal_hexdecode(s, strval, cplen, str, size, &cpoffset, grow_fn, grow_arg))<0) - { rval = -EINVAL; goto error; } - else + n = qpf_internal_hexdecode(s, strval, cplen, str, size, &cpoffset, grow_fn, grow_arg); + if (UNLIKELY(n < 0)) { - copied+=n; - cplen=0; + rval = -EINVAL; + goto error; } + copied += n; + cplen = 0; break; case QPF_SPEC_T_ESCQ: @@ -1355,7 +1358,7 @@ qpfPrintf_va_internal(pQPSession s, char** str, size_t* size, qpf_grow_fn_t grow case QPF_SPEC_T_HTENLBR: case QPF_SPEC_T_HEX: case QPF_SPEC_T_URL: - if (n_specs-i == 1 || (n_specs-i == 2 && specchain[i+1] == QPF_SPEC_T_NLEN)) + if (LIKELY(n_specs-i == 1 || (n_specs-i == 2 && specchain[i+1] == QPF_SPEC_T_NLEN))) { if (n_specs-i == 2) maxdst = specchain_n[i+1]; @@ -1432,7 +1435,7 @@ qpfPrintf_va_internal(pQPSession s, char** str, size_t* size, qpf_grow_fn_t grow if (quote && specchain[0] != QPF_SPEC_T_STR) { /** don't quote things other than strings **/ - if (cplen > maxdst) + if (UNLIKELY(cplen > maxdst)) { QPERR(QPF_ERR_T_INSOVERFLOW); cplen = maxdst; @@ -1441,7 +1444,7 @@ qpfPrintf_va_internal(pQPSession s, char** str, size_t* size, qpf_grow_fn_t grow } if (quote) { - if (maxdst < 2) + if (UNLIKELY(maxdst < 2)) { QPERR(QPF_ERR_T_BADFORMAT); rval = -EINVAL; @@ -1451,7 +1454,7 @@ qpfPrintf_va_internal(pQPSession s, char** str, size_t* size, qpf_grow_fn_t grow } if (quote) { - if (LIKELY(!nogrow) && (LIKELY(cpoffset+1+1 <= *size) || (grow_fn(str, size, cpoffset, grow_arg, cpoffset+1+1)))) + if (LIKELY(!nogrow && (cpoffset + 2 <= *size || grow_fn(str, size, cpoffset, grow_arg, cpoffset + 2)))) { (*str)[cpoffset++] = quote; } @@ -1464,7 +1467,7 @@ qpfPrintf_va_internal(pQPSession s, char** str, size_t* size, qpf_grow_fn_t grow } oldcpoffset = cpoffset; n = qpf_internal_Translate(s, strval, cplen, str, &cpoffset, size, maxdst, table, nogrow?NULL:grow_fn, grow_arg, min_room); - if (n < 0) + if (UNLIKELY(n < 0)) { QPERR(QPF_ERR_T_INTERNAL); rval = n; @@ -1473,7 +1476,7 @@ qpfPrintf_va_internal(pQPSession s, char** str, size_t* size, qpf_grow_fn_t grow if (n != cpoffset - oldcpoffset) nogrow = 1; if (quote) { - if ((LIKELY(cpoffset+1+1 <= *size) || (grow_fn(str, size, cpoffset, grow_arg, cpoffset+1+1)))) + if (LIKELY(cpoffset + 2 <= *size || grow_fn(str, size, cpoffset, grow_arg, cpoffset + 2))) { (*str)[cpoffset++] = quote; } @@ -1562,7 +1565,7 @@ qpfRegisterExt(char* ext_spec, int (*ext_fn)(), int is_source) { /** Check if extension max has been reached. **/ - if (QPF.n_ext >= QPF_MAX_EXTS) + if (UNLIKELY(QPF.n_ext >= QPF_MAX_EXTS)) { fprintf(stderr, "warning: qpfRegisterExt: QPF_MAX_EXTS exceeded\n"); return; From 9e97d25f57179a1267c7a5b5c36fbc030341005d Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Wed, 8 Apr 2026 15:11:01 -0600 Subject: [PATCH 04/21] Improve function and struct doc comments. --- centrallix-lib/include/qprintf.h | 1 + centrallix-lib/src/qprintf.c | 65 ++++++++++++++++++++++++-------- 2 files changed, 50 insertions(+), 16 deletions(-) diff --git a/centrallix-lib/include/qprintf.h b/centrallix-lib/include/qprintf.h index 0b85412f4..d85ca0c4e 100644 --- a/centrallix-lib/include/qprintf.h +++ b/centrallix-lib/include/qprintf.h @@ -39,6 +39,7 @@ *** @param offset An offset up to which data must be preserved. *** @param args Arguments for growing the buffer. *** @param req The requested size. + *** @returns True (1) if successful, and false (0) if an error occurs. ***/ // typedef int (*qpf_grow_fn_t)(char** str, size_t* size, size_t offset, void* args, size_t req); typedef int (*qpf_grow_fn_t)(char**, size_t*, size_t, void*, size_t); diff --git a/centrallix-lib/src/qprintf.c b/centrallix-lib/src/qprintf.c index 0d0ed9f37..921530e9c 100644 --- a/centrallix-lib/src/qprintf.c +++ b/centrallix-lib/src/qprintf.c @@ -152,6 +152,19 @@ qpf_spec_names[] = int qpf_spec_len[QPF_SPEC_T_MAXSPEC+1]; +/*** A QPConvTable expresses a way to translate characters. For example, if a + *** QPConvTable expresses how to escape quotes in a string, it might contain + *** mappings that turn `'` into `\'` and `"` into `\"`. + *** + *** @param Matrix A matrix of mappings where `Matrix[c]` is the character(s) + *** that character `c` should map to. Leave this NULL for characters that + *** do not need to be translated. Thus, in our example, `Matrix['"']` + *** should equal the string `"\""`, but `Matrix['a']` should be NULL. + *** @param MatrixLen The length of each string pointed to in `Matrix`. Thus, + *** in our example, `MatrixLen['"']` equal be 2. + *** @param MaxExpand The maximum number of characters that can result from + *** translating a single character. In our example, this would be 2. + ***/ typedef struct { char* Matrix[QPF_MATRIX_SIZE]; @@ -763,7 +776,7 @@ qpf_internal_base64decode(pQPSession s, const char* src, size_t src_size, char** *** before writing the result. *** @param grow_fn An optional grow function, used to grow the dst string if *** more space is needed. - *** @param grow_arg The argument to be passed to the grow function. + *** @param grow_arg An argument, passed to`grow_fn`() when it is called. *** @returns The number of bytes written. ***/ static inline int @@ -868,17 +881,35 @@ qpfPrintf_va(pQPSession s, char* str, size_t size, const char* format, va_list a } -/*** qpf_internal_Translate() - do a translation copy from one buffer to - *** another, respecting a soft and hard limit, and using a given char - *** translation table. Returns the number of chars that would have been - *** placed in dstbuf had there been enough room (or, if there was enough - *** room, the actual # chars placed in dstbuf); NOTE - does NOT return - *** the number of chars pulled from the srcbuf!!! +/*** Copy characters to the new buffer, translating each character using the + *** given pQPConvTable. Respects a soft and hard limit. + *** + *** I'm not sure what this stuff about the "soft and hard limit" means, but + *** I think it has something do do with the `limit` parameter. I'm also not + *** sure why that parameter is necessary. (Israel, 2026) *** - *** min_room applies to 'dstsize' only -- we must leave at least that much - *** room in dstbuf once we are done. Often this is "1", to leave room for - *** a null terminator, or "2" to leave room for a closing quote mark and a - *** null terminator, for instance. + *** @param s The qprintf session in use. + *** @param srcbuf The source string representation to translate and copy. + *** @param srcsize The length of `srcbuf` in bytes. + *** @param dstbuf A pointer to a location where the resulting string pointer + *** should be saved. + *** @param dstoffs The number of bytes to skip at the start of `dstbuf` + *** before writing the result. + *** @param dstsize The size of the currently allocated string at `dstbuf`. + *** @param limit The maximum amount that the destination string can grow past + *** the size of the source string. Causes a `QPF_ERR_T_INSOVERFLOW` + *** error if this limit is exceeded. + *** @param table The translation table to apply when copying each character. + *** @param grow_fn An optional grow function, used to grow the dst string if + *** more space is needed. + *** @param grow_arg An argument, passed to`grow_fn`() when it is called. + *** @param min_room A required amount of space that must be available at the + *** end of the destination buffer when the function completes. Often this is + *** `1`, to leave room for a null terminator, or `2` to leave room for a + *** closing quote mark followed by a null terminator. + *** @returns The number of chars placed in dstbuf (or the number that would + *** have been placed if there was enough room. Note: Does NOT return the + *** number of chars pulled from the srcbuf!!! ***/ static inline int qpf_internal_Translate(pQPSession s, const char* srcbuf, size_t srcsize, char** dstbuf, size_t* dstoffs, size_t* dstsize, size_t limit, pQPConvTable table, qpf_grow_fn_t grow_fn, void* grow_arg, size_t min_room) @@ -971,18 +1002,20 @@ qpf_internal_Translate(pQPSession s, const char* srcbuf, size_t srcsize, char** } -/*** Parse the provided format to apply all of the qprintf() format specifier - *** rules while printing the indicated characters. +/*** Parse the provided format, apply all of the qprintf() format specifier + *** rules, and write the result to a string buffer. *** *** Warning: The 'str' parameter may change during the execution of this *** function if grow_fn reallocates it. Do not store pointers to 'str'! *** Instead, use offsets. *** - *** NULL, &(s->Tmpbuf), &(s->TmpbufSize), htr_internal_GrowFn, (void*)s, fmt, va *** @param s Optional session struct. - *** @param str Pointer to a string buffer where data will be written. - *** @param size Pointer to the current size of the string buffer. + *** @param str A pointer to a string buffer where data will be written. + *** @param size A pointer to the current size of the string buffer. *** @param grow_fn A function to grow the string buffer. + *** @param grow_fn An optional grow function, used to grow `str` if more + *** space is needed. + *** @param grow_arg An argument, passed to`grow_fn`() when it is called. *** @param format The format of data which should be written. *** @param ap The arguments list to fulfill the provided format. ***/ From 3c317aeba9a01c94fe7a9c0f82f920a01055190b Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Wed, 8 Apr 2026 15:14:32 -0600 Subject: [PATCH 05/21] Improve readability of several functions with better code patterns and more comments. Overhaul qpf_internal_Translate() to improve readability. Improve readability of qpf_internal_SetupTable(). Improve code in multiple functions that ensures the module is initialized. Clean up and improve function doc comments. --- centrallix-lib/src/qprintf.c | 160 +++++++++++++++++------------------ 1 file changed, 78 insertions(+), 82 deletions(-) diff --git a/centrallix-lib/src/qprintf.c b/centrallix-lib/src/qprintf.c index 921530e9c..4f8eb1f8f 100644 --- a/centrallix-lib/src/qprintf.c +++ b/centrallix-lib/src/qprintf.c @@ -246,18 +246,20 @@ qpf_internal_FindStr(const char* haystack, size_t haystacklen, const char* needl int qpf_internal_SetupTable(pQPConvTable table) { - int i; - int mx = 1; - int n; + size_t mx = 1; - for(i=0;iMatrix[i]) - { - n = strlen(table->Matrix[i]); - if (n > mx) mx = n; - table->MatrixLen[i] = n; - } + /*** Skip characters that don't map to different characters. + *** LIKELY because most translation tables do not include + *** most characters. + ***/ + if (LIKELY(table->Matrix[i] == NULL)) continue; + + /** Compute the length of the string that this char maps to. **/ + const size_t n = strlen(table->Matrix[i]); + if (n > mx) mx = n; + table->MatrixLen[i] = n; } table->MaxExpand = mx; @@ -479,11 +481,10 @@ qpfOpenSession(void) { pQPSession s = NULL; - if (UNLIKELY(!QPF.is_init)) - { - if (UNLIKELY(qpfInitialize() < 0)) return NULL; - } + /** Ensure initialization. **/ + if (UNLIKELY(!QPF.is_init) && UNLIKELY(qpfInitialize() < 0)) return NULL; + /** Allocate and initialize the new session. **/ s = (pQPSession)nmMalloc(sizeof(QPSession)); if (UNLIKELY(!s)) return NULL; s->Errors = 0; @@ -908,97 +909,94 @@ qpfPrintf_va(pQPSession s, char* str, size_t size, const char* format, va_list a *** `1`, to leave room for a null terminator, or `2` to leave room for a *** closing quote mark followed by a null terminator. *** @returns The number of chars placed in dstbuf (or the number that would - *** have been placed if there was enough room. Note: Does NOT return the - *** number of chars pulled from the srcbuf!!! + *** have been placed if there was enough room), or -1 if an error occurs. + *** Note: Does NOT return the number of chars pulled from the srcbuf!!! ***/ static inline int -qpf_internal_Translate(pQPSession s, const char* srcbuf, size_t srcsize, char** dstbuf, size_t* dstoffs, size_t* dstsize, size_t limit, pQPConvTable table, qpf_grow_fn_t grow_fn, void* grow_arg, size_t min_room) - { - int rval = 0; - unsigned int tlen; - int i; - char* trans; +qpf_internal_Translate( + pQPSession s, + const char* srcbuf, + size_t srcsize, + char** dstbuf, + size_t* dstoffs, + size_t* dstsize, + size_t limit, + pQPConvTable table, + qpf_grow_fn_t grow_fn, + void* grow_arg, + size_t min_room +) { + int n_chars_written = 0; int nogrow = (grow_fn == NULL); - if (UNLIKELY(srcsize >= SIZE_MAX/2/table->MaxExpand)) + /** Check for sources that are FAR too large for us to handle. **/ + if (UNLIKELY(srcsize >= (SIZE_MAX / 2) / table->MaxExpand)) return -1; - if (LIKELY(srcsize)) + if (LIKELY(srcsize != 0)) { - rval += srcsize; - if ((srcsize*table->MaxExpand) <= limit && (srcsize*table->MaxExpand + min_room) <= (*dstsize - *dstoffs)) + n_chars_written += srcsize; + const size_t max_chars_to_write = srcsize * table->MaxExpand; + const size_t max_chars_needed = max_chars_to_write + min_room; + const size_t chars_available = *dstsize - *dstoffs; + if (max_chars_to_write <= limit && max_chars_needed <= chars_available) { /** Easy route - definitely enough space! **/ - for(i=0;iMatrix[(unsigned char)(srcbuf[i])]) != NULL))) - { - tlen = table->MatrixLen[(unsigned char)(srcbuf[i])]; - while(*trans) (*dstbuf)[(*dstoffs)++] = *(trans++); - rval += (tlen-1); - } - else + const unsigned char c = (unsigned char)(srcbuf[i]); + const char* translated_chars = table->Matrix[c]; + + /*** Check if the translation table specifies to do nothing to this character. + *** LIKELY because most translation tables do not include most characters. + ***/ + if (LIKELY(translated_chars == NULL)) { (*dstbuf)[(*dstoffs)++] = srcbuf[i]; + continue; } + + /** Write the translated characters to the destination buffer. **/ + const unsigned int translated_length = table->MatrixLen[c]; + while(*translated_chars) (*dstbuf)[(*dstoffs)++] = *(translated_chars++); + n_chars_written += (translated_length-1); } } else { /** Hard route - may or may not be enough space! **/ - for(i=0;iMatrix[(unsigned char)(srcbuf[i])]) != NULL))) + const unsigned char c = (unsigned char)(srcbuf[i]); + const char* translated_chars = table->Matrix[c]; + const unsigned int translated_length = (LIKELY(translated_chars == NULL)) ? 1 : table->MatrixLen[c]; + + if (UNLIKELY(translated_length > limit)) { - tlen = table->MatrixLen[(unsigned char)(srcbuf[i])]; - if (LIKELY(limit >= tlen)) - { - rval += (tlen-1); - if (LIKELY(!nogrow) && (LIKELY((*dstoffs)+tlen+min_room <= (*dstsize)) || - (grow_fn(dstbuf, dstsize, *dstoffs, grow_arg, (*dstoffs)+tlen+min_room)))) - { - while(*trans) (*dstbuf)[(*dstoffs)++] = *(trans++); - limit -= tlen; - } - else - { - QPERR(QPF_ERR_T_BUFOVERFLOW); - nogrow = 1; - } - } - else - { - QPERR(QPF_ERR_T_INSOVERFLOW); - rval--; - } + QPERR(QPF_ERR_T_INSOVERFLOW); + n_chars_written--; + continue; } - else + + /** Check the available space in the destination buffer. **/ + n_chars_written += (translated_length-1); + const size_t chars_needed = *dstoffs + translated_length + min_room; + if (nogrow || (chars_needed > *dstsize && !grow_fn(dstbuf, dstsize, *dstoffs, grow_arg, chars_needed))) { - if (LIKELY(limit > 0)) - { - if (LIKELY(!nogrow) && (LIKELY((*dstoffs)+1+min_room <= (*dstsize)) || - (grow_fn(dstbuf, dstsize, *dstoffs, grow_arg, (*dstoffs)+1+min_room)))) - { - (*dstbuf)[(*dstoffs)++] = srcbuf[i]; - limit--; - } - else - { - QPERR(QPF_ERR_T_BUFOVERFLOW); - nogrow = 1; - } - } - else - { - QPERR(QPF_ERR_T_INSOVERFLOW); - rval--; - } + QPERR(QPF_ERR_T_BUFOVERFLOW); + nogrow = 1; + continue; } + + /** Write the translated characters to the destination buffer. **/ + if (LIKELY(translated_chars == NULL)) (*dstbuf)[(*dstoffs)++] = srcbuf[i]; + else while(*translated_chars) (*dstbuf)[(*dstoffs)++] = *(translated_chars++); + limit -= translated_length; } } } - return rval; + return n_chars_written; } @@ -1052,10 +1050,8 @@ qpfPrintf_va_internal(pQPSession s, char** str, size_t* size, qpf_grow_fn_t grow size_t min_room; char quote; - if (UNLIKELY(!QPF.is_init)) - { - if (qpfInitialize() < 0) return -ENOMEM; - } + /** Ensure initialization. **/ + if (UNLIKELY(!QPF.is_init) && UNLIKELY(qpfInitialize() < 0)) return -ENOMEM; if (UNLIKELY(!s)) { From ff299ae2eb9fa0dd3eaeeaeb57694d1da1f99b19 Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Fri, 10 Apr 2026 16:19:20 -0600 Subject: [PATCH 06/21] Improve tools for handling errors from qprintf(). Add line numbers to errors. Add qpfLogErrors(). Add qpf_internal_getErrorName(). Add qpf_internal_count_zeros(). Add QPF_ERR_T_NO_ERRORS. Add QPF_ERR_COUNT. Improve formating of QPF_ERR_T defines. --- centrallix-lib/include/qprintf.h | 36 +++++++------- centrallix-lib/src/qprintf.c | 82 ++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 17 deletions(-) diff --git a/centrallix-lib/include/qprintf.h b/centrallix-lib/include/qprintf.h index d85ca0c4e..a52f06148 100644 --- a/centrallix-lib/include/qprintf.h +++ b/centrallix-lib/include/qprintf.h @@ -32,6 +32,23 @@ #include +#define QPF_ERR_T_NO_ERRORS (0) /* a default error buffer value with no errors */ +#define QPF_ERR_T_NOTIMPL (1<<0) /* unimplemented feature */ +#define QPF_ERR_T_BUFOVERFLOW (1<<1) /* dest buffer too small */ +#define QPF_ERR_T_INSOVERFLOW (1<<2) /* NLEN or *LEN restriction occurred */ +#define QPF_ERR_T_NOTPOSITIVE (1<<3) /* %POS conversion but number was neg */ +#define QPF_ERR_T_BADSYMBOL (1<<4) /* &SYM filter did not match the data. */ +#define QPF_ERR_T_MEMORY (1<<5) /* Memory allocation failed (internal). */ +#define QPF_ERR_T_BADLENGTH (1<<6) /* Length for NLEN or *LEN was invalid */ +#define QPF_ERR_T_BADFORMAT (1<<7) /* Format string was invalid */ +#define QPF_ERR_T_RESOURCE (1<<8) /* Internal resource limit hit */ +#define QPF_ERR_T_NULL (1<<9) /* NULL pointer passed (e.g. as a string) */ +#define QPF_ERR_T_INTERNAL (1<<10) /* Unrecoverable internal error. */ +#define QPF_ERR_T_BADFILE (1<<11) /* Bad filename for &FILE filter */ +#define QPF_ERR_T_BADPATH (1<<12) /* Bad pathname for &PATH filter */ +#define QPF_ERR_T_BADCHAR (1<<13) /* Bad character for filter (e.g. an octothorpe for &DB64) */ +#define QPF_ERR_COUNT (14) /* The number of errors listed above. */ + /*** A function to grow a string buffer. *** *** @param str The string buffer being grown. @@ -47,31 +64,16 @@ typedef int (*qpf_grow_fn_t)(char**, size_t*, size_t, void*, size_t); typedef struct _QPS { unsigned int Errors; /* QPF_ERR_T_xxx */ + unsigned int ErrorLines[QPF_ERR_COUNT]; } QPSession, *pQPSession; -#define QPF_ERR_T_NOTIMPL 1 /* unimplemented feature */ -#define QPF_ERR_T_BUFOVERFLOW 2 /* dest buffer too small */ -#define QPF_ERR_T_INSOVERFLOW 4 /* NLEN or *LEN restriction occurred */ -#define QPF_ERR_T_NOTPOSITIVE 8 /* %POS conversion but number was neg */ -#define QPF_ERR_T_BADSYMBOL 16 /* &SYM filter did not match the data. */ -#define QPF_ERR_T_MEMORY 32 /* Memory allocation failed (internal). */ -#define QPF_ERR_T_BADLENGTH 64 /* Length for NLEN or *LEN was invalid */ -#define QPF_ERR_T_BADFORMAT 128 /* Format string was invalid */ -#define QPF_ERR_T_RESOURCE 256 /* Internal resource limit hit */ -#define QPF_ERR_T_NULL 512 /* NULL pointer passed (e.g. as a string) */ -#define QPF_ERR_T_INTERNAL 1024 /* Uncorrectable internal error. */ -#define QPF_ERR_T_BADFILE 2048 /* Bad filename for &FILE filter */ -#define QPF_ERR_T_BADPATH 4096 /* Bad pathname for &PATH filter */ -#define QPF_ERR_T_BADCHAR 8192 /* Bad character for filter (e.g. an octothorpe for &DB64) */ - -#define QPERR(x) (s->Errors |= (x)) - /*** QPrintf methods ***/ pQPSession qpfOpenSession(void); int qpfCloseSession(pQPSession s); int qpfClearErrors(pQPSession s); unsigned int qpfErrors(pQPSession s); +void qpfLogErrors(pQPSession s); int qpfPrintf(pQPSession s, char* str, size_t size, const char* format, ...); int qpfPrintf_va(pQPSession s, char* str, size_t size, const char* format, va_list ap); void qpfRegisterExt(char* ext_spec, int (*ext_fn)(), int is_source); diff --git a/centrallix-lib/src/qprintf.c b/centrallix-lib/src/qprintf.c index 4f8eb1f8f..514e77591 100644 --- a/centrallix-lib/src/qprintf.c +++ b/centrallix-lib/src/qprintf.c @@ -200,6 +200,35 @@ typedef struct static QPF_t QPF = { n_ext:0, is_init:0 }; +/** TODO: Israel - Move to util.h after dups branch is merged. **/ +/*** Count the number of 0s until the first 1. If we pass 16 (aka. `1<<4`), + *** for example, the function returns 4. Useful for converting bitmask + *** values to array indices. + *** + *** @param n The number to be queried. + *** @returns The trailing zero count. + ***/ +static unsigned int +qpf_internal_count_zeros(int n) + { + int shift = 0; + + while ((n & 1) == 0) + { + n >>= 1; + shift++; + } + + return shift; + } + +#define QPERR(err) ({ \ + unsigned int _err = (err); \ + unsigned int _err_i = qpf_internal_count_zeros(_err); \ + s->Errors |= _err; \ + s->ErrorLines[_err_i] = __LINE__; \ + }) + /*** Searches for a substring within a buffer. *** Uses an optimized two-step approach: first locate the first character *** using `memchr()`, then verify the full match with `memcmp()`. @@ -488,6 +517,7 @@ qpfOpenSession(void) s = (pQPSession)nmMalloc(sizeof(QPSession)); if (UNLIKELY(!s)) return NULL; s->Errors = 0; + memset(s->ErrorLines, 0, sizeof(s->ErrorLines)); return s; } @@ -506,6 +536,55 @@ qpfErrors(pQPSession s) return s->Errors; } +/*** Gives the error name for a qprintf error. + *** + *** @param error A bitmask for a single error. + *** @return A string with the name for that error. + ***/ +static const char* +qpf_internal_getErrorName(unsigned int error) + { + switch (error) + { + case QPF_ERR_T_NOTIMPL: return "Not Implemented"; + case QPF_ERR_T_BUFOVERFLOW: return "Buffer Overflow"; + case QPF_ERR_T_INSOVERFLOW: return "Limit Overflow"; + case QPF_ERR_T_NOTPOSITIVE: return "Not Positive"; + case QPF_ERR_T_BADSYMBOL: return "Bad Symbol"; + case QPF_ERR_T_MEMORY: return "Out of Memory"; + case QPF_ERR_T_BADLENGTH: return "Bad Length"; + case QPF_ERR_T_BADFORMAT: return "Bad Format"; + case QPF_ERR_T_RESOURCE: return "Resource Exhaustion"; + case QPF_ERR_T_NULL: return "Null Parameter"; + case QPF_ERR_T_INTERNAL: return "Internal Error"; + case QPF_ERR_T_BADFILE: return "Bad File Name"; + case QPF_ERR_T_BADPATH: return "Bad File Path"; + case QPF_ERR_T_BADCHAR: return "Bad Character"; + default: return "Unknown or mixed error"; + } + } + +/*** Prints a message to stderr containing all the errors that occurred in the + *** specified session. If no errors have occurred, prints nothing. + ***/ +void +qpfLogErrors(pQPSession s) + { + unsigned int errors = s->Errors; + if (!errors) return; + + fprintf(stderr, "qprintf() errors:\n"); + for (unsigned int i = 0u; i < QPF_ERR_COUNT; i++) + { + const unsigned int err = (1 << i); + if (errors & err) + { + const char* error_name = qpf_internal_getErrorName(err); + const unsigned int line_number = s->ErrorLines[i]; + fprintf(stderr, "- %d: %s (%s:%d)\n", err, error_name, __FILE__, line_number); + } + } + } /*** Reset the errors mask, clearing all errors that have occurred. *** @@ -1608,3 +1687,6 @@ qpfRegisterExt(char* ext_spec, int (*ext_fn)(), int is_source) return; } + +/** Scope cleanup. **/ +#undef QPERR From 7a68189c8277419d377b922eb1523d5c0487d706 Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Fri, 10 Apr 2026 16:31:58 -0600 Subject: [PATCH 07/21] Improve #include formatting. --- centrallix-lib/include/qprintf.h | 17 ++++++------ centrallix-lib/src/qprintf.c | 44 +++++++++++++++++--------------- 2 files changed, 31 insertions(+), 30 deletions(-) diff --git a/centrallix-lib/include/qprintf.h b/centrallix-lib/include/qprintf.h index a52f06148..26b10cb9f 100644 --- a/centrallix-lib/include/qprintf.h +++ b/centrallix-lib/include/qprintf.h @@ -1,14 +1,6 @@ #ifndef _QPRINTF_H #define _QPRINTF_H -#ifdef CXLIB_INTERNAL -#include "cxsec.h" -#include "magic.h" -#else -#include "cxlib/cxsec.h" -#include "cxlib/magic.h" -#endif - /************************************************************************/ /* Centrallix Application Server System */ /* Centrallix Base Library */ @@ -29,9 +21,16 @@ /* printf() library calls. */ /************************************************************************/ - #include +#ifdef CXLIB_INTERNAL +#include "cxsec.h" +#include "magic.h" +#else +#include "cxlib/cxsec.h" +#include "cxlib/magic.h" +#endif + #define QPF_ERR_T_NO_ERRORS (0) /* a default error buffer value with no errors */ #define QPF_ERR_T_NOTIMPL (1<<0) /* unimplemented feature */ #define QPF_ERR_T_BUFOVERFLOW (1<<1) /* dest buffer too small */ diff --git a/centrallix-lib/src/qprintf.c b/centrallix-lib/src/qprintf.c index 514e77591..f4bb2cb98 100644 --- a/centrallix-lib/src/qprintf.c +++ b/centrallix-lib/src/qprintf.c @@ -1,23 +1,3 @@ -#ifdef HAVE_CONFIG_H -#include "cxlibconfig-internal.h" -#endif -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "qprintf.h" -#include "mtask.h" -#include "newmalloc.h" -#include "cxsec.h" -#include "util.h" -#include "expect.h" - /************************************************************************/ /* Centrallix Application Server System */ /* Centrallix Base Library */ @@ -36,9 +16,31 @@ /* strings. These functions do not support some of the */ /* more advanced (and dangerous) features of the normal */ /* printf() library calls. */ -/* See centrallix-sysdoc/QPrintf.md for more information. */ +/* See centrallix-sysdoc/QPrintf.md for more information. */ /************************************************************************/ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef HAVE_CONFIG_H +#include "cxlibconfig-internal.h" +#endif + +#include "cxsec.h" +#include "expect.h" +#include "mtask.h" +#include "newmalloc.h" +#include "qprintf.h" +#include "util.h" + /*** maximum # of externally-defined specifiers ***/ #define QPF_MAX_EXTS (64) From 3770556c554a4fe055cc9f89d0e96be98dcec5cd Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Fri, 10 Apr 2026 16:34:54 -0600 Subject: [PATCH 08/21] Refactor qpfPrintf_va_internal() to improve readability and maintainability using better code patterns. --- centrallix-lib/src/qprintf.c | 957 ++++++++++++++++++----------------- 1 file changed, 479 insertions(+), 478 deletions(-) diff --git a/centrallix-lib/src/qprintf.c b/centrallix-lib/src/qprintf.c index f4bb2cb98..dfbc43849 100644 --- a/centrallix-lib/src/qprintf.c +++ b/centrallix-lib/src/qprintf.c @@ -979,7 +979,7 @@ qpfPrintf_va(pQPSession s, char* str, size_t size, const char* format, va_list a *** before writing the result. *** @param dstsize The size of the currently allocated string at `dstbuf`. *** @param limit The maximum amount that the destination string can grow past - *** the size of the source string. Causes a `QPF_ERR_T_INSOVERFLOW` + *** the size of the source string. Causes an `QPF_ERR_T_INSOVERFLOW` *** error if this limit is exceeded. *** @param table The translation table to apply when copying each character. *** @param grow_fn An optional grow function, used to grow the dst string if @@ -1008,12 +1008,12 @@ qpf_internal_Translate( size_t min_room ) { int n_chars_written = 0; - int nogrow = (grow_fn == NULL); - + bool no_grow = (grow_fn == NULL); + /** Check for sources that are FAR too large for us to handle. **/ if (UNLIKELY(srcsize >= (SIZE_MAX / 2) / table->MaxExpand)) return -1; - + if (LIKELY(srcsize != 0)) { n_chars_written += srcsize; @@ -1062,10 +1062,10 @@ qpf_internal_Translate( /** Check the available space in the destination buffer. **/ n_chars_written += (translated_length-1); const size_t chars_needed = *dstoffs + translated_length + min_room; - if (nogrow || (chars_needed > *dstsize && !grow_fn(dstbuf, dstsize, *dstoffs, grow_arg, chars_needed))) + if (no_grow || (chars_needed > *dstsize && !grow_fn(dstbuf, dstsize, *dstoffs, grow_arg, chars_needed))) { QPERR(QPF_ERR_T_BUFOVERFLOW); - nogrow = 1; + no_grow = true; continue; } @@ -1076,7 +1076,7 @@ qpf_internal_Translate( } } } - + return n_chars_written; } @@ -1084,574 +1084,575 @@ qpf_internal_Translate( /*** Parse the provided format, apply all of the qprintf() format specifier *** rules, and write the result to a string buffer. *** - *** Warning: The 'str' parameter may change during the execution of this - *** function if grow_fn reallocates it. Do not store pointers to 'str'! + *** Warning: The 'dest' parameter may change during the execution of this + *** function if `grow_fn` reallocates it. Do not store pointers to 'dest'! *** Instead, use offsets. *** *** @param s Optional session struct. - *** @param str A pointer to a string buffer where data will be written. - *** @param size A pointer to the current size of the string buffer. + *** @param dest A pointer to a string buffer where data will be written. + *** @param dest_size A pointer to the current size of the string buffer. *** @param grow_fn A function to grow the string buffer. - *** @param grow_fn An optional grow function, used to grow `str` if more + *** @param grow_fn An optional grow function, used to grow `dest` if more *** space is needed. *** @param grow_arg An argument, passed to`grow_fn`() when it is called. *** @param format The format of data which should be written. *** @param ap The arguments list to fulfill the provided format. + *** @returns The number of characters copied, or a negative number if an + *** error occurs. ***/ int -qpfPrintf_va_internal(pQPSession s, char** str, size_t* size, qpf_grow_fn_t grow_fn, void* grow_arg, const char* format, va_list ap) +qpfPrintf_va_internal(pQPSession s, char** dest, size_t* dest_size, qpf_grow_fn_t grow_fn, void* grow_arg, const char* format, va_list ap) { - const char* specptr; - const char* endptr; - size_t copied = 0; + size_t copied = 0lu; + size_t dest_offset = 0lu; int rval; - size_t cplen; - ptrdiff_t ptrdiff_cplen; - char specchain[QPF_MAX_SPECS]; - int specchain_n[QPF_MAX_SPECS]; - int n_specs; - int i; - int n; - int found; - int intval; - long long llval; - const char* strval; - double dblval; - char tmpbuf[64]; - char chrval; - size_t cpoffset = 0; - size_t oldcpoffset; - int nogrow = 0; - int startspec; - int endspec; - size_t maxdst; - int ignore = 0; - pQPConvTable table; - QPSession null_session; - size_t min_room; - char quote; - + /** Ensure initialization. **/ if (UNLIKELY(!QPF.is_init) && UNLIKELY(qpfInitialize() < 0)) return -ENOMEM; - - if (UNLIKELY(!s)) + + /** Use a null session (that does nothing with errors) if no session is provided. **/ + QPSession null_session; + if (s == NULL) { null_session.Errors = 0; - s=&null_session; + s = &null_session; } - - /** this all falls apart if there isn't at least room for the - ** null terminator! - **/ - if (UNLIKELY((!*str || *size < 1) && !grow_fn(str, size, cpoffset, grow_arg, 1))) - { rval = -EINVAL; QPERR(QPF_ERR_T_BUFOVERFLOW); goto error; } - - /** search for %this-and-that (specifiers), copy everything else **/ - do { - /** Find the end of the non-specifier string segment **/ - specptr = strchr(format, '%'); - endptr = specptr?specptr:(format+strlen(format)); - - /** Copy the plain section of string **/ - if (!ignore) + + /** Ensure that there is at least enough room for the a null terminator. **/ + if (UNLIKELY((!*dest || *dest_size < 1) && !grow_fn(dest, dest_size, dest_offset, grow_arg, 1))) + { + rval = -EINVAL; + QPERR(QPF_ERR_T_BUFOVERFLOW); + goto error; + } + + /** Search for format specifiers (e.g. %STR, %INT, etc.) and copy all other characters. **/ + bool no_grow = false, ignore = false; + while (1) + { + /** Find the end of the non-specifier string segment. **/ + const char* spec_ptr = strchr(format, '%'); + const bool spec_found = LIKELY(spec_ptr != NULL); + const ptrdiff_t plain_str_len = (spec_found) ? (spec_ptr - format) : strlen(format); + + /** Copy the plain section of string. **/ + if (LIKELY(!ignore)) { - ptrdiff_cplen = (endptr - format); - if (UNLIKELY(ptrdiff_cplen < 0 || ptrdiff_cplen >= SIZE_MAX/4)) + if (UNLIKELY(plain_str_len < 0 || SIZE_MAX/4 <= plain_str_len)) { QPERR(QPF_ERR_T_BUFOVERFLOW); rval = -EINVAL; goto error; } - cplen = ptrdiff_cplen; - if (UNLIKELY(nogrow)) cplen = 0; - if (UNLIKELY(cpoffset+cplen+1 > SIZE_MAX/2)) + + /** Compute the length that we need to copy. */ + size_t copy_len = (UNLIKELY(no_grow)) ? 0 : plain_str_len; + const size_t space_needed = dest_offset + copy_len + 1; + if (UNLIKELY(space_needed > SIZE_MAX/2)) { QPERR(QPF_ERR_T_BUFOVERFLOW); rval = -EINVAL; goto error; } - if (UNLIKELY(cpoffset+cplen+1 > *size) && (nogrow || !grow_fn(str, size, cpoffset, grow_arg, cpoffset+cplen+1))) + + /** Ensure that we have enough space for the copy. **/ + if (UNLIKELY(space_needed > *dest_size) && (no_grow || !grow_fn(dest, dest_size, dest_offset, grow_arg, space_needed))) { QPERR(QPF_ERR_T_BUFOVERFLOW); - cplen = (*size)-cpoffset-1; - nogrow = 1; + no_grow = true; + + /** Reduce the copy length to the maximum we're able to copy. **/ + copy_len = (*dest_size) - dest_offset - 1; } - if (cplen) memcpy((*str) + cpoffset, format, cplen); - cpoffset += cplen; - copied += ptrdiff_cplen; + + /** Copy the plain string to the destination. **/ + if (LIKELY(copy_len > 0)) memcpy(*dest + dest_offset, format, copy_len); + dest_offset += copy_len; + copied += plain_str_len; } - format = endptr; - - /** Handle specifiers **/ - if (format[0] == '%') + format += plain_str_len; + + /** Check if there are no more format specifiers, we're done. **/ + if (!spec_found) break; + format++; /** Consume the % character. */ + + /** Parse simple, 1-character format specifiers. **/ + switch (format[0]) { - format++; - - /** Simple specifiers **/ - if (UNLIKELY(format[0] == '%')) + case '%': { - if (ignore) + /** Consume this character. **/ + format++; + if (ignore) continue; + + /** Ensure that we have enough space. **/ + const size_t space_needed = dest_offset + 2lu; + if (no_grow || (space_needed > *dest_size && !grow_fn(dest, dest_size, dest_offset, grow_arg, space_needed))) { - format++; + QPERR(QPF_ERR_T_BUFOVERFLOW); + no_grow = true; continue; } - if (LIKELY(!nogrow) && (LIKELY(cpoffset+2 <= *size) || (grow_fn(str, size, cpoffset, grow_arg, cpoffset+2)))) - (*str)[cpoffset++] = '%'; - else + /** Add the character to the output string. **/ + (*dest)[dest_offset++] = '%'; + copied++; + continue; + } + case '&': + { + /** Consume this character. **/ + format++; + if (ignore) continue; + + /** Ensure that we have enough space. **/ + const size_t space_needed = dest_offset + 2lu; + if (no_grow || (space_needed > *dest_size && !grow_fn(dest, dest_size, dest_offset, grow_arg, space_needed))) { QPERR(QPF_ERR_T_BUFOVERFLOW); - nogrow = 1; + no_grow = true; + continue; } + + /** Add the character to the output string. **/ + (*dest)[dest_offset++] = '&'; copied++; + continue; + } + case '[': + { + format++; + const int val = va_arg(ap, int); + if (!val) ignore = true; + continue; + } + case ']': + { + format++; + ignore = false; + continue; + } + } + + /** Source data specifier. Build the specifier chain **/ + unsigned int n_specs = 0u; + char specchain[QPF_MAX_SPECS]; + int specchain_n[QPF_MAX_SPECS]; + + /** Only support source format specifiers for the first iteration. **/ + unsigned int startspec = QPF_SPEC_T_STARTSRC; + unsigned int endspec = QPF_SPEC_T_ENDSRC; + do { + /** Check if we've reached the maximum number of specifiers in the chain. **/ + if (UNLIKELY(n_specs == QPF_MAX_SPECS)) + { + rval = -ENOMEM; + QPERR(QPF_ERR_T_RESOURCE); + goto error; + } + + /** Is this a numerically-constrained spec? (e.g. %nSTR, %nLSET, etc.) **/ + int n = -1; + if ('0' <= format[0] && format[0] <= '9') + { + n = strtoi(format, (char**)&format, 10); /* cast is needed because format is const char* */ + if (n < 0) n = 0; + } + else if (format[0] == '*') + { format++; + n = va_arg(ap, int); + if (n < 0) n = 0; } - else if (UNLIKELY(format[0] == '&')) + const bool has_n = (n != -1); + + /** Find this spec using qpf_spec_names. **/ + bool found = 0; + for (unsigned int i = startspec; i <= endspec; i++) { - if (ignore) + /** Skip specs if their numerical constraint status does not match what we parsed. **/ + const int spec_uses_n = (qpf_spec_names[i][0] == 'n'); + if (has_n != spec_uses_n) continue; + + /** Check that the spec name (without any inital "n" constraint marker) matches. **/ + const char* spec_name = qpf_spec_names[i] + ((spec_uses_n) ? 1 : 0); + const int spec_len = qpf_spec_len[i] - ((spec_uses_n) ? 1 : 0); + if (strncmp(format, spec_name, spec_len) == 0) { - format++; - continue; + /** Found it. **/ + format += spec_len; + specchain_n[n_specs] = n; + specchain[n_specs++] = i; + found = true; + break; + } + } + + /** Did we find the format specifier? **/ + if (UNLIKELY(!found)) + { + if (UNLIKELY(n_specs == 0u)) + { + /** need at least one spec **/ + rval = -EINVAL; + QPERR(QPF_ERR_T_BADFORMAT); + goto error; } - if (LIKELY(!nogrow) && (LIKELY(cpoffset+2 <= *size) || (grow_fn(str, size, cpoffset, grow_arg, cpoffset+2)))) - (*str)[cpoffset++] = '&'; - else + /** Invalid spec: Skip to printing. **/ + format--; + break; + } + + /** Later items in the specifier chain must be filter specifiers. **/ + startspec = QPF_SPEC_T_STARTFILT; + endspec = QPF_SPEC_T_ENDFILT; + } + while (format[0] == '&' && format++); /* Loop as long as there are '&' chars to consume. */ + + /** Get the data using the source spec. **/ + char tmp_buf[318]; /* 318 characters are needed to print DBL_MAX. */ + const char* strval = NULL; + const char format_specifier = specchain[0]; + size_t copy_len = 0lu; + switch (format_specifier) + { + case QPF_SPEC_T_INT: + { + const int int_val = va_arg(ap, int); + copy_len = qpf_internal_itoa(tmp_buf, sizeof(tmp_buf), int_val); + strval = tmp_buf; + break; + } + + case QPF_SPEC_T_STR: + { + strval = va_arg(ap, const char*); + if (UNLIKELY(strval == NULL && !ignore)) { - QPERR(QPF_ERR_T_BUFOVERFLOW); - nogrow = 1; + rval = -EINVAL; + QPERR(QPF_ERR_T_NULL); + goto error; } - copied++; - format++; + copy_len = (strval) ? strlen(strval) : 0; + break; } - else if (UNLIKELY(format[0] == ']')) + + case QPF_SPEC_T_POS: { - format++; - ignore = 0; + const int int_val = va_arg(ap, int); + if (UNLIKELY(int_val < 0 && !ignore)) + { + rval = -EINVAL; + QPERR(QPF_ERR_T_NOTPOSITIVE); + goto error; + } + copy_len = qpf_internal_itoa(tmp_buf, sizeof(tmp_buf), int_val); + strval = tmp_buf; + break; } - else if (UNLIKELY(format[0] == '[')) + + case QPF_SPEC_T_LL: { - format++; - intval = va_arg(ap, int); - if (!intval) + const long long ll_val = va_arg(ap, long long); + copy_len = snprintf(tmp_buf, sizeof(tmp_buf), "%lld", ll_val); + strval = tmp_buf; + break; + } + + case QPF_SPEC_T_DBL: + { + const double double_val = va_arg(ap, double); + copy_len = snprintf(tmp_buf, sizeof(tmp_buf), "%lf", double_val); + strval = tmp_buf; + break; + } + + case QPF_SPEC_T_NSTR: + { + strval = va_arg(ap, const char*); + if (UNLIKELY(strval == NULL && !ignore)) { - /*while(*format && format[0] != '%' && format[1] != ']') - format++; - if (*format) - format += 2;*/ - ignore = 1; + rval = -EINVAL; + QPERR(QPF_ERR_T_NULL); + goto error; } + copy_len = specchain_n[0]; + break; + } + + case QPF_SPEC_T_CHR: + { + tmp_buf[0] = va_arg(ap, int); + copy_len = 1; + strval = &tmp_buf[0]; + break; + } + + default: + { + rval = -EINVAL; + QPERR(QPF_ERR_T_BADFORMAT); + goto error; } - else + } + + /** If this specifier is ignored, we're done. Skip all filtering/writing logic. **/ + if (UNLIKELY(ignore)) continue; + + /** Check for invalid length. **/ + if (UNLIKELY(copy_len < 0)) + { + /** This error case appears to be unreachable. **/ + rval = -EINVAL; + QPERR(QPF_ERR_T_BADLENGTH); + goto error; + } + + /** Handle filters. **/ + pQPConvTable table; + size_t min_room = 1; + char quote = 0; + for (unsigned int i = 1; i < n_specs; i++) + { + const char filter_specifier = specchain[i]; + switch (filter_specifier) { - /** Source data specifier. Build the specifier chain **/ - n_specs = 0; - startspec = QPF_SPEC_T_STARTSRC; - endspec = QPF_SPEC_T_ENDSRC; - while(1) + case QPF_SPEC_T_NLEN: { - n = -1; - found = 0; - - /** Is this a numerically-constrained spec? **/ - if (*format >= '0' && *format <= '9') + if (UNLIKELY(copy_len > specchain_n[i])) { - n = strtoi(format, (char**)&endptr, 10); /* cast is needed because endptr is const char* */ - format = endptr; - if (n < 0) n = 0; + QPERR(QPF_ERR_T_INSOVERFLOW); + copy_len = specchain_n[i]; } - else if (*format == '*') + break; + } + + case QPF_SPEC_T_SYM: + { + /** TODO: Fix code the duplication below. **/ + if (n_specs-i == 2 && specchain[i + 1] == QPF_SPEC_T_NLEN && copy_len > specchain_n[i + 1]) + copy_len = specchain_n[i + 1]; + if (UNLIKELY(cxsecVerifySymbol_n(strval, copy_len) < 0)) { - format++; - n = va_arg(ap, int); - if (n < 0) n = 0; + rval = -EINVAL; + QPERR(QPF_ERR_T_BADSYMBOL); + goto error; } - - /** Look for which spec this is **/ - for(i=startspec; i<=endspec; i++) + break; + } + + case QPF_SPEC_T_FILE: + { + if (n_specs-i == 2 && specchain[i + 1] == QPF_SPEC_T_NLEN && copy_len > specchain_n[i + 1]) + copy_len = specchain_n[i + 1]; + if (UNLIKELY( + copy_len == 0 || + (copy_len == 1 && strval[0] == '.') || + (copy_len == 2 && strval[0] == '.' && strval[1] == '.') || + memchr(strval, '/', copy_len) != NULL || + memchr(strval, '\0', copy_len) != NULL + )) { - if ((n == -1 && - qpf_spec_names[i][0] != 'n' && - !strncmp(format, qpf_spec_names[i], qpf_spec_len[i])) || - (n >= 0 && - qpf_spec_names[i][0] == 'n' && - !strncmp(format, qpf_spec_names[i]+1, qpf_spec_len[i]-1))) - { - /** Found it. **/ - if (n == -1) format += qpf_spec_len[i]; - else format += (qpf_spec_len[i]-1); - specchain_n[n_specs] = n; - specchain[n_specs++] = i; - found = 1; - break; - } - } - startspec = QPF_SPEC_T_STARTFILT; - endspec = QPF_SPEC_T_ENDFILT; - - /** Did we find it? **/ - if (UNLIKELY(!found)) - { - if (n_specs == 0) - { - /** need at least one spec **/ - rval = -EINVAL; - QPERR(QPF_ERR_T_BADFORMAT); - goto error; - } - /** invalid spec, ignore and print **/ - format--; - break; + rval = -EINVAL; + QPERR(QPF_ERR_T_BADFILE); + goto error; } - - /** More? **/ - if (*format == '&') + break; + } + + case QPF_SPEC_T_PATH: + { + if (n_specs-i == 2 && specchain[i + 1] == QPF_SPEC_T_NLEN && copy_len > specchain_n[i + 1]) + copy_len = specchain_n[i + 1]; + if (UNLIKELY( + copy_len == 0 || + (copy_len == 2 && strval[0] == '.' && strval[1] == '.') || + (copy_len > 2 && strval[0] == '.' && strval[1] == '.' && strval[2] == '/') || + (copy_len > 2 && strval[copy_len-1] == '.' && strval[copy_len-2] == '.' && strval[copy_len-3] == '/') || + qpf_internal_FindStr(strval, copy_len, "/../", 4) >= 0 || + memchr(strval, '\0', copy_len) + )) { - if (UNLIKELY(n_specs == QPF_MAX_SPECS)) - { rval = -ENOMEM; QPERR(QPF_ERR_T_RESOURCE); goto error; } - format++; + rval = -EINVAL; + QPERR(QPF_ERR_T_BADPATH); + goto error; } - else + break; + } + + case QPF_SPEC_T_B64: + { + const int num_bytes = qpf_internal_base64encode(s, strval, copy_len, dest, dest_size, &dest_offset, grow_fn, grow_arg); + if (UNLIKELY(num_bytes < 0)) { - break; + rval = -EINVAL; + goto error; } + copied += num_bytes; + copy_len = 0; + break; } - - /** Get source **/ - switch(specchain[0]) + + case QPF_SPEC_T_DB64: { - case QPF_SPEC_T_INT: - intval = va_arg(ap, int); - cplen = qpf_internal_itoa(tmpbuf, sizeof(tmpbuf), intval); - strval = tmpbuf; - break; - - case QPF_SPEC_T_STR: - strval = va_arg(ap, const char*); - if (UNLIKELY(strval == NULL && !ignore)) - { rval = -EINVAL; QPERR(QPF_ERR_T_NULL); goto error; } - cplen = strval?strlen(strval):0; - break; - - case QPF_SPEC_T_POS: - intval = va_arg(ap, int); - if (UNLIKELY(intval < 0 && !ignore)) - { rval = -EINVAL; QPERR(QPF_ERR_T_NOTPOSITIVE); goto error; } - cplen = qpf_internal_itoa(tmpbuf, sizeof(tmpbuf), intval); - strval = tmpbuf; - break; - - case QPF_SPEC_T_LL: - llval = va_arg(ap, long long); - cplen = snprintf(tmpbuf, sizeof(tmpbuf), "%lld", llval); - strval = tmpbuf; - break; - - case QPF_SPEC_T_DBL: - dblval = va_arg(ap, double); - cplen = snprintf(tmpbuf, sizeof(tmpbuf), "%lf", dblval); - strval = tmpbuf; - break; - - case QPF_SPEC_T_NSTR: - strval = va_arg(ap, const char*); - if (UNLIKELY(strval == NULL && !ignore)) - { rval = -EINVAL; QPERR(QPF_ERR_T_NULL); goto error; } - cplen = specchain_n[0]; - break; - - case QPF_SPEC_T_CHR: - chrval = va_arg(ap, int); - cplen = 1; - strval = &chrval; - break; - - default: + const int num_bytes = qpf_internal_base64decode(s, strval, copy_len, dest, dest_size, &dest_offset, grow_fn, grow_arg); + if (UNLIKELY(num_bytes < 0)) + { rval = -EINVAL; - QPERR(QPF_ERR_T_BADFORMAT); goto error; + } + copied += num_bytes; + copy_len = 0; + break; } - - if (!ignore) + + case QPF_SPEC_T_DHEX: { - /** Length problem? **/ - if (UNLIKELY(cplen < 0)) - { rval = -EINVAL; QPERR(QPF_ERR_T_BADLENGTH); goto error; } - - /** Filters? **/ - for (i=1;i maxdst)) { - case QPF_SPEC_T_NLEN: - if (UNLIKELY(cplen > specchain_n[i])) - { - QPERR(QPF_ERR_T_INSOVERFLOW); - cplen = specchain_n[i]; - } - break; - - case QPF_SPEC_T_SYM: - if (n_specs-i == 2 && specchain[i+1] == QPF_SPEC_T_NLEN && cplen > specchain_n[i+1]) - cplen = specchain_n[i+1]; - if (UNLIKELY(cxsecVerifySymbol_n(strval, cplen) < 0)) - { rval = -EINVAL; QPERR(QPF_ERR_T_BADSYMBOL); goto error; } - break; - - case QPF_SPEC_T_FILE: - if (n_specs-i == 2 && specchain[i+1] == QPF_SPEC_T_NLEN && cplen > specchain_n[i+1]) - cplen = specchain_n[i+1]; - if (UNLIKELY((cplen == 1 && strval[0] == '.') || - (cplen == 2 && strval[0] == '.' && strval[1] == '.') || - memchr(strval, '/', cplen) || - memchr(strval, '\0', cplen) || - cplen == 0)) - { rval = -EINVAL; QPERR(QPF_ERR_T_BADFILE); goto error; } - break; - - case QPF_SPEC_T_PATH: - if (n_specs-i == 2 && specchain[i+1] == QPF_SPEC_T_NLEN && cplen > specchain_n[i+1]) - cplen = specchain_n[i+1]; - if (UNLIKELY((cplen == 2 && strval[0] == '.' && strval[1] == '.') || - (cplen > 2 && strval[0] == '.' && strval[1] == '.' && strval[2] == '/') || - memchr(strval, '\0', cplen) || - cplen == 0 || - (cplen > 2 && strval[cplen-1] == '.' && strval[cplen-2] == '.' && strval[cplen-3] == '/') || - qpf_internal_FindStr(strval, cplen, "/../", 4) >= 0)) - { rval = -EINVAL; QPERR(QPF_ERR_T_BADPATH); goto error; } - break; - - case QPF_SPEC_T_B64: - n = qpf_internal_base64encode(s, strval, cplen, str, size, &cpoffset, grow_fn, grow_arg); - if (UNLIKELY(n < 0)) - { - rval = -EINVAL; - goto error; - } - copied += n; - cplen = 0; - break; - - case QPF_SPEC_T_DB64: - n = qpf_internal_base64decode(s, strval, cplen, str, size, &cpoffset, grow_fn, grow_arg); - if (UNLIKELY(n < 0)) - { - rval = -EINVAL; - goto error; - } - copied += n; - cplen = 0; - break; - - case QPF_SPEC_T_DHEX: - n = qpf_internal_hexdecode(s, strval, cplen, str, size, &cpoffset, grow_fn, grow_arg); - if (UNLIKELY(n < 0)) - { - rval = -EINVAL; - goto error; - } - copied += n; - cplen = 0; - break; - - case QPF_SPEC_T_ESCQ: - case QPF_SPEC_T_ESCQWS: - case QPF_SPEC_T_JSSTR: - case QPF_SPEC_T_JSONSTR: - case QPF_SPEC_T_DSYB: - case QPF_SPEC_T_CSSVAL: - case QPF_SPEC_T_CSSURL: - case QPF_SPEC_T_ESCWS: - case QPF_SPEC_T_QUOT: - case QPF_SPEC_T_DQUOT: - case QPF_SPEC_T_HTE: - case QPF_SPEC_T_HTENLBR: - case QPF_SPEC_T_HEX: - case QPF_SPEC_T_URL: - if (LIKELY(n_specs-i == 1 || (n_specs-i == 2 && specchain[i+1] == QPF_SPEC_T_NLEN))) - { - if (n_specs-i == 2) - maxdst = specchain_n[i+1]; - else - maxdst = INT_MAX; - switch(specchain[i]) - { - case QPF_SPEC_T_ESCQ: table = &QPF.quote_matrix; - min_room = 1; - quote = 0; - break; - case QPF_SPEC_T_ESCQWS: table = &QPF.quote_ws_matrix; - min_room = 1; - quote = 0; - break; - case QPF_SPEC_T_JSSTR: table = &QPF.jsstr_matrix; - min_room = 1; - quote = 0; - break; - case QPF_SPEC_T_JSONSTR: table = &QPF.jsonstr_matrix; - min_room = 1; - quote = 0; - break; - case QPF_SPEC_T_DSYB: table = &QPF.dsyb_matrix; - min_room = 1; - quote = 0; - break; - case QPF_SPEC_T_SSYB: table = &QPF.ssyb_matrix; - min_room = 1; - quote = 0; - break; - case QPF_SPEC_T_CSSVAL: table = &QPF.cssval_matrix; - min_room = 1; - quote = 0; - break; - case QPF_SPEC_T_CSSURL: table = &QPF.cssurl_matrix; - min_room = 1; - quote = 0; - break; - case QPF_SPEC_T_ESCWS: table = &QPF.ws_matrix; - min_room = 1; - quote = 0; - break; - case QPF_SPEC_T_QUOT: table = &QPF.quote_matrix; - min_room = 2; - quote = '\''; - break; - case QPF_SPEC_T_DQUOT: table = &QPF.quote_matrix; - min_room = 2; - quote = '"'; - break; - case QPF_SPEC_T_HTE: table = &QPF.hte_matrix; - min_room = 1; - quote = 0; - break; - case QPF_SPEC_T_HTENLBR: table = &QPF.htenlbr_matrix; - min_room = 1; - quote = 0; - break; - case QPF_SPEC_T_HEX: table = &QPF.hex_matrix; - min_room = 1; - quote = 0; - break; - case QPF_SPEC_T_URL: table = &QPF.url_matrix; - min_room = 1; - quote = 0; - break; - default: table = NULL; - quote = 0; - min_room = 1; - QPERR(QPF_ERR_T_INTERNAL); - break; - } - if (quote && specchain[0] != QPF_SPEC_T_STR) - { - /** don't quote things other than strings **/ - if (UNLIKELY(cplen > maxdst)) - { - QPERR(QPF_ERR_T_INSOVERFLOW); - cplen = maxdst; - } - break; - } - if (quote) - { - if (UNLIKELY(maxdst < 2)) - { - QPERR(QPF_ERR_T_BADFORMAT); - rval = -EINVAL; - goto error; - } - maxdst -= 2; - } - if (quote) - { - if (LIKELY(!nogrow && (cpoffset + 2 <= *size || grow_fn(str, size, cpoffset, grow_arg, cpoffset + 2)))) - { - (*str)[cpoffset++] = quote; - } - else - { - QPERR(QPF_ERR_T_BUFOVERFLOW); - nogrow = 1; - } - copied++; - } - oldcpoffset = cpoffset; - n = qpf_internal_Translate(s, strval, cplen, str, &cpoffset, size, maxdst, table, nogrow?NULL:grow_fn, grow_arg, min_room); - if (UNLIKELY(n < 0)) - { - QPERR(QPF_ERR_T_INTERNAL); - rval = n; - goto error; - } - if (n != cpoffset - oldcpoffset) nogrow = 1; - if (quote) - { - if (LIKELY(cpoffset + 2 <= *size || grow_fn(str, size, cpoffset, grow_arg, cpoffset + 2))) - { - (*str)[cpoffset++] = quote; - } - else - { - QPERR(QPF_ERR_T_BUFOVERFLOW); - nogrow = 1; - } - copied++; - } - copied += n; - cplen = 0; - } - else - { - QPERR(QPF_ERR_T_NOTIMPL); - rval = -ENOSYS; - goto error; - } - break; - - default: - /** Unimplemented filter **/ - QPERR(QPF_ERR_T_NOTIMPL); - rval = -ENOSYS; - goto error; + QPERR(QPF_ERR_T_INSOVERFLOW); + copy_len = maxdst; } + break; } - - /** Copy it. **/ - if (LIKELY(cplen != 0)) + + /** Add opening quote (if requested). **/ + if (quote) { - copied += cplen; - if (UNLIKELY(cpoffset+cplen+1 > SIZE_MAX/2)) + if (UNLIKELY(maxdst < 2)) { - QPERR(QPF_ERR_T_BUFOVERFLOW); + QPERR(QPF_ERR_T_BADFORMAT); rval = -EINVAL; goto error; } - if (UNLIKELY(nogrow)) cplen = 0; - if (UNLIKELY(cpoffset+cplen+1 > *size) && (!grow_fn(str, size, cpoffset, grow_arg, cpoffset+cplen+1))) + maxdst -= 2; + + const size_t space_needed = dest_offset + 2lu; + if (UNLIKELY(no_grow || (space_needed > *dest_size && !grow_fn(dest, dest_size, dest_offset, grow_arg, space_needed)))) { QPERR(QPF_ERR_T_BUFOVERFLOW); - cplen = (*size) - cpoffset - 1; - nogrow = 1; + no_grow = true; } - memcpy((*str)+cpoffset, strval, cplen); + else (*dest)[dest_offset++] = quote; + copied++; } - - /** Update string counters **/ - cpoffset += cplen; + + /** Translate the string content using the table selected above. **/ + const size_t old_cpoffset = dest_offset; + const qpf_grow_fn_t gf = (no_grow) ? NULL : grow_fn; + const int n_chars = qpf_internal_Translate(s, strval, copy_len, dest, &dest_offset, dest_size, maxdst, table, gf, grow_arg, min_room); + if (UNLIKELY(n_chars < 0)) + { + /** Probably unreachable. **/ + QPERR(QPF_ERR_T_INTERNAL); + rval = n_chars; + goto error; + } + if (UNLIKELY(n_chars != dest_offset - old_cpoffset)) no_grow = true; + + /** Add closing quote (if requested). **/ + if (quote) + { + const size_t space_needed = dest_offset + 2lu; + if (space_needed > *dest_size && !grow_fn(dest, dest_size, dest_offset, grow_arg, space_needed)) + { + QPERR(QPF_ERR_T_BUFOVERFLOW); + no_grow = true; + } + else (*dest)[dest_offset++] = quote; + copied++; + } + copied += n_chars; + copy_len = 0; + + break; } + + default: + /** Unimplemented filter **/ + QPERR(QPF_ERR_T_NOTIMPL); + rval = -ENOSYS; + goto error; } } + + /** Copy the data into the buffer. **/ + if (UNLIKELY(copy_len == 0)) continue; + copied += copy_len; + const size_t space_needed = dest_offset + copy_len + 1lu; + if (UNLIKELY(space_needed > SIZE_MAX/2)) + { + QPERR(QPF_ERR_T_BUFOVERFLOW); + rval = -EINVAL; + goto error; + } + if (UNLIKELY(no_grow)) copy_len = 0; + if (UNLIKELY(space_needed > *dest_size && !grow_fn(dest, dest_size, dest_offset, grow_arg, space_needed))) + { + QPERR(QPF_ERR_T_BUFOVERFLOW); + copy_len = *dest_size - dest_offset - 1; + no_grow = true; + } + memcpy(*dest + dest_offset, strval, copy_len); + + /** Update string counters **/ + dest_offset += copy_len; } - while (specptr); - + + /** Success. **/ rval = copied; - + error: /** Null terminate. Only case where this does not happen is - ** if size == 0 on the initial call. Terminator is not counted + ** if dest_size == 0 on the initial call. Terminator is not counted ** in the return value. **/ - if ((*size) > cpoffset) (*str)[cpoffset] = '\0'; + if ((*dest_size) > dest_offset) (*dest)[dest_offset] = '\0'; + return rval; } From 6566a975dc358ee7dab405ff20f93c3169fdd44a Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Mon, 13 Apr 2026 09:44:12 -0600 Subject: [PATCH 09/21] Clean up qprintf a little bit more. --- centrallix-lib/src/qprintf.c | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/centrallix-lib/src/qprintf.c b/centrallix-lib/src/qprintf.c index dfbc43849..61fc1e222 100644 --- a/centrallix-lib/src/qprintf.c +++ b/centrallix-lib/src/qprintf.c @@ -107,6 +107,8 @@ const char* qpf_spec_names[] = { NULL, /* 0 */ + + /** Source specifiers. **/ "INT", /* 1 */ "STR", /* 2 */ "POS", /* 3 */ @@ -114,6 +116,8 @@ qpf_spec_names[] = "nSTR", /* 5 */ "CHR", /* 6 */ "LL", /* 7 */ + + /** Filter specifiers. **/ "QUOT", /* 8 */ "DQUOT", /* 9 */ "SYM", /* 10 */ @@ -1101,8 +1105,15 @@ qpf_internal_Translate( *** error occurs. ***/ int -qpfPrintf_va_internal(pQPSession s, char** dest, size_t* dest_size, qpf_grow_fn_t grow_fn, void* grow_arg, const char* format, va_list ap) - { +qpfPrintf_va_internal( + pQPSession s, + char** dest, + size_t* dest_size, + qpf_grow_fn_t grow_fn, + void* grow_arg, + const char* format, + va_list ap +) { size_t copied = 0lu; size_t dest_offset = 0lu; int rval; @@ -1145,7 +1156,7 @@ qpfPrintf_va_internal(pQPSession s, char** dest, size_t* dest_size, qpf_grow_fn_ goto error; } - /** Compute the length that we need to copy. */ + /** Compute the length that we need to copy. **/ size_t copy_len = (UNLIKELY(no_grow)) ? 0 : plain_str_len; const size_t space_needed = dest_offset + copy_len + 1; if (UNLIKELY(space_needed > SIZE_MAX/2)) @@ -1174,7 +1185,7 @@ qpfPrintf_va_internal(pQPSession s, char** dest, size_t* dest_size, qpf_grow_fn_ /** Check if there are no more format specifiers, we're done. **/ if (!spec_found) break; - format++; /** Consume the % character. */ + format++; /* Consume the % character. */ /** Parse simple, 1-character format specifiers. **/ switch (format[0]) From db66d7e6687e813b80abfc2f6c95104361f3036f Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Mon, 13 Apr 2026 09:47:04 -0600 Subject: [PATCH 10/21] Update docs for qprintf. Add docs for session functions. Add license. Clarify how to use source and filter format specifiers. Simplify wording for filter specifiers to improve readability. --- centrallix-sysdoc/QPrintf.md | 123 ++++++++++++++++++----------------- 1 file changed, 65 insertions(+), 58 deletions(-) diff --git a/centrallix-sysdoc/QPrintf.md b/centrallix-sysdoc/QPrintf.md index ff572cfa2..533d1f9d0 100644 --- a/centrallix-sysdoc/QPrintf.md +++ b/centrallix-sysdoc/QPrintf.md @@ -3,6 +3,8 @@ Date: January 31, 2006 Author: Greg Beeley (GRB) +License: Copyright (C) 2001-2026 LightSys Technology Services. See LICENSE.txt for more information. + ## Overview Centrallix is a modern application server environment that must work with a lot of untrusted and semi-trusted data, frequently building commands and structures which contain data encapsulated within control structures, such as SQL, HTML, JavaScript, and more. @@ -11,82 +13,87 @@ The goal of this module is to provide an easy, seamless way to safely format suc ## Functions Two functions will initially be available: -- qpfPrintf() - quoted formatted printing into a fixed-length buffer. +- `qpfPrintf()` - quoted formatted printing into a fixed-length buffer. +- `qpfPrintf_va()` - quoted formatted printing into a fixed-length buffer, where the vararg pointer is explicitly provided in lieu of passing the arguments directly to the function. -- qpfPrintf_va() - quoted formatted printing into a fixed-length buffer, where the vararg pointer is explicitly provided in lieu of passing the arguments directly to the function. +The calling syntax of these functions is similar to that of the `snprintf()` and `vsnprintf()` respectively. However, the formatting specifiers are different, and an initial "session" parameter can be provided to track any errors that occur (this can be left NULL if cumulative error handling is not needed). -The calling syntax of these functions is similar to that of the corresponding snprintf() and vsnprintf() calls, but the formatting specifiers are different, and an initial "session" parameter can be provided - but can be left NULL in most cases where cumulative error handling is not needed. +The following functions for handling error sessions are also provided: -## Formatting -Formatting specifiers will begin with a % sign, as in normal printf(). However, the specifiers will be very different and can be combined together to obtain the desired result. Formatting specifiers can affect a parameter's length, position, padding, content, quoting, and more. +- `qpfOpenSession()` - Open a new error session. +- `qpfCloseSession()` - Close an error session, freeing all allocated resources. +- `qpfClearErrors()` - Clear errors on an error session so that it can be reused. +- `qpfErrors()` - Get the error mask for the errors that have occurred in an error session. +- `qpfLogErrors()` - Log a helpful message to the console that errors that have occurred in an error session. -To combine specifiers, simply chain them with the ampersand &. The first one will be applied first to the parameter, and then the second, and so forth. +## Formatting +Format specifiers begin with the percent character (`%`), similarly to the `printf()` functions. However, the `qprintf()` module uses a new set of format specifers. The module includes several source format specifiers (e.g. `%STR`, `%INT`, etc.) which indicate what type of data the function should expect so that it can format that data into the output string. Filter format specifiers (e.g. `%QUOT`, `%PATH`, etc.) can be appended to source format specifiers with the ampersand (`&`) to modify or process the string data, granting control over properties such as its length, position, padding, quoting, and more. Multiple format specifiers may be added and their effects will be applied in order from left to right. The module also provides control format specifiers which control the formatted data in other ways. -For non-string values, once the INT, POS, DBL, etc., conversion has been performed, further specifiers can be added to further process the string. +Some specifiers begin with an `n`, which should be replaced with a number or a wildcard (`*`), in which case the function will expect an int to be provided. This number usually controlls the behavior of the format specifier in some way. For example, when using `%nSTR`, the developer may use `%4STR` to indicate a string exactly 4 characters long. -Please check the end of this document for information on which of these specifiers are currently implemented. +**Warning**: Not all format specifiers below have been implemented yet! Please check the end of this document for information on which specifiers are implemented so far. -### Simple specifiers: +### Control Format Specifiers | Specifier | Description | --------- | ------------ -| %% | A percent sign. -| %& | An ampersand sign. -| %[ | Beginning of conditional printing (an integer argument is expected, 0 = noprint, nonzero = print) -| %] | End of conditional printing +| `%%` | A percent sign. +| `%&` | An ampersand sign. +| `%[` | Beginning of conditional printing (expects an integer: 0 = noprint, nonzero = print). +| `%]` | End of conditional printing. -### Source data specifiers: +### Source Format Specifiers | Specifier | Description | --------- | ------------ -| %INT | An integer value, with range of the normal 'int' value in the C language. Can be positive, negative, or zero. -| %LL | A 64-bit integer value, with range of 'long long' value in the C language. Can be positive, negative, or zero. -| %POS | A non-negative integer value (zero allowed). -| %DBL | Double-precision floating point value. -| %STR | A normal zero-terminated string. -| %nSTR | A string of exactly N length (binary safe), where N is an integer supplied in the format string, or if `*`, supplied as an argument immediately preceding the string pointer. Warning: this does *not* honor null terminators. Be careful. -| %CHR | A single character. -| %XSTR | An XString. -| %EXP | An Expression tree node. -| %POD | A Pointer-to-object-data. The type of the POD is specified as an argument immediately preceding the string pointer. -| %PTOD | A typed pointer-to-object-data. - -### Specifiers used for filtering or processing the data: +| `%INT` | An integer value, with range of the normal 'int' value in the C language. Can be positive, negative, or zero. +| `%LL` | A 64-bit integer value, with range of 'long long' value in the C language. Can be positive, negative, or zero. +| `%POS` | A non-negative integer value (zero allowed). Expects a positive int, and the function errors if the int is negative. +| `%DBL` | A double-precision floating point value. +| `%STR` | A normal null-terminated string. +| `%nSTR` | A string of exactly `n` length (binary safe), where `n` is the integer supplied in the format string, or if `*`, supplied as an argument immediately preceding the string pointer. Warning: This does *not* honor null-terminators. Be careful. +| `%CHR` | A single character. +| `%XSTR` | An XString. +| `%EXP` | An Expression tree node. +| `%POD` | A Pointer-to-object-data. The type of the POD is specified as an argument immediately preceding the string pointer. +| `%PTOD` | A typed pointer-to-object-data. + +### Filter Format Specifiers: | Specifier | Description | ----------------- | ------------ -| " | Adds single quotes around the string value if its source type was a string or date/time value (esp. useful for expressions and pods). -| &DQUOT | Adds double quotes around the string value if its source type was a string or date/time value. -| &ESCQ | Causes quotes in the string to be escaped, with single quotes, double quotes, and backslashes quoted with a leading backslash. -| &WS | Causes whitespace in the string to be processed into its native values (\n -> newline, \r -> carriage return, and \t -> horizontal tab). -| &ESCWS | Causes newlines, carriage returns, and tab characters to be processed back into escaped representations \n, \r, and \t. -| &ESCSP | Causes spaces to be escaped with a backslash. -| &UNESC | Causes string to be unescaped (backslashes removed and escaped values converted to their normal characters). -| &SSYB | Causes single quotes in the string to be doubled, sybase quote style ' -> '' -| &DSYB | Causes double quotes in the string to be doubled " -> "" -| &FILE | Presumes that the string is a filename, and thus results in an error if the string contains '/' or if the string is solely '.' or '..'. -| &PATH | Presumes that the string is a pathname, and so cannot contain '..' at the beginning, end, or between two '/' characters. -| &SYM | Treats the string as a 'symbol', beginning with [_a-zA-Z] and then containing [_a-zA-Z0-9]. Results in an error if the string does not match. -| &HEX | Hex-encodes the entire string. -| &DHEX | Hex-decodes the entire string. -| &B64 | Base64-encodes the entire string. -| &DB64 | Base64-decodes the entire string. -| &RF/reg/ | Filters string value through regular expression, and if it does not match in its entirety, causes an error. -| &RR/reg/rep/ | Filters string value through regular expression and replaces occurrences of 'reg' with 'rep'. It is not an error if the regular expression does not match at all. -| &HTE | Converts special HTML characters to HTML entities (includes &, <, >, ', and ". -| &DHTE | Converts the above HTML entities back to normal characters. -| &URL | Converts any special characters other than [A-Za-z0-9] into %nn where nn is the hex value of the character. -| &DURL | Converts any %nn back to normal characters. -| &nLSET | Space-pads the string on the right (left-align) until there are at least n characters in the string. -| &nRSET | Space-pads the string on the left (right-align) until there are at least n characters in the string. -| &nZRSET | Zero-pads the string on the left (right-align) until there are at least n characters in the string. -| &nLEN | Truncates the string to at most n characters. -| &SQLARG | Makes the argument safe for inclusion in a SQL command as a data value. -| &SQLSYM | Makes the argument safe for inclusion in a SQL command as a symbol (for example, table or column name). -| &HTDATA | Makes the argument safe for inclusion in an HTML document as data, for example between tags or as an attribute of a tag. +| `"` | Adds single quotes around the string value if its source type was a string or DateTime value (esp. useful for expressions and pods). +| `&DQUOT` | Adds double quotes around the string value if its source type was a string or DateTime value. +| `&ESCQ` | Escapes quotes (`'"`) and backslashes (`\`) with a leading backslash. +| `&WS` | Processes whitespace notations into the actual characters (\n -> newline, \r -> carriage return, and \t -> horizontal tab). +| `&ESCWS` | Escapes newlines, carriage returns, and tab characters into their notations: `\n`, `\r`, and `\t`. +| `&ESCSP` | Escapes spaces with a leading backslash. +| `&UNESC` | Unescapes whitespace (backslashes removed and escaped values converted to their normal characters). +| `&SSYB` | Doubles single quotes, sybase quote style `'` -> `''` +| `&DSYB` | Doubles double quotes, sybase quote style `"` -> `""` +| `&FILE` | Ensures the string is a valid filename, giving an error if it contains `'/'` or is only `"."` or `".."`. +| `&PATH` | Ensures the string is a pathname, giving an error if it has `'..'` at the start, end, or between two `'/'` characters. +| `&SYM` | Ensures the string is a symbol (starts with `[_a-zA-Z]`, followed by `[_a-zA-Z0-9]`), giving an error if is does not. +| `&HEX` | **Hex-Encode**s the string (e.g. `"Example"` -> `"4578616d706c65"`). +| `&DHEX` | **Hex-Decode**s the string (e.g. `"4578616d706c65"` -> `"Example"`). +| `&B64` | **Base64-Encode**s the entire string (e.g. `"Example"` -> `"RXhhbXBsZQ=="`). +| `&DB64` | **Base64-Decode**s the entire string (e.g. `"RXhhbXBsZQ=="` -> `"Example"`). +| `&HTE` | **HTML-Encode**: Escapes special HTML characters into HTML entities (including &, <, >, ', and ") to prevent script injection. +| `&DHTE` | **HTML-Dencode**: Unescapes HTML entities back to normal characters. +| `&URL` | **URL-Encode**: Escapes any special characters other than `[A-Za-z0-9]` into `%nn` where `nn` is the character's hex value. +| `&DURL` | **URL-Decode**: Converts any `%nn` encodings back to normal characters. +| `&RF/reg/` | Ensures the string matches a regular expression, giving an error if it does not. +| `&RR/reg/rep/` | Replaces occurrences of the `reg` regular expression with `"rep"`. Does not give an error if no matches occur. +| `&nLSET` | Right-pads the string (left-align) with spaces until it has at least `n` characters. +| `&nRSET` | Left-pads the string (right-align) with spaces until it has at least `n` characters. +| `&nZRSET` | Left-pads the string (right-align) with zeros until it has at least `n` characters. +| `&nLEN` | Truncates the string to at most `n` characters. +| `&SQLARG` | Ensures the string is safe as an SQL data value. +| `&SQLSYM` | Ensures the string is safe as an SQL symbol (for example, table or column name). +| `&HTDATA` | Ensures the string is safe as an HTML document as data, for example between tags or as an attribute of a tag. ## Implemented -Here are the currently implemented specifier chains: +Below is a list of all implemented specifier chains: - %INT - %LL @@ -111,4 +118,4 @@ Here are the currently implemented specifier chains: - %STR&PATH - %STR&PATH&nLEN -All others are unimplemented and will result in a return value of -ENOSYS. +All others are unimplemented and may result in a return value of `-ENOSYS` with the `QPF_ERR_T_NOTIMPL` error set. From 91bbea976e9a93f7552d6713cff8025d1e1c6728 Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Mon, 13 Apr 2026 14:41:52 -0600 Subject: [PATCH 11/21] Resolve greptile comments. Fix is_only_followed_by_nlen using || instead of &&. Fix closing quote space check didn't handle buffer overflows very well. Fix null_session error line numbers not being initialized. Fix qpfClearErrors() being lazy about clearing error line numbers. Fix qpf_internal_count_zeros(0) edge case. --- centrallix-lib/src/qprintf.c | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/centrallix-lib/src/qprintf.c b/centrallix-lib/src/qprintf.c index 61fc1e222..b45f2bfbd 100644 --- a/centrallix-lib/src/qprintf.c +++ b/centrallix-lib/src/qprintf.c @@ -219,6 +219,7 @@ qpf_internal_count_zeros(int n) { int shift = 0; + if (UNLIKELY(n == 0)) return 0; while ((n & 1) == 0) { n >>= 1; @@ -601,6 +602,7 @@ int qpfClearErrors(pQPSession s) { s->Errors = 0; + memset(&s->ErrorLines, 0, sizeof(s->ErrorLines)); return 0; } @@ -1125,7 +1127,7 @@ qpfPrintf_va_internal( QPSession null_session; if (s == NULL) { - null_session.Errors = 0; + memset(&null_session, 0, sizeof(QPSession)); s = &null_session; } @@ -1549,7 +1551,7 @@ qpfPrintf_va_internal( use_table: { /** Skip this spec if the case doesn't meet any of the requirements to use it. **/ const int is_final_spec = (n_specs - i == 1); - const int is_only_followed_by_nlen = (n_specs - i == 2 || specchain[i + 1] == QPF_SPEC_T_NLEN); + const int is_only_followed_by_nlen = (n_specs - i == 2 && specchain[i + 1] == QPF_SPEC_T_NLEN); if (!is_final_spec && !is_only_followed_by_nlen) { QPERR(QPF_ERR_T_NOTIMPL); @@ -1593,7 +1595,6 @@ qpfPrintf_va_internal( } /** Translate the string content using the table selected above. **/ - const size_t old_cpoffset = dest_offset; const qpf_grow_fn_t gf = (no_grow) ? NULL : grow_fn; const int n_chars = qpf_internal_Translate(s, strval, copy_len, dest, &dest_offset, dest_size, maxdst, table, gf, grow_arg, min_room); if (UNLIKELY(n_chars < 0)) @@ -1603,18 +1604,21 @@ qpfPrintf_va_internal( rval = n_chars; goto error; } - if (UNLIKELY(n_chars != dest_offset - old_cpoffset)) no_grow = true; + if (UNLIKELY(s->Errors & QPF_ERR_T_BUFOVERFLOW)) no_grow = true; /** Add closing quote (if requested). **/ if (quote) { const size_t space_needed = dest_offset + 2lu; - if (space_needed > *dest_size && !grow_fn(dest, dest_size, dest_offset, grow_arg, space_needed)) + if (UNLIKELY(no_grow || (space_needed > *dest_size && !grow_fn(dest, dest_size, dest_offset, grow_arg, space_needed)))) { QPERR(QPF_ERR_T_BUFOVERFLOW); no_grow = true; } - else (*dest)[dest_offset++] = quote; + + /** Write the closing quote, even if a buffer overflow occurred. **/ + /** We passed min_room = 2, so there will be enough space for it. **/ + (*dest)[dest_offset++] = quote; copied++; } copied += n_chars; From 0d99e110c68424759c5711e358d690cedd24313e Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Tue, 14 Apr 2026 11:10:11 -0600 Subject: [PATCH 12/21] Fix missing no_grow check. --- centrallix-lib/src/qprintf.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/centrallix-lib/src/qprintf.c b/centrallix-lib/src/qprintf.c index b45f2bfbd..75b983b12 100644 --- a/centrallix-lib/src/qprintf.c +++ b/centrallix-lib/src/qprintf.c @@ -1646,7 +1646,7 @@ qpfPrintf_va_internal( goto error; } if (UNLIKELY(no_grow)) copy_len = 0; - if (UNLIKELY(space_needed > *dest_size && !grow_fn(dest, dest_size, dest_offset, grow_arg, space_needed))) + if (UNLIKELY(no_grow || (space_needed > *dest_size && !grow_fn(dest, dest_size, dest_offset, grow_arg, space_needed)))) { QPERR(QPF_ERR_T_BUFOVERFLOW); copy_len = *dest_size - dest_offset - 1; From 76a904961cb0e40890e737fc4b2bef4c6f1eeb09 Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Tue, 14 Apr 2026 14:17:43 -0600 Subject: [PATCH 13/21] Fix a Greptile comment with testing. Fix a quote buffer overflow comment from Greptile. Add test_qprintf_69.c to validate that the "fixed" code now works properly. Note: Honestly, I'm so in the weeds that I only understand about 80% of what's going on here, but it works, and this probably isn't worth a ton of additional effort. --- centrallix-lib/src/qprintf.c | 12 ++++-- centrallix-lib/tests/test_qprintf_69.c | 58 ++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 centrallix-lib/tests/test_qprintf_69.c diff --git a/centrallix-lib/src/qprintf.c b/centrallix-lib/src/qprintf.c index 75b983b12..69eba302c 100644 --- a/centrallix-lib/src/qprintf.c +++ b/centrallix-lib/src/qprintf.c @@ -1615,9 +1615,15 @@ qpfPrintf_va_internal( QPERR(QPF_ERR_T_BUFOVERFLOW); no_grow = true; } - - /** Write the closing quote, even if a buffer overflow occurred. **/ - /** We passed min_room = 2, so there will be enough space for it. **/ + + /*** Write the closing quote with space for the + *** null-terminator, even if a buffer overflow + *** has occurred, by moving the offset back and + *** overwriting some of the end of the provided + *** string, if needed. + ***/ + if (space_needed > *dest_size) + dest_offset = *dest_size - 2; (*dest)[dest_offset++] = quote; copied++; } diff --git a/centrallix-lib/tests/test_qprintf_69.c b/centrallix-lib/tests/test_qprintf_69.c new file mode 100644 index 000000000..526ba503e --- /dev/null +++ b/centrallix-lib/tests/test_qprintf_69.c @@ -0,0 +1,58 @@ +#include +#include +#include +#include +#include +#include "qprintf.h" +#include + +long long +test(char** tname) + { + int i, rval; + int iter; + unsigned char buf[16]; + + /*** This test verifies that, when printing quoted text to a buffer that + *** is too small using %STR", the string quoting is handled properly + *** and the string is still null-terminated, even if the buffer is not + *** large enough for any of the text. + ***/ + + *tname = "qprintf-69 %STR" small buffer quote & null termination"; + iter = 100000; + for(i=0;i Date: Tue, 14 Apr 2026 14:45:26 -0600 Subject: [PATCH 14/21] Fix a Greptile comment and update qpf_internal_itoa(). Remove an error check that always passed. Add more explicit typecasts. Update qpf_internal_itoa() to return size_t because it cannot error. Add an UNLIKELY() to speed up qpf_internal_itoa(). Fix missing info in the qpf_internal_itoa() doc comment. --- centrallix-lib/src/qprintf.c | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/centrallix-lib/src/qprintf.c b/centrallix-lib/src/qprintf.c index 69eba302c..86899a8ed 100644 --- a/centrallix-lib/src/qprintf.c +++ b/centrallix-lib/src/qprintf.c @@ -628,16 +628,17 @@ qpfCloseSession(pQPSession s) *** @param dst The destination string buffer. *** @param dstlen The allocated length of the string buffer, used to avoid *** buffer overflows. + *** @param i The value to be written. + *** @returns The number of characters written to the buffer. ***/ -static inline int +static inline size_t qpf_internal_itoa(char* dst, size_t dstlen, int i) { char ibuf[sizeof(int)*8*3/10+4]; char* iptr = ibuf; int r; int i2 = i; - int rval; - if (i2 == 0) + if (UNLIKELY(i2 == 0)) { *(iptr++) = '0'; } @@ -651,7 +652,7 @@ qpf_internal_itoa(char* dst, size_t dstlen, int i) } if (i < 0) *(iptr++) = '-'; } - rval = iptr - ibuf; + const size_t rval = iptr - ibuf; while(iptr > ibuf && dstlen > 1) { *(dst++) = *(--iptr); @@ -1368,7 +1369,7 @@ qpfPrintf_va_internal( case QPF_SPEC_T_LL: { const long long ll_val = va_arg(ap, long long); - copy_len = snprintf(tmp_buf, sizeof(tmp_buf), "%lld", ll_val); + copy_len = (size_t)snprintf(tmp_buf, sizeof(tmp_buf), "%lld", ll_val); strval = tmp_buf; break; } @@ -1376,7 +1377,7 @@ qpfPrintf_va_internal( case QPF_SPEC_T_DBL: { const double double_val = va_arg(ap, double); - copy_len = snprintf(tmp_buf, sizeof(tmp_buf), "%lf", double_val); + copy_len = (size_t)snprintf(tmp_buf, sizeof(tmp_buf), "%lf", double_val); strval = tmp_buf; break; } @@ -1413,15 +1414,6 @@ qpfPrintf_va_internal( /** If this specifier is ignored, we're done. Skip all filtering/writing logic. **/ if (UNLIKELY(ignore)) continue; - /** Check for invalid length. **/ - if (UNLIKELY(copy_len < 0)) - { - /** This error case appears to be unreachable. **/ - rval = -EINVAL; - QPERR(QPF_ERR_T_BADLENGTH); - goto error; - } - /** Handle filters. **/ pQPConvTable table; size_t min_room = 1; From dbeec431f95e661f43cde8af5a2b2bad32805eb8 Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Tue, 14 Apr 2026 15:18:13 -0600 Subject: [PATCH 15/21] Fix a buffer underflow bug caused by a previous fix. Add a test case to verify that the buffer underflow is fixed. Clean up and improve correctness of test cases added on this branch. --- centrallix-lib/src/qprintf.c | 17 +++++--- centrallix-lib/tests/test_qprintf_69.c | 10 ++--- centrallix-lib/tests/test_qprintf_70.c | 59 ++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 12 deletions(-) create mode 100644 centrallix-lib/tests/test_qprintf_70.c diff --git a/centrallix-lib/src/qprintf.c b/centrallix-lib/src/qprintf.c index 86899a8ed..ad9288e9d 100644 --- a/centrallix-lib/src/qprintf.c +++ b/centrallix-lib/src/qprintf.c @@ -1609,14 +1609,17 @@ qpfPrintf_va_internal( } /*** Write the closing quote with space for the - *** null-terminator, even if a buffer overflow - *** has occurred, by moving the offset back and - *** overwriting some of the end of the provided - *** string, if needed. + *** null-terminator if at all possible, even if + *** a buffer overflow has already occurred, by + *** moving the offset back and overwriting some + *** of the end of the provided string, if needed. ***/ - if (space_needed > *dest_size) - dest_offset = *dest_size - 2; - (*dest)[dest_offset++] = quote; + if (LIKELY(*dest_size >= 2)) + { + if (UNLIKELY(space_needed > *dest_size)) + dest_offset = *dest_size - 2; + (*dest)[dest_offset++] = quote; + } copied++; } copied += n_chars; diff --git a/centrallix-lib/tests/test_qprintf_69.c b/centrallix-lib/tests/test_qprintf_69.c index 526ba503e..84c4b5cc4 100644 --- a/centrallix-lib/tests/test_qprintf_69.c +++ b/centrallix-lib/tests/test_qprintf_69.c @@ -1,10 +1,8 @@ +#include #include -#include -#include -#include #include + #include "qprintf.h" -#include long long test(char** tname) @@ -23,11 +21,13 @@ test(char** tname) iter = 100000; for(i=0;i +#include +#include + +#include "qprintf.h" + +long long +test(char** tname) + { + int i, rval; + int iter; + unsigned char buf[16]; + + /*** This test verifies that a bug was successfully fixed: When + *** printing quoted text to a buffer that was only one character + *** long using %STR", the function would underflow the buffer, + *** clobbering the byte before the buffer with 0x27, aka. the ' + *** character, intended to be the closing quote. + ***/ + + *tname = "qprintf-70 %STR" buffer size underflow"; + iter = 100000; + for(i=0;i Date: Tue, 14 Apr 2026 16:30:31 -0600 Subject: [PATCH 16/21] Modify error line numbers to be stored as unsigned shorts instead of unsigned ints. Add an error message if a line number overflows USHRT_MAX. Add a doc comment to error sessions. --- centrallix-lib/include/qprintf.h | 12 +++++++++++- centrallix-lib/src/qprintf.c | 6 ++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/centrallix-lib/include/qprintf.h b/centrallix-lib/include/qprintf.h index 26b10cb9f..5d3bbc1b8 100644 --- a/centrallix-lib/include/qprintf.h +++ b/centrallix-lib/include/qprintf.h @@ -60,10 +60,20 @@ // typedef int (*qpf_grow_fn_t)(char** str, size_t* size, size_t offset, void* args, size_t req); typedef int (*qpf_grow_fn_t)(char**, size_t*, size_t, void*, size_t); +/*** Stores information about a qprint session, including details about errors + *** such as parsing and formatting issues that have occurred. + *** + *** @param Errors An error mask indicating any errors that have occurred, or + *** 0 (aka. `QPF_ERR_T_NO_ERRORS`) if no errors have occurred. + *** @param ErrorLines An array where each index represents a type of error in + *** order (e.g. 0 represents `QPF_ERR_T_NOTIMPL`) and each value represents + *** the line number where the error occurred, or 0 if this error type has + *** not occurred during the session. + ***/ typedef struct _QPS { unsigned int Errors; /* QPF_ERR_T_xxx */ - unsigned int ErrorLines[QPF_ERR_COUNT]; + unsigned short ErrorLines[QPF_ERR_COUNT]; } QPSession, *pQPSession; diff --git a/centrallix-lib/src/qprintf.c b/centrallix-lib/src/qprintf.c index ad9288e9d..c9e4b95f7 100644 --- a/centrallix-lib/src/qprintf.c +++ b/centrallix-lib/src/qprintf.c @@ -230,8 +230,10 @@ qpf_internal_count_zeros(int n) } #define QPERR(err) ({ \ - unsigned int _err = (err); \ - unsigned int _err_i = qpf_internal_count_zeros(_err); \ + const unsigned int _err = (err); \ + const unsigned int _err_i = qpf_internal_count_zeros(_err); \ + if (__LINE__ > USHRT_MAX) \ + fprintf(stderr, "Failed to save code location %s:%d: File too long!!\n", __FILE__, __LINE__); \ s->Errors |= _err; \ s->ErrorLines[_err_i] = __LINE__; \ }) From b9e7e5a3a6def1316418e82ccc884a7be234f1b9 Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Tue, 14 Apr 2026 16:31:11 -0600 Subject: [PATCH 17/21] Clean up. Update copyright notice dates and descriptions. Clean up and improve some spacing. Add doc comment to QPSession. --- centrallix-lib/include/qprintf.h | 23 +++++++++++------------ centrallix-lib/src/qprintf.c | 11 +++++------ 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/centrallix-lib/include/qprintf.h b/centrallix-lib/include/qprintf.h index 5d3bbc1b8..e12451398 100644 --- a/centrallix-lib/include/qprintf.h +++ b/centrallix-lib/include/qprintf.h @@ -5,7 +5,7 @@ /* Centrallix Application Server System */ /* Centrallix Base Library */ /* */ -/* Copyright (C) 1998-2006 LightSys Technology Services, Inc. */ +/* Copyright (C) 1998-2026 LightSys Technology Services, Inc. */ /* */ /* You may use these files and this library under the terms of the */ /* GNU Lesser General Public License, Version 2.1, contained in the */ @@ -14,21 +14,21 @@ /* Module: qprintf.c, qprintf.h */ /* Author: Greg Beeley (GRB) */ /* Creation: January 31, 2006 */ -/* Description: Quoting Printf routine, used to make sure that */ -/* injection type attacks don't occur when building */ -/* strings. These functions do not support some of the */ -/* more advanced (and dangerous) features of the normal */ -/* printf() library calls. */ +/* Description: Quoting Printf routine that helps to prevent injection */ +/* attacks when building strings. These functions also do */ +/* not support some of the more advanced (and dangerous) */ +/* features found in the standard printf() library. */ +/* See centrallix-sysdoc/QPrintf.md for more information. */ /************************************************************************/ #include #ifdef CXLIB_INTERNAL -#include "cxsec.h" -#include "magic.h" + #include "cxsec.h" + #include "magic.h" #else -#include "cxlib/cxsec.h" -#include "cxlib/magic.h" + #include "cxlib/cxsec.h" + #include "cxlib/magic.h" #endif #define QPF_ERR_T_NO_ERRORS (0) /* a default error buffer value with no errors */ @@ -57,8 +57,7 @@ *** @param req The requested size. *** @returns True (1) if successful, and false (0) if an error occurs. ***/ -// typedef int (*qpf_grow_fn_t)(char** str, size_t* size, size_t offset, void* args, size_t req); -typedef int (*qpf_grow_fn_t)(char**, size_t*, size_t, void*, size_t); +typedef int (*qpf_grow_fn_t)(char** str, size_t* size, size_t offset, void* args, size_t req); /*** Stores information about a qprint session, including details about errors *** such as parsing and formatting issues that have occurred. diff --git a/centrallix-lib/src/qprintf.c b/centrallix-lib/src/qprintf.c index c9e4b95f7..e5d00d7c1 100644 --- a/centrallix-lib/src/qprintf.c +++ b/centrallix-lib/src/qprintf.c @@ -11,11 +11,10 @@ /* Module: qprintf.c, qprintf.h */ /* Author: Greg Beeley (GRB) */ /* Creation: January 31, 2006 */ -/* Description: Quoting Printf routine, used to make sure that */ -/* injection type attacks don't occur when building */ -/* strings. These functions do not support some of the */ -/* more advanced (and dangerous) features of the normal */ -/* printf() library calls. */ +/* Description: Quoting Printf routine that helps to prevent injection */ +/* attacks when building strings. These functions also do */ +/* not support some of the more advanced (and dangerous) */ +/* features found in the standard printf() library. */ /* See centrallix-sysdoc/QPrintf.md for more information. */ /************************************************************************/ @@ -31,7 +30,7 @@ #include #ifdef HAVE_CONFIG_H -#include "cxlibconfig-internal.h" + #include "cxlibconfig-internal.h" #endif #include "cxsec.h" From d93c43b5cc58059d3f15ee62d41afae8cde044a1 Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Mon, 27 Apr 2026 14:34:20 -0600 Subject: [PATCH 18/21] Add code to handle edge cases. --- centrallix-lib/src/qprintf.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/centrallix-lib/src/qprintf.c b/centrallix-lib/src/qprintf.c index e5d00d7c1..372332ca1 100644 --- a/centrallix-lib/src/qprintf.c +++ b/centrallix-lib/src/qprintf.c @@ -573,13 +573,14 @@ qpf_internal_getErrorName(unsigned int error) } /*** Prints a message to stderr containing all the errors that occurred in the - *** specified session. If no errors have occurred, prints nothing. + *** specified session. If no errors have occurred, print nothing. ***/ void qpfLogErrors(pQPSession s) { + if (s == NULL) return; unsigned int errors = s->Errors; - if (!errors) return; + if (errors == 0) return; fprintf(stderr, "qprintf() errors:\n"); for (unsigned int i = 0u; i < QPF_ERR_COUNT; i++) @@ -1120,7 +1121,7 @@ qpfPrintf_va_internal( ) { size_t copied = 0lu; size_t dest_offset = 0lu; - int rval; + int rval = -1; /** Ensure initialization. **/ if (UNLIKELY(!QPF.is_init) && UNLIKELY(qpfInitialize() < 0)) return -ENOMEM; From 1a0faf86aeda79f05bdcd22a519e7cc5158aa11f Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Thu, 14 May 2026 12:11:38 -0600 Subject: [PATCH 19/21] Add support for using grow functions to qprintf. Add qpfNoGrow() and qpfSysMallocGrow() grow functions for use when calling qpfPrint_g*() functions. Add qpfPrintf_g() and qpfPrintf_gva() qprintf variants that allow passing a grow function. Improve doc comments for all qpfPrintf*() functions. --- centrallix-lib/include/qprintf.h | 4 ++ centrallix-lib/src/qprintf.c | 106 +++++++++++++++++++++++++++++-- 2 files changed, 104 insertions(+), 6 deletions(-) diff --git a/centrallix-lib/include/qprintf.h b/centrallix-lib/include/qprintf.h index e12451398..725b6aec5 100644 --- a/centrallix-lib/include/qprintf.h +++ b/centrallix-lib/include/qprintf.h @@ -82,8 +82,12 @@ int qpfCloseSession(pQPSession s); int qpfClearErrors(pQPSession s); unsigned int qpfErrors(pQPSession s); void qpfLogErrors(pQPSession s); +int qpfNoGrow(char** str, size_t* size, size_t offs, void* arg, size_t req_size); +int qpfSysMallocGrow(char** str, size_t* size, size_t offset, void* args, size_t req_size); int qpfPrintf(pQPSession s, char* str, size_t size, const char* format, ...); int qpfPrintf_va(pQPSession s, char* str, size_t size, const char* format, va_list ap); +int qpfPrintf_g(pQPSession s, char** str, size_t* size, qpf_grow_fn_t grow_fn, void* grow_arg, const char* format, ...); +int qpfPrintf_gva(pQPSession s, char** str, size_t* size, qpf_grow_fn_t grow_fn, void* grow_arg, const char* format, va_list ap); void qpfRegisterExt(char* ext_spec, int (*ext_fn)(), int is_source); /*** Raw interface - should only be used internally by cxlib **/ diff --git a/centrallix-lib/src/qprintf.c b/centrallix-lib/src/qprintf.c index 372332ca1..747636e90 100644 --- a/centrallix-lib/src/qprintf.c +++ b/centrallix-lib/src/qprintf.c @@ -936,16 +936,53 @@ qpf_internal_hexdecode(pQPSession s, const char* src, size_t src_size, char** ds /*** Returns the amount of additional size that will fit, but does not *** reallocate the string to grow it to a larger size. + *** + *** Useful for passing to qpfPrintf_g*() functions. ***/ int -qpfPrintf_grow(char** str, size_t* size, size_t offs, void* arg, size_t req_size) +qpfNoGrow(char** str, size_t* size, size_t offs, void* arg, size_t req_size) { return (*size) >= req_size; } -/*** qpfPrintf() - do the quoting printf operation, given a standard vararg - *** function call. +/*** Assumes the string buffer was allocated using nmSysMalloc() and grows the + *** buffer as needed to reach the requested size. Does not use grow args. + *** + *** Useful for passing to qpfPrintf_g*() functions. + ***/ +int +qpfSysMallocGrow(char** str, size_t* size, size_t offset, void* args, size_t req_size) + { + /** Handle edge cases. **/ + if (UNLIKELY(str == NULL || *str == NULL || size == NULL)) return 0; + if (*size >= req_size) return 1; + if (UNLIKELY(*size == 0)) *size = 1; + + /** Determine new size. **/ + size_t new_size = *size; + do new_size *= 2; + while (new_size < req_size); + + /** Reallocate. **/ + char* new_str = nmSysRealloc(*str, new_size); + if (UNLIKELY(new_str == NULL)) return 0; + *str = new_str; + *size = new_size; + + /** Success. **/ + return 1; + } + + +/*** Print formatted text using the qprintf formatting. + *** + *** @param s The qprintf session in use. + *** @param str The destination string buffer for printed data. + *** @param strsize The length of `str` in bytes. + *** @param format The qprintf format to follow when printing data. + *** @param ... A variable arguments list used to populate the format. + *** @returns The number of chars written. ***/ int qpfPrintf(pQPSession s, char* str, size_t size, const char* format, ...) @@ -962,13 +999,70 @@ qpfPrintf(pQPSession s, char* str, size_t size, const char* format, ...) } -/*** qpfPrintf_va() - same as qpfPrintf(), but takes a va_list instead of - *** a list of arguments. + +/*** Print formatted text using the qprintf formatting. + *** + *** @param s The qprintf session in use. + *** @param str The destination string buffer for printed data. + *** @param strsize The length of `str` in bytes. + *** @param format The qprintf format to follow when printing data. + *** @param ap A variable arguments list used to populate the format. + *** @returns The number of chars written. ***/ int qpfPrintf_va(pQPSession s, char* str, size_t size, const char* format, va_list ap) { - return qpfPrintf_va_internal(s, &str, &size, qpfPrintf_grow, NULL, format, ap); + return qpfPrintf_gva(s, &str, &size, qpfNoGrow, NULL, format, ap); + } + + +/*** Print formatted text using the qprintf formatting, using provided the + *** grow function to reallocate the provided string as needed. + *** + *** @param s The qprintf session in use. + *** @param str A pointer to the destination string buffer for printed data, + *** which might be reallocated by `grow_fn`, if it is called. + *** @param strsize A pointer to the length value of `str` in bytes, which + *** might be updated by `grow_fn` if it reallocates `str` to a new size. + *** @param grow_fn The grow function called if `str` is not large enough. + *** @param grow_arg The void* argument passed to `grow_fn`. + *** @param format The qprintf format to follow when printing data. + *** @param ... A variable arguments list used to populate the format. + *** @returns The number of chars written. + ***/ +int +qpfPrintf_g(pQPSession s, char** str, size_t* size, qpf_grow_fn_t grow_fn, void* grow_arg, const char* format, ...) + { + va_list va; + int rval; + + /** Grab the va ptr and call the _va version **/ + va_start(va, format); + rval = qpfPrintf_gva(s, str, size, grow_fn, grow_arg, format, va); + va_end(va); + + return rval; + } + + +/*** Print formatted text using the qprintf formatting, using provided the + *** grow function to reallocate the provided string as needed. + *** + *** @param s The qprintf session in use. + *** @param str A pointer to the destination string buffer for printed data, + *** which might be reallocated by `grow_fn`, if it is called. + *** @param strsize A pointer to the length value of `str` in bytes, which + *** might be updated by `grow_fn` if it reallocates `str` to a new size. + *** @param grow_fn The grow function called if `str` is not large enough. + *** @param grow_arg The void* argument passed to `grow_fn`. + *** @param format The qprintf format to follow when printing data. + *** @param ap A variable arguments list used to populate the format. + *** @returns The number of chars written. + ***/ +int +qpfPrintf_gva(pQPSession s, char** str, size_t* size, qpf_grow_fn_t grow_fn, void* grow_arg, const char* format, va_list ap) + { + return qpfPrintf_va_internal(s, str, size, grow_fn, grow_arg, format, ap); } From fc43c9e58c7641509239a4650a569891cf051127 Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Thu, 14 May 2026 14:43:58 -0600 Subject: [PATCH 20/21] Add code to assert error types passed to QPERR() are valid. --- centrallix-lib/src/qprintf.c | 1 + 1 file changed, 1 insertion(+) diff --git a/centrallix-lib/src/qprintf.c b/centrallix-lib/src/qprintf.c index 747636e90..4ba76e105 100644 --- a/centrallix-lib/src/qprintf.c +++ b/centrallix-lib/src/qprintf.c @@ -231,6 +231,7 @@ qpf_internal_count_zeros(int n) #define QPERR(err) ({ \ const unsigned int _err = (err); \ const unsigned int _err_i = qpf_internal_count_zeros(_err); \ + assert(_err_i < QPF_ERR_COUNT) \ if (__LINE__ > USHRT_MAX) \ fprintf(stderr, "Failed to save code location %s:%d: File too long!!\n", __FILE__, __LINE__); \ s->Errors |= _err; \ From b29817c4f2ddddb3a0430e2ec18f3c4e3bc19c19 Mon Sep 17 00:00:00 2001 From: Lightning11wins Date: Thu, 14 May 2026 15:36:57 -0600 Subject: [PATCH 21/21] Fix compiler error. --- centrallix-lib/src/qprintf.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/centrallix-lib/src/qprintf.c b/centrallix-lib/src/qprintf.c index 4ba76e105..6fa7f289c 100644 --- a/centrallix-lib/src/qprintf.c +++ b/centrallix-lib/src/qprintf.c @@ -18,6 +18,7 @@ /* See centrallix-sysdoc/QPrintf.md for more information. */ /************************************************************************/ +#include #include #include #include @@ -231,7 +232,7 @@ qpf_internal_count_zeros(int n) #define QPERR(err) ({ \ const unsigned int _err = (err); \ const unsigned int _err_i = qpf_internal_count_zeros(_err); \ - assert(_err_i < QPF_ERR_COUNT) \ + assert(_err_i < QPF_ERR_COUNT); \ if (__LINE__ > USHRT_MAX) \ fprintf(stderr, "Failed to save code location %s:%d: File too long!!\n", __FILE__, __LINE__); \ s->Errors |= _err; \