fix: harden UCE runtime and starter

This commit is contained in:
udo
2026-06-11 13:44:24 +00:00
parent 71ddcaf7d4
commit 7f757654b6
128 changed files with 276200 additions and 872 deletions
+43 -2
View File
@@ -18,13 +18,19 @@ RENDER(Request& context)
site_tests_page_start("Core APIs", "Pure helper coverage for strings, UTF-8 splitting, DTree, and JSON encoding/decoding.");
auto comma_parts = split("alpha,beta,gamma", ",");
check("split()", comma_parts.size() == 3 && comma_parts[1] == "beta", join(comma_parts, " | "));
auto empty_delim_parts = split("alpha", "");
check("split()", comma_parts.size() == 3 && comma_parts[1] == "beta" && empty_delim_parts.size() == 1 && empty_delim_parts[0] == "alpha", join(comma_parts, " | "));
auto spaced_parts = split_space(" one two three ");
check("split_space()", spaced_parts.size() == 3 && spaced_parts[2] == "three", join(spaced_parts, " / "));
check("trim()", trim(" padded value ") == "padded value", trim(" padded value "));
StringMap kv = split_kv("\n#comment\nalpha = one\nempty=\n", '=', true, false);
StringMap http_headers = split_http_headers("GET /demo.uce?x=1 HTTP/1.1\r\nHost: example.test\r\nX-Empty:\r\n");
StringMap leading_crlf_headers = split_http_headers("\r\nGET /lead.uce HTTP/1.1\r\nHost: lead.example\r\n");
StringMap header_only = split_http_headers("Host: example.test\nX-Token: abc\n");
check("trim() / split_kv() / split_http_headers()", trim(" padded value ") == "padded value" && kv["alpha"] == "one" && kv["empty"] == "" && http_headers["REQUEST_METHOD"] == "GET" && http_headers["DOCUMENT_URI"] == "/demo.uce" && http_headers["QUERY_STRING"] == "x=1" && http_headers["HTTP_X_EMPTY"] == "" && leading_crlf_headers["REQUEST_METHOD"] == "GET" && leading_crlf_headers["DOCUMENT_URI"] == "/lead.uce" && leading_crlf_headers["HTTP_HOST"] == "lead.example" && header_only["REQUEST_METHOD"] == "" && header_only["HTTP_HOST"] == "example.test" && header_only["HTTP_X_TOKEN"] == "abc", trim(" padded value ") + " / " + var_dump(kv) + " / " + var_dump(http_headers) + " / " + var_dump(leading_crlf_headers) + " / " + var_dump(header_only));
check("replace()", replace("alpha-beta-beta", "beta", "done") == "alpha-done-done", replace("alpha-beta-beta", "beta", "done"));
check("html_escape() attribute-safe quotes", html_escape("<&>\"Don't") == "&lt;&amp;&gt;&quot;Don&#39;t", html_escape("<&>\"Don't"));
check("regex_match()", regex_match("[A-Z][a-z]+", "Alice") && !regex_match("[A-Z][a-z]+", "Alice!"), "full-string validation");
DTree regex_email = regex_search("(?<user>[A-Za-z0-9._%+-]+)@(?<host>[A-Za-z0-9.-]+)", "Contact ops@example.test");
@@ -42,6 +48,41 @@ RENDER(Request& context)
check("str_starts_with()", str_starts_with("websocket-suite", "websocket"), "websocket-suite starts with websocket");
check("str_ends_with()", str_ends_with("component.uce", ".uce"), "component.uce ends with .uce");
check("to_lower() / to_upper()", to_lower("MiXeD") == "mixed" && to_upper("MiXeD") == "MIXED", to_lower("MiXeD") + " / " + to_upper("MiXeD"));
check("request context params", context.params["SCRIPT_URL"] != "" && context.params["BASE_URL"] != "" && context.params["ROUTE_PATH"] != "" && context.params["ROUTE_PAGE"] != "" && context.params["ROUTE_VALID"] == "1", "script=" + context.params["SCRIPT_URL"] + " base=" + context.params["BASE_URL"] + " route=" + context.params["ROUTE_PATH"] + " page=" + context.params["ROUTE_PAGE"] + " valid=" + context.params["ROUTE_VALID"]);
String saved_query_string = context.params["QUERY_STRING"];
context.params["QUERY_STRING"] = "workspace/projects&theme=dark";
check("request_query_path() delegates to request_query_route()", request_query_path(context) == request_query_route(context)["l_path"].to_string() && request_query_path(context) == "workspace/projects", request_query_path(context) + " / " + json_encode(request_query_route(context)));
context.params["QUERY_STRING"] = saved_query_string;
check("route path sanitizers", route_path_normalize("/workspace/projects/") == "workspace/projects" && route_path_is_safe("workspace/projects") && !route_path_is_safe("../demo") && !route_path_is_safe("workspace/../demo") && !route_path_is_safe("workspace/file.uce") && route_path_sanitize("../demo") == "" && route_path_sanitize("") == "index", route_path_sanitize("/workspace/projects/"));
StringList route_parts = {"dashboard", "index", "dashboard", "themes"};
auto unique_routes = list_unique(route_parts);
auto sorted_routes = list_sort(unique_routes);
auto upper_routes = map(sorted_routes, [](String item) { return(to_upper(item)); });
auto dashboard_routes = filter(route_parts, [](String item) { return(item == "dashboard"); });
check("map/filter/list_unique/sort/find/some/every", unique_routes.size() == 3 && sorted_routes[0] == "dashboard" && upper_routes[2] == "THEMES" && dashboard_routes.size() == 2 && list_find(route_parts, [](String item) { return(str_starts_with(item, "them")); }, "missing") == "themes" && list_some(route_parts, [](String item) { return(item == "index"); }) && list_every(unique_routes, [](String item) { return(item != ""); }), join(upper_routes, ","));
DTree nav;
DTree nav_home;
nav_home["title"] = "Home";
nav_home["section"] = "main";
nav.push(nav_home);
DTree nav_dash;
nav_dash["title"] = "Dashboard";
nav_dash["section"] = "app";
nav.push(nav_dash);
DTree nav_themes;
nav_themes["title"] = "Themes";
nav_themes["section"] = "app";
nav.push(nav_themes);
DTree empty_tree;
DTree empty_pop = empty_tree.pop();
DTree app_nav = dtree_filter(nav, [](DTree item, String key) { return(item["section"].to_string() == "app"); });
DTree nav_titles = dtree_map(app_nav, [](DTree item, String key) { DTree title; title = item["title"].to_string(); return(title); });
DTree grouped_nav = dtree_group_by(nav, [](DTree item, String key) { return(item["section"].to_string()); });
DTree nav_dash_summary = dtree_pick(nav_dash, {"title"});
DTree nav_dash_public = dtree_omit(nav_dash, {"section"});
check("DTree collection helpers", empty_pop.to_string() == "" && app_nav.is_list() && nav_titles["0"].to_string() == "Dashboard" && grouped_nav["app"]["1"]["title"].to_string() == "Themes" && join(dtree_keys(nav_dash_summary), ",") == "title" && dtree_values(nav_dash_public)["0"].to_string() == "Dashboard", json_encode(grouped_nav));
String binary_payload = "core";
binary_payload.push_back((char)0x00);
+2 -2
View File
@@ -25,7 +25,7 @@ RENDER(Request& context)
set_cookie("site-tests-cookie", "cookie-value", time() + 300);
context.header["X-Site-Tests"] = "http-suite";
String query_src = "alpha=1&beta=two%20words&gamma=ok";
String query_src = "alpha=1&beta=two%20words&gamma=ok&token=a%3Db&keyless&empty=&trailing=ok&";
StringMap query = parse_query(query_src);
String uri_input = "alpha beta/?x=1&y=2";
String encoded = uri_encode(uri_input);
@@ -43,7 +43,7 @@ RENDER(Request& context)
check("uri_encode() / uri_decode()", decoded == uri_input, encoded + " => " + decoded);
check("uri_decode() malformed percent literals", malformed_percent == "% %A %GG ok done", malformed_percent);
check("parse_uri() empty input", empty_uri.parts["raw"] == "", var_dump(empty_uri));
check("parse_query()", query["alpha"] == "1" && query["beta"] == "two words", query_dump);
check("parse_query()", query["alpha"] == "1" && query["beta"] == "two words" && query["token"] == "a=b" && query["keyless"] == "" && query["empty"] == "" && query["trailing"] == "ok" && query.count("") == 0, query_dump);
check("set_cookie()", set_cookie_dump.find("site-tests-cookie") != String::npos && set_cookie_dump.find("cookie-value") != String::npos && set_cookie_dump.find("HttpOnly") != String::npos && set_cookie_dump.find("SameSite=Lax") != String::npos, set_cookie_dump);
check("response header mutation", context.header["X-Site-Tests"] == "http-suite", header_dump);
check("session_start()", session_id != "", "session_id=" + session_id);
+21 -11
View File
@@ -2,19 +2,29 @@
RENDER(Request& context)
{
String tests_dir = dirname(context.params["SCRIPT_FILENAME"]);
DTree manifest = site_tests_manifest(tests_dir + "/manifest.txt");
StringList entries = list_sort(ls(tests_dir));
site_tests_page_start("Coverage Index", "Public and local-only UCE regression coverage pages.");
?><div class="tests-grid"><?
site_tests_card("core.uce", "Core APIs", "Pure helper coverage for strings, regex, UTF-8, DTree, and JSON.", "public");
site_tests_card("preprocessor.uce", "Preprocessor", "Literal-output parser regression coverage.", "public");
site_tests_card("http.uce", "HTTP And Session", "Request, response, cookie, and session helpers.", "public");
site_tests_card("components.uce", "Components", "component(), props, and component rendering.", "public");
site_tests_card("markdown.uce", "Markdown", "Markdown parsing, rendering, and component hooks.", "public");
site_tests_card("units.uce", "Units", "unit_call(), lifecycle hooks, and unit metadata.", "public");
site_tests_card("websockets.ws.uce", "WebSockets", "Browser-driven WebSocket helper checks.", "public websocket");
site_tests_card("io.uce", "Filesystem", "Filesystem helpers that are restricted outside trusted networks.", "internal");
site_tests_card("zip.uce", "ZIP", "Archive helpers that create and extract temporary server-side files.", "internal");
site_tests_card("services.uce", "Sockets And Services", "Network/service helpers that are restricted outside trusted networks.", "internal");
site_tests_card("tasks.uce", "Tasks", "Background task helper coverage.", "internal");
for(String entry : entries)
{
if(!str_ends_with(entry, ".uce"))
continue;
DTree meta = manifest[entry];
String title = meta["title"].to_string();
String description = meta["description"].to_string();
String tags = meta["tags"].to_string();
if(title == "")
{
site_tests_card(entry, "Missing test metadata: " + entry, "Add this file to site/tests/manifest.txt so the dashboard and network suite stay aligned.", "metadata missing");
continue;
}
if(meta["index"].to_string() == "0")
continue;
site_tests_card(entry, title, description, tags);
}
?></div><?
site_tests_page_end();
}
+17
View File
@@ -0,0 +1,17 @@
# file|title|description|tags|expected|suite|index
index.uce|Coverage Index|Public and local-only UCE regression coverage pages.|http suite uce public|Coverage Index|1|1
core.uce|Core APIs|Pure helper coverage for strings, regex, UTF-8, DTree, and JSON.|http suite uce public|Core APIs|1|1
preprocessor.uce|Preprocessor|Literal-output parser regression coverage.|http suite uce public|top-level )" marker|1|1
http.uce|HTTP And Session|Request, response, cookie, and session helpers.|http suite uce public|HTTP And Session|1|1
components.uce|Components|component(), props, and component rendering.|http suite uce public|Components|1|1
markdown.uce|Markdown|Markdown parsing, rendering, and component hooks.|http suite uce public|Markdown|1|1
units.uce|Units|unit_call(), lifecycle hooks, and unit metadata.|http suite uce public|Units|1|1
websockets.ws.uce|WebSockets|Browser-driven WebSocket helper checks.|http suite uce public websocket|WebSockets|1|1
io.uce|Filesystem|Filesystem helpers that are restricted outside trusted networks.|http suite uce internal|Filesystem|1|1
sqlite.uce|SQLite|SQLite connector with prepared named parameters and DTree rows.|http suite uce internal sqlite|SQLite|1|1
zip.uce|ZIP|Archive helpers that create and extract temporary server-side files.|http suite uce internal|ZIP|1|1
services.uce|Sockets And Services|Network/service helpers that are restricted outside trusted networks.|http suite uce internal|Sockets And Services|1|1
tasks.uce|Tasks|Background task helper coverage.|http suite uce internal|Tasks|1|1
security_headers.uce|Security Header Sanitizer|Low-level response header sanitizer fixture covered by the security smoke suite.|security http internal fixture|security header sanitizer test|0|1
cli.uce|CLI Fixture|Local CLI socket fixture; HTTP render intentionally returns 404.|cli internal fixture|uce-unit-cli|0|0
call_helpers.uce|Unit Call Fixture|Fixture for unit_call and unit_render helper tests.|internal fixture|UNIT_RENDER_FIXTURE|0|0
+7
View File
@@ -1,5 +1,11 @@
#include "testlib.h"
ONCE(Request& context)
@fragment preprocessor-test
{
<>fragment attr once</>
}
String preprocessor_nested_literal()
{
ob_start();
@@ -35,6 +41,7 @@ RENDER(Request& context)
<?
check("raw string terminator in nested literal", contains(nested, "nested )\" marker"), nested);
check("entrypoint @fragment attribute captures output", context.call["fragments"]["preprocessor-test"].to_string() == "fragment attr once", context.call["fragments"]["preprocessor-test"].to_string());
check("inline code island after dangerous literal", true, "parser returned to C++ after rendering literal content containing )\"");
site_tests_summary(passed, failed, skipped, "Literal content containing the C++ raw-string terminator sequence must compile and render unchanged.");
+11
View File
@@ -86,6 +86,17 @@ RENDER(Request& context)
);
}
MySQL placeholder_guard;
StringMap mysql_params;
mysql_params["id"] = "1";
placeholder_guard.query("select ?", mysql_params);
String mysql_placeholder_error = placeholder_guard.error();
mark(
"mysql_query() rejects positional placeholders",
mysql_placeholder_error.find("positional ? placeholders are not supported") != String::npos ? "pass" : "fail",
mysql_placeholder_error
);
MySQL mysql;
mysql.connect("127.0.0.1", "root", "");
String mysql_error_text = trim(mysql.error());
+93
View File
@@ -0,0 +1,93 @@
#include "testlib.h"
RENDER(Request& context)
{
u64 passed = 0;
u64 failed = 0;
u64 skipped = 0;
auto check = [&](String name, bool ok, String detail)
{
site_tests_case(name, ok ? "pass" : "fail", detail);
if(ok)
passed++;
else
failed++;
};
site_tests_page_start("SQLite", "SQLite connector coverage for prepared params, result typing, metadata, and cleanup.");
String db_path = "/tmp/uce-site-tests-sqlite.sqlite";
file_unlink(db_path);
file_unlink(db_path + "-wal");
file_unlink(db_path + "-shm");
SQLite* db = sqlite_connect(db_path);
check("sqlite_connect()", db && db->connection != 0 && sqlite_error(db) == "connected", sqlite_error(db));
sqlite_query(db, "create table users(id integer primary key autoincrement, email text not null unique, visits integer not null, rating real not null)");
check("sqlite create table", sqlite_error(db) == "ok", sqlite_error(db));
sqlite_query(db, "create table dropped(id integer); insert into dropped values(1);");
String multi_statement_error = sqlite_error(db);
StringMap dropped_params;
dropped_params["name"] = "dropped";
DTree dropped_tables = sqlite_query(db, "select name from sqlite_master where type = 'table' and name = :name", dropped_params);
check("sqlite_query() rejects multi-statement SQL", multi_statement_error.find("exactly one SQL statement") != String::npos && dropped_tables["0"]["name"].to_string() == "", multi_statement_error + " tables=" + json_encode(dropped_tables));
StringMap ada;
ada["email"] = "ada@example.test";
ada["visits"] = "7";
ada["rating"] = "4.5";
sqlite_query(db, "insert into users(email, visits, rating) values(:email, :visits, :rating)", ada);
u64 ada_id = sqlite_insert_id(db);
check("sqlite named insert", ada_id == 1 && sqlite_affected_rows(db) == 1, "id=" + std::to_string(ada_id) + " affected=" + std::to_string((u64)sqlite_affected_rows(db)) + " error=" + sqlite_error(db));
StringMap bob;
bob["email"] = "bob@example.test";
bob["visits"] = "3";
bob["rating"] = "2.25";
sqlite_query(db, "insert into users(email, visits, rating) values(:email, :visits, :rating)", bob);
check("sqlite second insert", sqlite_insert_id(db) == 2 && sqlite_affected_rows(db) == 1, "id=" + std::to_string(sqlite_insert_id(db)));
StringMap query_params;
query_params["min_visits"] = "4";
DTree rows = sqlite_query(db, "select id, email, visits, rating from users where visits >= :min_visits order by id", query_params);
check("sqlite_query() rows", rows.is_array() && rows["0"]["email"].to_string() == "ada@example.test" && rows["0"]["visits"].to_s64() == 7 && rows["0"]["rating"].to_f64() > 4.49, json_encode(rows));
sqlite_query(db, "select id from users where id = ?", query_params);
String positional_error = sqlite_error(db);
check("sqlite_query() rejects positional placeholders", positional_error.find("positional ? placeholders are not supported") != String::npos, positional_error);
sqlite_query(db, "select id from users where id = @id", query_params);
String non_colon_error = sqlite_error(db);
check("sqlite_query() rejects non-colon placeholders", non_colon_error.find("only supports :name placeholders") != String::npos, non_colon_error);
sqlite_query(db, "update users set visits = visits + 1 where email = :email", ada);
check("sqlite_affected_rows()", sqlite_affected_rows(db) == 1, std::to_string((u64)sqlite_affected_rows(db)));
sqlite_query(db, "select nope from missing_table");
check("sqlite_error()", sqlite_error(db).find("sqlite prepare failed") != String::npos && sqlite_error(db).find("no such table") != String::npos, sqlite_error(db));
sqlite_disconnect(db);
String cleanup_db_path = db_path + ".cleanup";
file_unlink(cleanup_db_path);
file_unlink(cleanup_db_path + "-wal");
file_unlink(cleanup_db_path + "-shm");
SQLite* cleanup_db = sqlite_connect(cleanup_db_path);
sqlite_query(cleanup_db, "create table cleanup_probe(id integer)");
cleanup_sqlite_connections();
check("sqlite request cleanup releases tracked wrappers", context.resources.sqlite_connections.size() == 0, "tracked=" + std::to_string((u64)context.resources.sqlite_connections.size()));
sqlite_disconnect(cleanup_db);
file_unlink(cleanup_db_path);
file_unlink(cleanup_db_path + "-wal");
file_unlink(cleanup_db_path + "-shm");
file_unlink(db_path);
file_unlink(db_path + "-wal");
file_unlink(db_path + "-shm");
site_tests_summary(passed, failed, skipped, "SQLite tests write only under /tmp/uce-site-tests-sqlite.sqlite and clean up WAL/SHM sidecars.");
site_tests_page_end();
}
+23
View File
@@ -64,6 +64,29 @@ void site_tests_card(String href, String title, String description, String tags
print("</a>");
}
DTree site_tests_manifest(String manifest_path = "manifest.txt")
{
DTree manifest;
for(String line : split(file_get_contents(manifest_path), "\n"))
{
line = trim(line);
if(line == "" || line[0] == '#')
continue;
StringList parts = split(line, "|");
if(parts.size() < 7)
continue;
String file = trim(parts[0]);
manifest[file]["file"] = file;
manifest[file]["title"] = trim(parts[1]);
manifest[file]["description"] = trim(parts[2]);
manifest[file]["tags"] = trim(parts[3]);
manifest[file]["expected"] = trim(parts[4]);
manifest[file]["suite"] = trim(parts[5]);
manifest[file]["index"] = trim(parts[6]);
}
return(manifest);
}
void site_tests_summary(u64 passed, u64 failed, u64 skipped, String note = "")
{
print("<div class=\"tests-summary\">");