diff --git a/centrallix-lib/include/qprintf.h b/centrallix-lib/include/qprintf.h index d638ef684..725b6aec5 100644 --- a/centrallix-lib/include/qprintf.h +++ b/centrallix-lib/include/qprintf.h @@ -1,19 +1,11 @@ #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 */ /* */ -/* 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 */ @@ -22,48 +14,80 @@ /* 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 -typedef int (*qpf_grow_fn_t)(char**, size_t*, size_t, void*, size_t); +#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 */ +#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. + *** @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. + *** @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); + +/*** 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 short 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(); +pQPSession qpfOpenSession(void); 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 6827cc358..6fa7f289c 100644 --- a/centrallix-lib/src/qprintf.c +++ b/centrallix-lib/src/qprintf.c @@ -1,22 +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 */ @@ -30,14 +11,36 @@ /* 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. */ -/* See centrallix-sysdoc/QPrintf.md for more information. */ +/* 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 +#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) @@ -56,7 +59,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 ***/ @@ -104,43 +107,47 @@ const char* qpf_spec_names[] = { NULL, /* 0 */ + + /** Source specifiers. **/ "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 */ + + /** Filter specifiers. **/ + "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 */ @@ -151,6 +158,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]; @@ -166,31 +186,82 @@ 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 }; +/** 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; + + if (UNLIKELY(n == 0)) return 0; + while ((n & 1) == 0) + { + n >>= 1; + shift++; + } + + return shift; + } + +#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; \ + 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()`. + *** + *** @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) { 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; while(pos <= haystacklen - needlelen) { @@ -200,25 +271,34 @@ 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) { - 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; @@ -226,21 +306,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 +334,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 +353,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 +366,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 +379,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 +399,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 +449,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,42 +485,60 @@ 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; - if (!QPF.is_init) - { - if (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 (!s) return NULL; + if (UNLIKELY(!s)) return NULL; s->Errors = 0; + memset(s->ErrorLines, 0, sizeof(s->ErrorLines)); return s; } -/*** 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) @@ -444,18 +546,75 @@ 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, print nothing. + ***/ +void +qpfLogErrors(pQPSession s) + { + if (s == NULL) return; + unsigned int errors = s->Errors; + if (errors == 0) 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); + } + } + } -/*** 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) { s->Errors = 0; + memset(&s->ErrorLines, 0, sizeof(s->ErrorLines)); return 0; } -/*** 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,19 +626,23 @@ 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. + *** @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'; } @@ -493,7 +656,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); @@ -504,7 +667,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 +747,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) @@ -582,7 +774,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; @@ -605,7 +797,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; @@ -615,7 +807,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; @@ -631,7 +823,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; @@ -647,7 +839,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; @@ -664,7 +856,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 An argument, passed to`grow_fn`() when it is called. + *** @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) @@ -678,7 +884,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; @@ -702,7 +908,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; @@ -712,7 +918,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; @@ -730,8 +936,55 @@ qpf_internal_hexdecode(pQPSession s, const char* src, size_t src_size, char** ds } -/*** qpfPrintf() - do the quoting printf operation, given a standard vararg - *** function call. +/*** 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 +qpfNoGrow(char** str, size_t* size, size_t offs, void* arg, size_t req_size) + { + return (*size) >= req_size; + } + + +/*** 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, ...) @@ -748,696 +1001,802 @@ qpfPrintf(pQPSession s, char* str, size_t size, const char* format, ...) } -/*** qpfPrintf_grow() - returns whether the additional size will fit. + +/*** 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_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_grow(char** str, size_t* size, size_t offs, void* arg, size_t req_size) +qpfPrintf_g(pQPSession s, char** str, size_t* size, qpf_grow_fn_t grow_fn, void* grow_arg, const char* format, ...) { - return (*size) >= req_size; + 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; } -/*** qpfPrintf_va() - same as qpfPrintf(), but takes a va_list instead of - *** a list of arguments. +/*** 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_va(pQPSession s, char* str, size_t size, const char* format, va_list ap) +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, qpfPrintf_grow, NULL, format, ap); + return qpfPrintf_va_internal(s, str, size, grow_fn, grow_arg, format, ap); } -/*** 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 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 + *** 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), 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; - int nogrow = (grow_fn == NULL); - - if (srcsize >= SIZE_MAX/2/table->MaxExpand) +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; + 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 (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 (no_grow || (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); + no_grow = true; + 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; } -/*** 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, apply all of the qprintf() format specifier + *** rules, and write the result to a string buffer. + *** + *** 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 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 `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) - { - const char* specptr; - const char* endptr; - size_t copied = 0; - 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; - - if (!QPF.is_init) +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 = -1; + + /** Ensure initialization. **/ + if (UNLIKELY(!QPF.is_init) && UNLIKELY(qpfInitialize() < 0)) return -ENOMEM; + + /** Use a null session (that does nothing with errors) if no session is provided. **/ + QPSession null_session; + if (s == NULL) { - if (qpfInitialize() < 0) return -ENOMEM; + memset(&null_session, 0, sizeof(QPSession)); + s = &null_session; } - - if (!s) + + /** 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))) { - null_session.Errors = 0; - s=&null_session; + rval = -EINVAL; + QPERR(QPF_ERR_T_BUFOVERFLOW); + goto error; } - - /** 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)) - { 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) + + /** 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 (LIKELY(!nogrow) && (LIKELY(cpoffset+2 <= *size) || (grow_fn(str, size, cpoffset, grow_arg, cpoffset+2)))) - (*str)[cpoffset++] = '%'; - else + /** 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++; - format++; + continue; } - else if (UNLIKELY(format[0] == '&')) + case '&': { - if (LIKELY(!nogrow) && (LIKELY(cpoffset+2 <= *size) || (grow_fn(str, size, cpoffset, grow_arg, cpoffset+2)))) - (*str)[cpoffset++] = '&'; - else + /** 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; } - else if (UNLIKELY(format[0] == ']')) + case ']': { format++; - ignore = 0; + 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; } - else if (UNLIKELY(format[0] == '[')) + + /** 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++; - intval = va_arg(ap, int); - if (!intval) + n = va_arg(ap, int); + if (n < 0) n = 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++) + { + /** 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) { - /*while(*format && format[0] != '%' && format[1] != ']') - format++; - if (*format) - format += 2;*/ - ignore = 1; + /** Found it. **/ + format += spec_len; + specchain_n[n_specs] = n; + specchain[n_specs++] = i; + found = true; + break; } } - else + + /** Did we find the format specifier? **/ + if (UNLIKELY(!found)) { - /** Source data specifier. Build the specifier chain **/ - n_specs = 0; - startspec = QPF_SPEC_T_STARTSRC; - endspec = QPF_SPEC_T_ENDSRC; - while(1) + if (UNLIKELY(n_specs == 0u)) { - n = -1; - found = 0; - - /** Is this a numerically-constrained spec? **/ - if (*format >= '0' && *format <= '9') + /** need at least one spec **/ + rval = -EINVAL; + QPERR(QPF_ERR_T_BADFORMAT); + goto error; + } + + /** 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)) + { + rval = -EINVAL; + QPERR(QPF_ERR_T_NULL); + goto error; + } + copy_len = (strval) ? strlen(strval) : 0; + break; + } + + case QPF_SPEC_T_POS: + { + 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; + } + + case QPF_SPEC_T_LL: + { + const long long ll_val = va_arg(ap, long long); + copy_len = (size_t)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 = (size_t)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)) + { + 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; + } + } + + /** If this specifier is ignored, we're done. Skip all filtering/writing logic. **/ + if (UNLIKELY(ignore)) continue; + + /** 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) + { + case QPF_SPEC_T_NLEN: + { + 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 (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 (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] == '.') || - (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 ((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: - if((n=qpf_internal_base64encode(s, strval, cplen, str, size, &cpoffset, grow_fn, grow_arg))<0) - { rval = -EINVAL; goto error; } - else - { - 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 - { - 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 - { - 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 (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 (cplen > maxdst) - { - QPERR(QPF_ERR_T_INSOVERFLOW); - cplen = maxdst; - } - break; - } - if (quote) - { - if (maxdst < 2) - { - QPERR(QPF_ERR_T_BADFORMAT); - rval = -EINVAL; - goto error; - } - maxdst -= 2; - } - if (quote) - { - if (LIKELY(!nogrow) && (LIKELY(cpoffset+1+1 <= *size) || (grow_fn(str, size, cpoffset, grow_arg, cpoffset+1+1)))) - { - (*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 (n < 0) - { - QPERR(QPF_ERR_T_INTERNAL); - rval = n; - goto error; - } - if (n != cpoffset - oldcpoffset) nogrow = 1; - if (quote) - { - if ((LIKELY(cpoffset+1+1 <= *size) || (grow_fn(str, size, cpoffset, grow_arg, cpoffset+1+1)))) - { - (*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 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(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 (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; + } + + /*** Write the closing quote with space for the + *** 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 (LIKELY(*dest_size >= 2)) + { + if (UNLIKELY(space_needed > *dest_size)) + dest_offset = *dest_size - 2; + (*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(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; + 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; } -/*** 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 **/ - if (QPF.n_ext >= QPF_MAX_EXTS) + /** Check if extension max has been reached. **/ + if (UNLIKELY(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; @@ -1445,3 +1804,6 @@ qpfRegisterExt(char* ext_spec, int (*ext_fn)(), int is_source) return; } + +/** Scope cleanup. **/ +#undef QPERR diff --git a/centrallix-lib/tests/test_qprintf_69.c b/centrallix-lib/tests/test_qprintf_69.c new file mode 100644 index 000000000..84c4b5cc4 --- /dev/null +++ b/centrallix-lib/tests/test_qprintf_69.c @@ -0,0 +1,58 @@ +#include +#include +#include + +#include "qprintf.h" + +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 +#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 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.