366 lines · 9.5 KB · C
RawDownload
// SPDX-License-Identifier: ISC/* * Lua JSON bindings over cJSON. Exposes a global "json" table with: *   json.encode(value) -> string *   json.decode(string) -> value *   json.null          -> lightuserdata sentinel */#include <stdbool.h>#include <stddef.h>#include <stdlib.h>#include <string.h> #include <cjson/cJSON.h> #include <lua.h>#include <lauxlib.h>#include <lualib.h> #include "banned.h" /* Prototype — called from lua_plugin.c. */int clm_lua_json_open(lua_State *L); /* Push a cJSON object as a Lua value (table/string/number/etc). * Called from lua_plugin.c to decode tool args without a Lua hop. */void clm_lua_push_json_value(lua_State *L, cJSON *obj); /* Sentinel for JSON null. */static char json_null_sentinel; /* ------------------------------------------------------------------ *//* decode: JSON string -> Lua value                                    *//* ------------------------------------------------------------------ */ voidclm_lua_push_json_value(lua_State *L, cJSON *obj){	if (obj == NULL) {		lua_pushlightuserdata(L, &json_null_sentinel);		return;	} 	if (cJSON_IsNull(obj)) {		lua_pushlightuserdata(L, &json_null_sentinel);		return;	}	if (cJSON_IsBool(obj)) {		lua_pushboolean(L, cJSON_IsTrue(obj));		return;	}	if (cJSON_IsNumber(obj)) {		double val = obj->valuedouble;		lua_Integer ival = (lua_Integer)val;		/* Exact round-trip check is intentional: only take the integer		 * fast path when the double truly has no fractional part. */#pragma GCC diagnostic push#pragma GCC diagnostic ignored "-Wfloat-equal"		if ((double)ival == val) {#pragma GCC diagnostic pop			lua_pushinteger(L, ival);		} else {			lua_pushnumber(L, val);		}		return;	}	if (cJSON_IsString(obj)) {		lua_pushlstring(L, obj->valuestring, strlen(obj->valuestring));		return;	}	if (cJSON_IsArray(obj)) {		int size = cJSON_GetArraySize(obj);		lua_createtable(L, size, 0);		for (int i = 0; i < size; i++) {			cJSON *item = cJSON_GetArrayItem(obj, i);			clm_lua_push_json_value(L, item);			lua_rawseti(L, -2, i + 1);		}		return;	}	if (cJSON_IsObject(obj)) {		lua_newtable(L);		for (cJSON *child = obj->child; child != NULL;		    child = child->next) {			lua_pushstring(L, child->string);			clm_lua_push_json_value(L, child);			lua_rawset(L, -3);		}		return;	}	/* Should not reach here */	lua_pushliteral(L, "[invalid JSON type]");} /* ------------------------------------------------------------------ *//* decode: JSON string -> Lua value                                    *//* ------------------------------------------------------------------ */ static intlua_json_decode(lua_State *L){	size_t len;	const char *str = luaL_checklstring(L, 1, &len);	cJSON *obj; 	/* Enforce per-plugin decode size limit. */	lua_getfield(L, LUA_REGISTRYINDEX, "_clm_json_max");	size_t max_len = (size_t)lua_tointeger(L, -1);	lua_pop(L, 1);	if (max_len > 0 && len > max_len)		return luaL_error(L, "json.decode: input too large (%zu > %zu)",		    len, max_len); 	obj = cJSON_ParseWithLengthOpts(str, len, NULL, 0);	if (obj == NULL) {		const char *error_ptr = cJSON_GetErrorPtr();		if (error_ptr != NULL) {			return luaL_error(L,			    "json.decode: invalid JSON at offset %td",			    error_ptr - str);		} else {			return luaL_error(L, "json.decode: out of memory");		}	} 	clm_lua_push_json_value(L, obj);	cJSON_Delete(obj);	return 1;} /* ------------------------------------------------------------------ *//* encode: Lua value -> JSON string                                    *//* ------------------------------------------------------------------ */ static cJSON *lua_to_json(lua_State *L, int idx, int depth); /* Registry key for the metatable set by json.array() to force array encoding. */#define CLM_JSON_ARRAY_MT "clm_json_array" /* True if the table at idx carries the json.array() marker metatable. */static boolhas_array_marker(lua_State *L, int idx){	if (!lua_getmetatable(L, idx))		return false;	luaL_getmetatable(L, CLM_JSON_ARRAY_MT);	bool marked = lua_rawequal(L, -1, -2);	lua_pop(L, 2);	return marked;} /* * Decide whether a Lua table encodes as a JSON array or object. * * Lua cannot distinguish an empty array from an empty object -- both are {}. * We default an empty (or otherwise ambiguous) table to a JSON OBJECT, because * empty objects dominate in practice (notably JSON Schema, e.g. an empty * "properties": {}). A strict server (Nova) rejects "properties": [], so the * object default is the safe one. To force an empty (or any) table to encode * as an array, wrap it with json.array(). */static boolis_lua_array(lua_State *L, int idx){	if (has_array_marker(L, idx))		return true; 	size_t len = lua_rawlen(L, idx);	if (len == 0)		return false; /* ambiguous/empty: default to object */ 	/* Verify no keys beyond 1..len. */	lua_Integer count = 0;	lua_pushnil(L);	while (lua_next(L, idx) != 0) {		lua_pop(L, 1); /* pop value, keep key for iteration */		count++;		if (count > (lua_Integer)len) {			lua_pop(L, 1); /* pop the key before returning */			return false;  /* extra keys beyond rawlen */		}	}	return count == (lua_Integer)len;} static cJSON *lua_to_json(lua_State *L, int idx, int depth); static intcmp_key(const void *a, const void *b){	return strcmp(*(const char *const *)a, *(const char *const *)b);} /* * Convert a Lua table to a JSON object, keys sorted. Lua hashes strings with * a per-process seed, so lua_next order varies between runs, and a tool * schema built here has to stay byte-identical for a provider to reuse its * cached prompt prefix. Falls back to lua_next order on allocation failure. */static cJSON *lua_table_to_object(lua_State *L, int idx, int depth){	cJSON *obj = cJSON_CreateObject();	char **keys = NULL;	size_t n = 0, cap = 0, i; 	lua_pushnil(L);	while (lua_next(L, idx) != 0) {		if (lua_type(L, -2) == LUA_TSTRING) {			const char *key = lua_tostring(L, -2); 			if (n == cap) {				size_t ncap = cap ? cap * 2 : 8;				char **p = realloc(keys, ncap * sizeof(*keys)); 				if (p == NULL) {					/* Out of memory: finish unsorted. */					cJSON_AddItemToObject(obj, key,					    lua_to_json(L, -1, depth + 1));					lua_pop(L, 1);					continue;				}				keys = p;				cap = ncap;			}			keys[n] = strdup(key);			if (keys[n] == NULL)				cJSON_AddItemToObject(				    obj, key, lua_to_json(L, -1, depth + 1));			else				n++;		}		lua_pop(L, 1);	} 	/* An empty table leaves keys NULL, and qsort's array argument is	 * declared non-null: passing NULL is undefined even for zero	 * elements, and traps under a checking build. */	if (n > 1)		qsort(keys, n, sizeof(*keys), cmp_key);	for (i = 0; i < n; i++) {		lua_getfield(L, idx, keys[i]);		cJSON_AddItemToObject(		    obj, keys[i], lua_to_json(L, -1, depth + 1));		lua_pop(L, 1);		free(keys[i]);	}	free(keys);	return obj;} static cJSON *lua_to_json(lua_State *L, int idx, int depth){	if (depth > 64)		return NULL; /* prevent stack overflow on deeply nested tables		              */ 	idx = lua_absindex(L, idx); 	switch (lua_type(L, idx)) {	case LUA_TNIL:		return NULL; /* cJSON treats NULL as null in arrays/objects */	case LUA_TBOOLEAN:		return cJSON_CreateBool(lua_toboolean(L, idx));	case LUA_TNUMBER:		if (lua_isinteger(L, idx))			return cJSON_CreateNumber(lua_tointeger(L, idx));		return cJSON_CreateNumber(lua_tonumber(L, idx));	case LUA_TSTRING: {		size_t slen;		const char *s = lua_tolstring(L, idx, &slen);		return cJSON_CreateString(s);	}	case LUA_TLIGHTUSERDATA:		/* json.null sentinel */		if (lua_touserdata(L, idx) == &json_null_sentinel)			return NULL;		return cJSON_CreateString("(userdata)");	case LUA_TTABLE: {		if (is_lua_array(L, idx)) {			size_t len = lua_rawlen(L, idx);			cJSON *arr = cJSON_CreateArray();			for (size_t i = 1; i <= len; i++) {				lua_rawgeti(L, idx, (lua_Integer)i);				cJSON_AddItemToArray(				    arr, lua_to_json(L, -1, depth + 1));				lua_pop(L, 1);			}			return arr;		}		return lua_table_to_object(L, idx, depth);	}	default:		return cJSON_CreateString("(unsupported type)");	}} static intlua_json_encode(lua_State *L){	cJSON *obj;	char *s; 	luaL_checkany(L, 1);	obj = lua_to_json(L, 1, 0); 	if (obj == NULL) {		lua_pushstring(L, "null");		return 1;	}	s = cJSON_PrintUnformatted(obj);	if (s == NULL) {		cJSON_Delete(obj);		return luaL_error(L, "json.encode: out of memory");	}	lua_pushstring(L, s);	free(s);	cJSON_Delete(obj);	return 1;} /* ------------------------------------------------------------------ *//* Module registration                                                 *//* ------------------------------------------------------------------ */ /* * json.array(t) -> t : mark a table so json.encode always encodes it as a JSON * array, even when empty. Needed only for the (rare) empty-array case, since a * non-empty sequence is detected automatically and empty tables default to an * object. Returns the same table for convenient inline use. */static intlua_json_array(lua_State *L){	luaL_checktype(L, 1, LUA_TTABLE);	lua_settop(L, 1);	luaL_getmetatable(L, CLM_JSON_ARRAY_MT);	lua_setmetatable(L, 1);	return 1;} static const luaL_Reg json_funcs[] = {    {"decode", lua_json_decode},    {"encode", lua_json_encode},    {"array", lua_json_array},    {NULL, NULL},}; intclm_lua_json_open(lua_State *L){	lua_newtable(L);	luaL_setfuncs(L, json_funcs, 0); 	/* Marker metatable for json.array(); created once per state. */	luaL_newmetatable(L, CLM_JSON_ARRAY_MT);	lua_pop(L, 1); 	/* json.null sentinel */	lua_pushlightuserdata(L, &json_null_sentinel);	lua_setfield(L, -2, "null"); 	lua_setglobal(L, "json");	return 0;}