tinymux/mux/modules/engine/funcweb.cpp
Stephen Dennis af97bce20c fix(digest): resolve digest()/hmac() names via EVP_MD_fetch on OpenSSL 3.0+ (#1961)
digest(<name>) and hmac(...,<name>) resolved algorithm names with the legacy
EVP_get_digestbyname(). On OpenSSL 3.0 that does not resolve hyphenated aliases
(e.g. "sha-1") until the default provider has been lazily loaded by an earlier
successful digest. In threaded netmux that warm-up straddles TC005's two cand()
branches, so smoke went red 4/4 on OpenSSL 3.0.13 (Debian 12 / Ubuntu 22.04 /
RHEL 9), while newer OpenSSL (3.6.2) resolves the alias cold and passes.

Use the provider-native EVP_MD_fetch(NULL, name, NULL) on OpenSSL 3.0+
(non-LibreSSL), which resolves aliases deterministically from a cold process,
and free the fetched EVP_MD. Keep EVP_get_digestbyname() on pre-3.0 / LibreSSL,
where it returns a static const and the lazy-provider behavior does not occur.
Gated on OPENSSL_VERSION_NUMBER rather than AC_CHECK_FUNCS to avoid regenerating
configure with autoconf 2.71 vs the tree's required 2.73 (#1477); EVP_MD_fetch
is inherently a 3.0 API so the version guard is semantically exact.

Also fix an EVP_MD_CTX leak in fun_digest on the unsupported-name path (it
returned after EVP_MD_CTX_new() without freeing the context).

Verified on OpenSSL 3.0.13 (Kagura): reverting just these two files gives
TC005 FAIL 3/3; with the fix TC005 PASS 4/4 and full smoke ALL 1588 PASSED.
digest(sha-1)/hmac(...,sha-1) resolve from a cold process; sha_1/bogus still
rejected. Needs 3.6.2 no-regression confirmation on the box that already passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 13:22:07 -06:00

1156 lines
33 KiB
C++

/*! \file funcweb.cpp
* \brief Web integration functions: base64, HMAC, JSON, URL encoding.
*
*/
#include "copyright.h"
#include "autoconf.h"
#include "config.h"
#include "externs.h"
#include "sqlite3.h"
#ifdef UNIX_DIGEST
#include <openssl/hmac.h>
#include <openssl/evp.h>
#endif
// --------------------------------------------------------------------------
// Base64 encode/decode
// --------------------------------------------------------------------------
static const UTF8 b64_encode_table[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
// Decoding table: maps ASCII byte to 0-63, or 0xFF for invalid.
//
static const uint8_t b64_decode_table[256] =
{
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, // 0x00-0x07
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, // 0x08-0x0F
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, // 0x10-0x17
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, // 0x18-0x1F
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, // 0x20-0x27
0xFF,0xFF,0xFF, 62,0xFF,0xFF,0xFF, 63, // 0x28-0x2F (+, /)
52, 53, 54, 55, 56, 57, 58, 59, // 0x30-0x37 (0-7)
60, 61,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, // 0x38-0x3F (8-9)
0xFF, 0, 1, 2, 3, 4, 5, 6, // 0x40-0x47 (A-G)
7, 8, 9, 10, 11, 12, 13, 14, // 0x48-0x4F (H-O)
15, 16, 17, 18, 19, 20, 21, 22, // 0x50-0x57 (P-W)
23, 24, 25,0xFF,0xFF,0xFF,0xFF,0xFF, // 0x58-0x5F (X-Z)
0xFF, 26, 27, 28, 29, 30, 31, 32, // 0x60-0x67 (a-g)
33, 34, 35, 36, 37, 38, 39, 40, // 0x68-0x6F (h-o)
41, 42, 43, 44, 45, 46, 47, 48, // 0x70-0x77 (p-w)
49, 50, 51,0xFF,0xFF,0xFF,0xFF,0xFF, // 0x78-0x7F (x-z)
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, // 0x80-0x87
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, // 0x88-0x8F
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, // 0x90-0x97
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, // 0x98-0x9F
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, // 0xA0-0xA7
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, // 0xA8-0xAF
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, // 0xB0-0xB7
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, // 0xB8-0xBF
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, // 0xC0-0xC7
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, // 0xC8-0xCF
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, // 0xD0-0xD7
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, // 0xD8-0xDF
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, // 0xE0-0xE7
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, // 0xE8-0xEF
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, // 0xF0-0xF7
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, // 0xF8-0xFF
};
// encode64(<string>)
//
// Returns the base64 encoding of <string>.
//
FUNCTION(fun_encode64)
{
UNUSED_PARAMETER(fp);
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
const uint8_t *pIn = reinterpret_cast<const uint8_t *>(fargs[0]);
size_t nIn = strlen(reinterpret_cast<const char *>(fargs[0]));
size_t nTriples = nIn / 3;
size_t nLeftover = nIn % 3;
while (nTriples--)
{
uint32_t stage = (pIn[0] << 16) | (pIn[1] << 8) | pIn[2];
safe_chr(b64_encode_table[(stage >> 18) ], buff, bufc);
safe_chr(b64_encode_table[(stage >> 12) & 0x3F], buff, bufc);
safe_chr(b64_encode_table[(stage >> 6) & 0x3F], buff, bufc);
safe_chr(b64_encode_table[(stage ) & 0x3F], buff, bufc);
pIn += 3;
}
switch (nLeftover)
{
case 1:
{
uint32_t stage = pIn[0] << 16;
safe_chr(b64_encode_table[(stage >> 18) ], buff, bufc);
safe_chr(b64_encode_table[(stage >> 12) & 0x3F], buff, bufc);
safe_chr('=', buff, bufc);
safe_chr('=', buff, bufc);
}
break;
case 2:
{
uint32_t stage = (pIn[0] << 16) | (pIn[1] << 8);
safe_chr(b64_encode_table[(stage >> 18) ], buff, bufc);
safe_chr(b64_encode_table[(stage >> 12) & 0x3F], buff, bufc);
safe_chr(b64_encode_table[(stage >> 6) & 0x3F], buff, bufc);
safe_chr('=', buff, bufc);
}
break;
}
}
// decode64(<base64-string>)
//
// Decodes a base64-encoded string back to its original content.
//
FUNCTION(fun_decode64)
{
UNUSED_PARAMETER(fp);
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
const uint8_t *pIn = reinterpret_cast<const uint8_t *>(fargs[0]);
size_t nIn = strlen(reinterpret_cast<const char *>(fargs[0]));
// Strip trailing padding.
//
size_t nPad = 0;
while (nIn > 0 && pIn[nIn - 1] == '=')
{
nIn--;
nPad++;
}
if (nPad > 2)
{
safe_str(S_("#-1 INVALID BASE64"), buff, bufc);
return;
}
// Decode 4-character groups.
//
uint32_t accum = 0;
int nBits = 0;
for (size_t i = 0; i < nIn; i++)
{
uint8_t v = b64_decode_table[pIn[i]];
if (0xFF == v)
{
safe_str(S_("#-1 INVALID BASE64"), buff, bufc);
return;
}
accum = (accum << 6) | v;
nBits += 6;
if (24 == nBits)
{
safe_chr(static_cast<UTF8>((accum >> 16) & 0xFF), buff, bufc);
safe_chr(static_cast<UTF8>((accum >> 8) & 0xFF), buff, bufc);
safe_chr(static_cast<UTF8>((accum ) & 0xFF), buff, bufc);
accum = 0;
nBits = 0;
}
}
// Flush remaining bits.
//
if (nBits >= 12)
{
// 2 or 3 base64 chars left over → 1 or 2 bytes.
//
accum <<= (24 - nBits);
safe_chr(static_cast<UTF8>((accum >> 16) & 0xFF), buff, bufc);
if (nBits >= 18)
{
safe_chr(static_cast<UTF8>((accum >> 8) & 0xFF), buff, bufc);
}
}
else if (nBits == 6)
{
// A single base64 char is invalid (not enough bits for a byte).
//
safe_str(S_("#-1 INVALID BASE64"), buff, bufc);
return;
}
}
// --------------------------------------------------------------------------
// HMAC
// --------------------------------------------------------------------------
// hmac(<message>, <key>[, <algorithm>])
//
// Returns the hex-encoded HMAC of <message> using <key>.
// Default algorithm is sha256. Any algorithm supported by
// digest() may be used.
//
FUNCTION(fun_hmac)
{
UNUSED_PARAMETER(fp);
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
#ifdef UNIX_DIGEST
const char *pAlgo = "sha256";
if (nfargs >= 3 && fargs[2][0] != '\0')
{
pAlgo = reinterpret_cast<const char *>(fargs[2]);
}
// Provider-native fetch on OpenSSL 3.0+ so aliases resolve from a cold
// process; see fun_digest (#1961). Fetched EVP_MD is a ref to free below.
#if OPENSSL_VERSION_NUMBER >= 0x30000000L && !defined(LIBRESSL_VERSION_NUMBER)
EVP_MD *md = EVP_MD_fetch(nullptr, pAlgo, nullptr);
#else
const EVP_MD *md = EVP_get_digestbyname(pAlgo);
#endif
if (nullptr == md)
{
safe_str(S_("#-1 UNSUPPORTED DIGEST TYPE"), buff, bufc);
return;
}
const char *pMsg = reinterpret_cast<const char *>(fargs[0]);
size_t nMsg = strlen(pMsg);
const char *pKey = reinterpret_cast<const char *>(fargs[1]);
size_t nKey = strlen(pKey);
uint8_t result[EVP_MAX_MD_SIZE];
unsigned int result_len = 0;
const unsigned char *pHmac = HMAC(md,
pKey, static_cast<int>(nKey),
reinterpret_cast<const unsigned char *>(pMsg), nMsg,
result, &result_len);
#if OPENSSL_VERSION_NUMBER >= 0x30000000L && !defined(LIBRESSL_VERSION_NUMBER)
EVP_MD_free(md);
#endif
if (nullptr == pHmac)
{
safe_str(S_("#-1 HMAC FAILED"), buff, bufc);
return;
}
// Output as uppercase hex.
//
safe_hex(result, result_len, true, buff, bufc);
#else
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(fargs);
safe_str(S_("#-1 HMAC REQUIRES SSL"), buff, bufc);
#endif // UNIX_DIGEST
}
// --------------------------------------------------------------------------
// JSON validation (native, RFC 8259)
// --------------------------------------------------------------------------
// Lightweight recursive-descent JSON validator.
// Adapted from ~/slow-32/examples/validatejson.c.
// No memory allocation — walks the input in place.
//
namespace {
struct JsonValidator
{
const UTF8 *buf;
size_t len;
size_t pos;
int depth;
bool at_end() const { return pos >= len; }
UTF8 peek() const { return at_end() ? 0 : buf[pos]; }
UTF8 advance() { return buf[pos++]; }
void skip_ws()
{
while (!at_end())
{
UTF8 c = peek();
if (' ' == c || '\t' == c || '\n' == c || '\r' == c)
{
advance();
}
else
{
break;
}
}
}
bool parse_string()
{
if (at_end() || advance() != '"')
{
return false;
}
while (!at_end())
{
UTF8 c = peek();
if ('"' == c)
{
advance();
return true;
}
if (c < 0x20)
{
return false;
}
if ('\\' == c)
{
advance();
if (at_end())
{
return false;
}
UTF8 e = advance();
switch (e)
{
case '"': case '\\': case '/':
case 'b': case 'f': case 'n': case 'r': case 't':
break;
case 'u':
{
for (int i = 0; i < 4; i++)
{
if (at_end())
{
return false;
}
UTF8 h = advance();
if ( !('0' <= h && h <= '9')
&& !('a' <= h && h <= 'f')
&& !('A' <= h && h <= 'F'))
{
return false;
}
}
// Check for surrogate pairs.
//
// We validated 4 hex digits above; compute the value.
//
const UTF8 *p = buf + pos - 4;
unsigned u1 = 0;
for (int i = 0; i < 4; i++)
{
UTF8 ch = p[i];
unsigned v;
if ('0' <= ch && ch <= '9') v = ch - '0';
else if ('a' <= ch && ch <= 'f') v = 10 + ch - 'a';
else v = 10 + ch - 'A';
u1 = (u1 << 4) | v;
}
if (0xD800 <= u1 && u1 <= 0xDBFF)
{
// High surrogate — must be followed by \uDC00-DFFF.
//
if ( pos + 5 >= len
|| buf[pos] != '\\'
|| buf[pos + 1] != 'u')
{
return false;
}
advance(); advance(); // skip backslash-u
unsigned u2 = 0;
for (int i = 0; i < 4; i++)
{
if (at_end()) return false;
UTF8 ch2 = advance();
unsigned v2;
if ('0' <= ch2 && ch2 <= '9') v2 = ch2 - '0';
else if ('a' <= ch2 && ch2 <= 'f') v2 = 10 + ch2 - 'a';
else if ('A' <= ch2 && ch2 <= 'F') v2 = 10 + ch2 - 'A';
else return false;
u2 = (u2 << 4) | v2;
}
if (u2 < 0xDC00 || u2 > 0xDFFF)
{
return false;
}
}
else if (0xDC00 <= u1 && u1 <= 0xDFFF)
{
// Lone low surrogate.
//
return false;
}
}
break;
default:
return false;
}
}
else
{
// Consume a UTF-8 character.
//
uint8_t b0 = static_cast<uint8_t>(advance());
int extra = 0;
if (b0 < 0x80)
{
extra = 0;
}
else if ((b0 & 0xE0) == 0xC0)
{
extra = 1;
}
else if ((b0 & 0xF0) == 0xE0)
{
extra = 2;
}
else if ((b0 & 0xF8) == 0xF0)
{
extra = 3;
}
else
{
return false;
}
for (int i = 0; i < extra; i++)
{
if (at_end() || (static_cast<uint8_t>(peek()) & 0xC0) != 0x80)
{
return false;
}
advance();
}
}
}
return false; // unterminated string
}
bool parse_number()
{
if (!at_end() && '-' == peek())
{
advance();
}
if (at_end() || peek() < '0' || peek() > '9')
{
return false;
}
if ('0' == peek())
{
advance();
if (!at_end() && peek() >= '0' && peek() <= '9')
{
return false; // leading zero
}
}
else
{
while (!at_end() && peek() >= '0' && peek() <= '9')
{
advance();
}
}
if (!at_end() && '.' == peek())
{
advance();
if (at_end() || peek() < '0' || peek() > '9')
{
return false;
}
while (!at_end() && peek() >= '0' && peek() <= '9')
{
advance();
}
}
if (!at_end() && ('e' == peek() || 'E' == peek()))
{
advance();
if (!at_end() && ('+' == peek() || '-' == peek()))
{
advance();
}
if (at_end() || peek() < '0' || peek() > '9')
{
return false;
}
while (!at_end() && peek() >= '0' && peek() <= '9')
{
advance();
}
}
return true;
}
bool parse_literal(const char *lit)
{
for (const char *p = lit; *p; p++)
{
if (at_end() || peek() != static_cast<UTF8>(*p))
{
return false;
}
advance();
}
return true;
}
bool parse_array()
{
advance(); // '['
if (++depth > 512) return false;
skip_ws();
if (!at_end() && ']' == peek())
{
advance();
depth--;
return true;
}
for (;;)
{
if (!parse_value()) return false;
skip_ws();
if (at_end()) return false;
UTF8 c = advance();
if (']' == c) { depth--; return true; }
if (',' != c) return false;
skip_ws();
}
}
bool parse_object()
{
advance(); // '{'
if (++depth > 512) return false;
skip_ws();
if (!at_end() && '}' == peek())
{
advance();
depth--;
return true;
}
for (;;)
{
if (at_end() || '"' != peek()) return false;
if (!parse_string()) return false;
skip_ws();
if (at_end() || ':' != advance()) return false;
skip_ws();
if (!parse_value()) return false;
skip_ws();
if (at_end()) return false;
UTF8 c = advance();
if ('}' == c) { depth--; return true; }
if (',' != c) return false;
skip_ws();
}
}
bool parse_value()
{
skip_ws();
if (at_end()) return false;
UTF8 c = peek();
if ('"' == c) return parse_string();
if ('{' == c) return parse_object();
if ('[' == c) return parse_array();
if ('t' == c) return parse_literal("true");
if ('f' == c) return parse_literal("false");
if ('n' == c) return parse_literal("null");
if ('-' == c || ('0' <= c && c <= '9')) return parse_number();
return false;
}
bool validate()
{
skip_ws();
if (at_end()) return false;
if (!parse_value()) return false;
skip_ws();
return at_end();
}
};
} // anonymous namespace
// isjson(<string>[, <type>])
//
// Returns 1 if <string> is valid JSON, 0 otherwise.
// Optional <type> checks for a specific JSON type:
// string, number, object, array, true, false, null, boolean.
//
FUNCTION(fun_isjson)
{
UNUSED_PARAMETER(fp);
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
const UTF8 *pStr = fargs[0];
size_t nStr = strlen(reinterpret_cast<const char *>(pStr));
JsonValidator jv;
jv.buf = pStr;
jv.len = nStr;
jv.pos = 0;
jv.depth = 0;
if (!jv.validate())
{
safe_chr('0', buff, bufc);
return;
}
// If a type argument was given, check it.
//
if (nfargs >= 2 && fargs[1][0] != '\0')
{
// Find the first non-whitespace character to determine the type.
//
const UTF8 *p = pStr;
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')
{
p++;
}
bool bMatch = false;
const char *pType = reinterpret_cast<const char *>(fargs[1]);
if (0 == mux_stricmp(fargs[1], T("string")))
{
bMatch = ('"' == *p);
}
else if (0 == mux_stricmp(fargs[1], T("number")))
{
bMatch = ('-' == *p || ('0' <= *p && *p <= '9'));
}
else if (0 == mux_stricmp(fargs[1], T("object")))
{
bMatch = ('{' == *p);
}
else if (0 == mux_stricmp(fargs[1], T("array")))
{
bMatch = ('[' == *p);
}
else if (0 == mux_stricmp(fargs[1], T("true")))
{
bMatch = ('t' == *p);
}
else if (0 == mux_stricmp(fargs[1], T("false")))
{
bMatch = ('f' == *p);
}
else if (0 == mux_stricmp(fargs[1], T("null")))
{
bMatch = ('n' == *p);
}
else if (0 == mux_stricmp(fargs[1], T("boolean")))
{
bMatch = ('t' == *p || 'f' == *p);
}
else
{
safe_str(S_("#-1 UNKNOWN JSON TYPE"), buff, bufc);
return;
}
safe_chr(bMatch ? '1' : '0', buff, bufc);
}
else
{
safe_chr('1', buff, bufc);
}
}
// --------------------------------------------------------------------------
// JSON functions via SQLite JSON1
// --------------------------------------------------------------------------
// Persistent in-memory SQLite handle for JSON operations.
// No table access — purely computational.
//
static sqlite3 *g_pJsonDB = nullptr;
static sqlite3 *json_db()
{
if (nullptr == g_pJsonDB)
{
int rc = sqlite3_open(":memory:", &g_pJsonDB);
if (SQLITE_OK != rc)
{
g_pJsonDB = nullptr;
}
}
return g_pJsonDB;
}
// Helper: run a single-result SQL expression with bound text parameters.
// Returns true and appends the result to buff/bufc.
// Returns false and appends an error message on failure.
//
static bool json_sql_exec(
const char *sql,
const UTF8 *args[], int nArgs,
UTF8 *buff, UTF8 **bufc)
{
sqlite3 *db = json_db();
if (nullptr == db)
{
safe_str(S_("#-1 INTERNAL ERROR"), buff, bufc);
return false;
}
sqlite3_stmt *pStmt = nullptr;
int rc = sqlite3_prepare_v2(db, sql, -1, &pStmt, nullptr);
if (SQLITE_OK != rc)
{
safe_str(S_("#-1 INTERNAL ERROR"), buff, bufc);
return false;
}
for (int i = 0; i < nArgs; i++)
{
sqlite3_bind_text(pStmt, i + 1,
reinterpret_cast<const char *>(args[i]), -1, SQLITE_TRANSIENT);
}
rc = sqlite3_step(pStmt);
if (SQLITE_ROW == rc)
{
int colType = sqlite3_column_type(pStmt, 0);
if (SQLITE_NULL != colType)
{
const UTF8 *pResult = reinterpret_cast<const UTF8 *>(
sqlite3_column_text(pStmt, 0));
if (pResult)
{
safe_str(pResult, buff, bufc);
}
}
sqlite3_finalize(pStmt);
return true;
}
// If we get here, the expression failed. Provide a useful error.
//
const char *pErr = sqlite3_errmsg(db);
if (pErr && strstr(pErr, "malformed JSON"))
{
safe_str(S_("#-1 INVALID JSON"), buff, bufc);
}
else if (pErr && strstr(pErr, "json"))
{
safe_str(S_("#-1 JSON ERROR"), buff, bufc);
}
else
{
safe_str(S_("#-1 JSON ERROR"), buff, bufc);
}
sqlite3_finalize(pStmt);
return false;
}
// json(<type>, <value>)
//
// Construct a JSON value of the given type.
//
// Types:
// string — wraps <value> in quotes with proper escaping
// number — validates and returns a JSON number
// boolean — converts to true/false
// null — returns null (no <value> needed)
// array — treats <value> as a space-separated list, returns JSON array
// object — treats <value> as key value key value..., returns JSON object
//
FUNCTION(fun_json)
{
UNUSED_PARAMETER(fp);
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
const UTF8 *pType = fargs[0];
const UTF8 *pValue = (nfargs >= 2) ? fargs[1] : T("");
if (0 == mux_stricmp(pType, T("string")))
{
// Use SQLite json_quote() for proper escaping.
//
const UTF8 *args[1] = { pValue };
json_sql_exec("SELECT json_quote(?1)", args, 1, buff, bufc);
}
else if (0 == mux_stricmp(pType, T("number")))
{
// Validate as JSON number via SQLite.
//
const UTF8 *args[1] = { pValue };
if (!json_sql_exec("SELECT json(?1)", args, 1, buff, bufc))
{
// json_sql_exec already reported the error. But if the
// input wasn't JSON, try wrapping as a number directly.
// Reset buffer and try as bare number.
}
}
else if (0 == mux_stricmp(pType, T("boolean")))
{
// MUX truthiness: "0", "", "#-1 ..." are false.
//
bool bVal = xlate(const_cast<UTF8 *>(pValue));
safe_str(bVal ? T("true") : T("false"), buff, bufc);
}
else if (0 == mux_stricmp(pType, T("null")))
{
safe_str(T("null"), buff, bufc);
}
else if (0 == mux_stricmp(pType, T("array")))
{
// Build a JSON array from a space-separated list.
//
safe_chr('[', buff, bufc);
UTF8 *pList = pValue ? alloc_lbuf("fun_json.array") : nullptr;
if (pList)
{
mux_strncpy(pList, pValue, LBUF_SIZE - 1);
SEP sep;
sep.n = 1;
sep.str[0] = ' ';
sep.str[1] = '\0';
UTF8 *pWord = trim_space_sep(pList, sep);
bool bFirst = true;
while (pWord && *pWord)
{
// split_token NUL-terminates the element in place;
// next_token would leave the tail attached.
//
UTF8 *pElem = split_token(&pWord, sep);
if (!bFirst)
{
safe_chr(',', buff, bufc);
}
bFirst = false;
// Each element should already be valid JSON.
// Append it directly.
//
safe_str(pElem, buff, bufc);
}
free_lbuf(pList);
}
safe_chr(']', buff, bufc);
}
else if (0 == mux_stricmp(pType, T("object")))
{
// Build a JSON object from key-value pairs.
//
UTF8 *pStart = *bufc;
safe_chr('{', buff, bufc);
UTF8 *pList = pValue ? alloc_lbuf("fun_json.object") : nullptr;
if (pList)
{
mux_strncpy(pList, pValue, LBUF_SIZE - 1);
SEP sep;
sep.n = 1;
sep.str[0] = ' ';
sep.str[1] = '\0';
UTF8 *pWord = trim_space_sep(pList, sep);
bool bFirst = true;
while (pWord && *pWord)
{
// split_token NUL-terminates each token in place;
// next_token would leave the tail attached.
//
UTF8 *pKey = split_token(&pWord, sep);
if (!pWord || !*pWord)
{
// Odd number of elements — missing value. Rewind only
// this function's own output, not the whole buffer.
//
*bufc = pStart;
safe_str(S_("#-1 ODD NUMBER OF ELEMENTS"), buff, bufc);
free_lbuf(pList);
return;
}
UTF8 *pVal = split_token(&pWord, sep);
if (!bFirst)
{
safe_chr(',', buff, bufc);
}
bFirst = false;
// Key should be a JSON string.
//
safe_str(pKey, buff, bufc);
safe_chr(':', buff, bufc);
safe_str(pVal, buff, bufc);
}
free_lbuf(pList);
}
safe_chr('}', buff, bufc);
}
else
{
safe_str(S_("#-1 UNKNOWN JSON TYPE"), buff, bufc);
}
}
// json_query(<json>, <query-type>[, <path>])
//
// Query a JSON value. Query types:
// type — returns the JSON type (object, array, string, number,
// boolean, null)
// size — for arrays/objects, returns element count
// exists — returns 1 if <path> exists, 0 otherwise
// get — extract value at <path> (default if 1 arg after json)
// keys — for objects, returns space-separated list of keys
// values — for objects/arrays, returns space-separated list of values
//
// Paths use SQLite JSON path syntax: $.key.subkey, $[0], etc.
//
FUNCTION(fun_json_query)
{
UNUSED_PARAMETER(fp);
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
const UTF8 *pJson = fargs[0];
// Default operation is "get" if a path is given, "type" if no args.
//
const UTF8 *pOp = T("type");
if (nfargs >= 2 && fargs[1][0] != '\0')
{
pOp = fargs[1];
}
const UTF8 *pPath = (nfargs >= 3) ? fargs[2] : T("$");
if (0 == mux_stricmp(pOp, T("type")))
{
const UTF8 *args[2] = { pJson, pPath };
json_sql_exec("SELECT json_type(?1, ?2)", args, 2, buff, bufc);
}
else if (0 == mux_stricmp(pOp, T("size")))
{
// json_array_length works for arrays; for objects we use
// json_each to count keys.
//
const UTF8 *args[2] = { pJson, pPath };
if (!json_sql_exec(
"SELECT CASE json_type(?1, ?2) "
"WHEN 'array' THEN json_array_length(?1, ?2) "
"WHEN 'object' THEN (SELECT count(*) FROM json_each(?1, ?2)) "
"ELSE 0 END",
args, 2, buff, bufc))
{
// Error already reported.
}
}
else if (0 == mux_stricmp(pOp, T("exists")))
{
const UTF8 *args[2] = { pJson, pPath };
json_sql_exec(
"SELECT CASE WHEN json_type(?1, ?2) IS NOT NULL THEN 1 ELSE 0 END",
args, 2, buff, bufc);
}
else if (0 == mux_stricmp(pOp, T("get")))
{
const UTF8 *args[2] = { pJson, pPath };
json_sql_exec("SELECT json_extract(?1, ?2)", args, 2, buff, bufc);
}
else if (0 == mux_stricmp(pOp, T("keys")))
{
const UTF8 *args[1] = { pJson };
json_sql_exec(
"SELECT group_concat(key, ' ') FROM json_each(?1)",
args, 1, buff, bufc);
}
else if (0 == mux_stricmp(pOp, T("values")))
{
const UTF8 *args[1] = { pJson };
json_sql_exec(
"SELECT group_concat(value, ' ') FROM json_each(?1)",
args, 1, buff, bufc);
}
else
{
safe_str(S_("#-1 UNKNOWN QUERY TYPE"), buff, bufc);
}
}
// json_mod(<json>, <operation>, <path>, <value>)
//
// Modify a JSON value. Operations:
// set — set value at path (create if missing, replace if exists)
// insert — insert value at path (only if missing)
// replace — replace value at path (only if exists)
// remove — remove value at path
// patch — RFC 7396 merge-patch
//
FUNCTION(fun_json_mod)
{
UNUSED_PARAMETER(fp);
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
if (nfargs < 3)
{
safe_str(S_("#-1 TOO FEW ARGUMENTS"), buff, bufc);
return;
}
const UTF8 *pJson = fargs[0];
const UTF8 *pOp = fargs[1];
const UTF8 *pPath = fargs[2];
const UTF8 *pVal = (nfargs >= 4) ? fargs[3] : T("null");
if (0 == mux_stricmp(pOp, T("set")))
{
const UTF8 *args[3] = { pJson, pPath, pVal };
json_sql_exec("SELECT json_set(?1, ?2, json(?3))", args, 3, buff, bufc);
}
else if (0 == mux_stricmp(pOp, T("insert")))
{
const UTF8 *args[3] = { pJson, pPath, pVal };
json_sql_exec("SELECT json_insert(?1, ?2, json(?3))", args, 3, buff, bufc);
}
else if (0 == mux_stricmp(pOp, T("replace")))
{
const UTF8 *args[3] = { pJson, pPath, pVal };
json_sql_exec("SELECT json_replace(?1, ?2, json(?3))", args, 3, buff, bufc);
}
else if (0 == mux_stricmp(pOp, T("remove")))
{
const UTF8 *args[2] = { pJson, pPath };
json_sql_exec("SELECT json_remove(?1, ?2)", args, 2, buff, bufc);
}
else if (0 == mux_stricmp(pOp, T("patch")))
{
const UTF8 *args[2] = { pJson, pPath };
json_sql_exec("SELECT json_patch(?1, ?2)", args, 2, buff, bufc);
}
else
{
safe_str(S_("#-1 UNKNOWN OPERATION"), buff, bufc);
}
}
// --------------------------------------------------------------------------
// URL percent-encoding (RFC 3986)
// --------------------------------------------------------------------------
// Unreserved characters: A-Z a-z 0-9 - _ . ~
//
static bool url_is_unreserved(UTF8 ch)
{
return mux_isalnum(ch) || ch == '-' || ch == '_' || ch == '.' || ch == '~';
}
static const UTF8 hex_digits[] = "0123456789ABCDEF";
XFUNCTION(fun_url_escape)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
UNUSED_PARAMETER(fp);
const UTF8 *p = fargs[0];
while (*p)
{
if (url_is_unreserved(*p))
{
safe_chr(*p, buff, bufc);
}
else
{
safe_chr('%', buff, bufc);
safe_chr(hex_digits[(*p >> 4) & 0x0F], buff, bufc);
safe_chr(hex_digits[*p & 0x0F], buff, bufc);
}
p++;
}
}
static int hex_val(UTF8 ch)
{
if ('0' <= ch && ch <= '9') return ch - '0';
if ('A' <= ch && ch <= 'F') return ch - 'A' + 10;
if ('a' <= ch && ch <= 'f') return ch - 'a' + 10;
return -1;
}
XFUNCTION(fun_url_unescape)
{
UNUSED_PARAMETER(executor);
UNUSED_PARAMETER(caller);
UNUSED_PARAMETER(enactor);
UNUSED_PARAMETER(eval);
UNUSED_PARAMETER(nfargs);
UNUSED_PARAMETER(cargs);
UNUSED_PARAMETER(ncargs);
UNUSED_PARAMETER(fp);
const UTF8 *p = fargs[0];
while (*p)
{
if ('%' == *p && p[1] && p[2])
{
int hi = hex_val(p[1]);
int lo = hex_val(p[2]);
if (hi >= 0 && lo >= 0)
{
safe_chr(static_cast<UTF8>((hi << 4) | lo), buff, bufc);
p += 3;
continue;
}
}
else if ('+' == *p)
{
// application/x-www-form-urlencoded: + is space.
//
safe_chr(' ', buff, bufc);
p++;
continue;
}
safe_chr(*p, buff, bufc);
p++;
}
}