W5
This commit is contained in:
+77
-25
@@ -10,6 +10,10 @@
|
||||
// handle_complete() changes. CLI, serve_http, and websocket stay native.
|
||||
|
||||
#include "../lib/wasm_trace.h"
|
||||
// The native server TU has the real connectors (sqlite/mysql) compiled in, so
|
||||
// the worker's host-side connector hostcalls are available here. The W3 CLI
|
||||
// driver does not define this and gets a named-trap stub for those imports.
|
||||
#define UCE_WASM_HOST_CONNECTORS 1
|
||||
#include "worker.cpp"
|
||||
|
||||
#include <atomic>
|
||||
@@ -44,6 +48,12 @@ static String wasm_backend_ensure_started(Request* context)
|
||||
path_join(cfg["COMPILER_SYS_PATH"], "bin/wasm/core.wasm"));
|
||||
wc.site_root = path_join(cfg["COMPILER_SYS_PATH"], cfg["SITE_DIRECTORY"]);
|
||||
wc.cache_root = cfg["BIN_DIRECTORY"];
|
||||
// write membrane allowlist: the site tree plus the runtime scratch dirs
|
||||
// pages legitimately write to (matches native reachable write targets).
|
||||
wc.write_roots = { wc.site_root, "/tmp" };
|
||||
for(const char* key : { "BIN_DIRECTORY", "SESSION_PATH", "TMP_UPLOAD_PATH" })
|
||||
if(cfg[key] != "")
|
||||
wc.write_roots.push_back(cfg[key]);
|
||||
wc.memory_limit = (int64_t)config_u64("WASM_MEMORY_LIMIT_BYTES", 512ull * 1024 * 1024);
|
||||
wc.epoch_deadline_ticks = config_u64("WASM_EPOCH_DEADLINE_TICKS", 200);
|
||||
wc.verbose = config_bool("WASM_BACKEND_VERBOSE", false);
|
||||
@@ -75,31 +85,68 @@ static bool wasm_artifact_exists(Request* context, const String& entry_unit)
|
||||
if(entry_unit == "")
|
||||
return(false);
|
||||
String wasm_path = context->server->config["BIN_DIRECTORY"] + entry_unit + ".wasm";
|
||||
struct stat st;
|
||||
return(stat(wasm_path.c_str(), &st) == 0 && S_ISREG(st.st_mode));
|
||||
struct stat wasm_st;
|
||||
if(stat(wasm_path.c_str(), &wasm_st) != 0 || !S_ISREG(wasm_st.st_mode))
|
||||
return(false);
|
||||
// The wasm path bypasses the native JIT recompile, so a source edit would
|
||||
// otherwise be served from a stale .wasm. Require the artifact to be newer
|
||||
// than the source; if it is stale, fall back to native — which JIT-rebuilds
|
||||
// both .so and .wasm — so the next request gets a fresh artifact.
|
||||
struct stat src_st;
|
||||
if(stat(entry_unit.c_str(), &src_st) == 0 && wasm_st.st_mtime < src_st.st_mtime)
|
||||
return(false);
|
||||
return(true);
|
||||
}
|
||||
|
||||
// Decide, by source inspection, whether a page must stay on the native
|
||||
// backend for now (W5 surfaces not yet behind the membrane).
|
||||
//
|
||||
// Caveats this deliberately accepts:
|
||||
// - ENTRY-UNIT ONLY: a clean entry page that component()s into a native-only
|
||||
// unit is still served by wasm. With signals_based_traps off (see
|
||||
// make_engine) the worst case is a clean wasm error or a stubbed-empty
|
||||
// result, never a worker crash — so this is a best-effort routing hint,
|
||||
// not a guarantee. Full coverage is a W5 concern (un-stub the APIs).
|
||||
// - SUBSTRING match, intentionally conservative: a token like "xml_" also
|
||||
// matches an identifier or doc string containing it. That only ever
|
||||
// over-routes to native (always correct), never the reverse.
|
||||
static bool wasm_backend_native_fallback_uncached(Request* context, const String& entry_unit)
|
||||
{
|
||||
String source = file_get_contents(entry_unit);
|
||||
// Supported by the wasm core, intentionally NOT in this list:
|
||||
// - regex_* (host PCRE2 hostcall), xml_*/yaml_*/markdown_* (compiled in),
|
||||
// - unit_render()/component() (host resolver).
|
||||
// What remains is genuinely host-owned / native-only for now:
|
||||
// - zip, sqlite, background tasks, filesystem writes, sleeps;
|
||||
// - unit_call + compiler/unit introspection (need the native toolchain).
|
||||
StringList native_only_tokens = {
|
||||
"zip_", "task(", "task_repeat(",
|
||||
"task_pid(", "task_kill(", "unit_call(",
|
||||
"unit_compile(", "unit_info(", "units_list(", "compiler_load_shared_unit(",
|
||||
"usleep(", "sleep("
|
||||
};
|
||||
for(auto& token : native_only_tokens)
|
||||
if(source.find(token) != String::npos)
|
||||
return(true);
|
||||
return(false);
|
||||
}
|
||||
|
||||
static bool wasm_backend_native_fallback_needed(Request* context, const String& entry_unit)
|
||||
{
|
||||
if(!context || !context->server || entry_unit == "")
|
||||
return(true);
|
||||
String site_root = path_join(context->server->config["COMPILER_SYS_PATH"], context->server->config["SITE_DIRECTORY"]);
|
||||
// W5 keeps native as the reference backend for compiler/docs pages and the
|
||||
// host-owned service surfaces that are not membrane APIs yet. This makes the
|
||||
// default backend safe while W6 decides which fallbacks to retire vs keep.
|
||||
if(entry_unit.rfind(path_join(site_root, "doc") + "/", 0) == 0)
|
||||
return(true);
|
||||
String source = file_get_contents(entry_unit);
|
||||
StringList native_only_tokens = {
|
||||
"markdown_to_", "zip_", "sqlite_", "regex_", "xml_", "yaml_", "task(", "task_repeat(",
|
||||
"task_pid(", "task_kill(", "unit_call(", "unit_render(",
|
||||
"unit_compile(", "unit_info(", "units_list(", "compiler_load_shared_unit(",
|
||||
"file_put_contents(", "file_append(", "usleep(", "sleep("
|
||||
};
|
||||
for(auto& token : native_only_tokens)
|
||||
if(source.find(token) != String::npos)
|
||||
return(true);
|
||||
return(false);
|
||||
// Cache the verdict per process, keyed on path + mtime: the source scan is
|
||||
// otherwise a file read + 20 substring searches on every page request.
|
||||
struct CachedVerdict { time_t mtime; bool fallback; };
|
||||
static std::map<String, CachedVerdict> cache;
|
||||
struct stat st;
|
||||
time_t mtime = (stat(entry_unit.c_str(), &st) == 0) ? st.st_mtime : 0;
|
||||
auto it = cache.find(entry_unit);
|
||||
if(it != cache.end() && it->second.mtime == mtime)
|
||||
return(it->second.fallback);
|
||||
bool fallback = wasm_backend_native_fallback_uncached(context, entry_unit);
|
||||
cache[entry_unit] = { mtime, fallback };
|
||||
return(fallback);
|
||||
}
|
||||
|
||||
// True if this request should be served by the wasm backend. Falls through to
|
||||
@@ -142,12 +189,17 @@ String wasm_backend_serve(Request& request, const String& entry_unit)
|
||||
if(!response.ok)
|
||||
return(response.error == "" ? String("wasm workspace failed") : response.error);
|
||||
|
||||
request.header["X-UCE-Backend"] = "wasm";
|
||||
request.header["X-UCE-Wasm-Workspace-Birth-Us"] = std::to_string(response.workspace_birth_us);
|
||||
request.header["X-UCE-Wasm-Component-Resolve-Count"] = std::to_string(response.component_resolve_count);
|
||||
request.header["X-UCE-Wasm-Component-Resolve-Total-Us"] = std::to_string(response.component_resolve_total_us);
|
||||
request.header["X-UCE-Wasm-Component-Resolve-Avg-Us"] = std::to_string(
|
||||
response.component_resolve_count ? response.component_resolve_total_us / response.component_resolve_count : 0);
|
||||
// Diagnostic timing headers are opt-in: they leak workspace internals and
|
||||
// belong to the W5 benchmark harness, not public responses.
|
||||
if(config_bool("WASM_BACKEND_VERBOSE", false))
|
||||
{
|
||||
request.header["X-UCE-Backend"] = "wasm";
|
||||
request.header["X-UCE-Wasm-Workspace-Birth-Us"] = std::to_string(response.workspace_birth_us);
|
||||
request.header["X-UCE-Wasm-Component-Resolve-Count"] = std::to_string(response.component_resolve_count);
|
||||
request.header["X-UCE-Wasm-Component-Resolve-Total-Us"] = std::to_string(response.component_resolve_total_us);
|
||||
request.header["X-UCE-Wasm-Component-Resolve-Avg-Us"] = std::to_string(
|
||||
response.component_resolve_count ? response.component_resolve_total_us / response.component_resolve_count : 0);
|
||||
}
|
||||
|
||||
// status line: keep the native default unless the unit set one
|
||||
String status = response.meta["status"].to_string();
|
||||
|
||||
+74
-7
@@ -37,14 +37,81 @@ DValue MySQL::get_pending_result() { return(DValue()); }
|
||||
|
||||
String mysql_escape(String raw, char quote_char) { (void)quote_char; return(raw); }
|
||||
|
||||
bool SQLite::connect(String path) { this->path = path; connection = 0; set_error(1, WASM_DB_UNAVAILABLE); return(false); }
|
||||
void SQLite::disconnect() { connection = 0; }
|
||||
String SQLite::error() { return(error_code ? String(WASM_DB_UNAVAILABLE) : String("")); }
|
||||
DValue SQLite::query(String q) { (void)q; set_error(1, WASM_DB_UNAVAILABLE); return(DValue()); }
|
||||
DValue SQLite::query(String q, const StringMap& params) { (void)q; (void)params; set_error(1, WASM_DB_UNAVAILABLE); return(DValue()); }
|
||||
// sqlite runs host-side (the host links libsqlite and owns the connections in
|
||||
// a per-workspace handle table). One UCEB1-marshalled hostcall carries
|
||||
// {op,handle,path,query,params} in and {handle,result,insert_id,affected,
|
||||
// error_code,statement_info} out. `connection` holds the host handle (>0).
|
||||
extern "C" size_t uce_host_sqlite(const char* in, size_t in_len, char* out, size_t cap);
|
||||
|
||||
static DValue wasm_sqlite_call(DValue request)
|
||||
{
|
||||
String encoded = ucb_encode(request);
|
||||
size_t need = uce_host_sqlite(encoded.data(), encoded.size(), 0, 0);
|
||||
if(need == 0)
|
||||
return(DValue());
|
||||
String buffer(need, 0);
|
||||
size_t got = uce_host_sqlite(encoded.data(), encoded.size(), &buffer[0], need);
|
||||
if(got == 0 || got > need)
|
||||
return(DValue());
|
||||
DValue response;
|
||||
String error;
|
||||
ucb_decode(String(buffer.data(), got), response, &error);
|
||||
return(response);
|
||||
}
|
||||
|
||||
void SQLite::set_error(s32 code, String info) { error_code = code; statement_info = info; }
|
||||
bool SQLite::apply_default_pragmas() { return(false); }
|
||||
bool SQLite::bind_params(void* statement, const StringMap& params) { (void)statement; (void)params; return(false); }
|
||||
|
||||
bool SQLite::connect(String path)
|
||||
{
|
||||
this->path = path;
|
||||
DValue request;
|
||||
request["op"] = "connect";
|
||||
request["path"] = path;
|
||||
DValue response = wasm_sqlite_call(request);
|
||||
u64 handle = response["handle"].to_u64();
|
||||
connection = (void*)(uintptr_t)handle;
|
||||
error_code = (s32)response["error_code"].to_s64();
|
||||
statement_info = response["statement_info"].to_string();
|
||||
return(handle != 0 && error_code == 0);
|
||||
}
|
||||
|
||||
void SQLite::disconnect()
|
||||
{
|
||||
if(connection)
|
||||
{
|
||||
DValue request;
|
||||
request["op"] = "disconnect";
|
||||
request["handle"] = (f64)(uintptr_t)connection;
|
||||
wasm_sqlite_call(request);
|
||||
connection = 0;
|
||||
}
|
||||
}
|
||||
|
||||
String SQLite::error()
|
||||
{
|
||||
return(statement_info);
|
||||
}
|
||||
|
||||
DValue SQLite::query(String q, const StringMap& params)
|
||||
{
|
||||
DValue request;
|
||||
request["op"] = "query";
|
||||
request["handle"] = (f64)(uintptr_t)connection;
|
||||
request["query"] = q;
|
||||
for(auto& entry : params)
|
||||
request["params"][entry.first] = entry.second;
|
||||
DValue response = wasm_sqlite_call(request);
|
||||
insert_id = response["insert_id"].to_u64();
|
||||
affected_rows = (u32)response["affected"].to_u64();
|
||||
error_code = (s32)response["error_code"].to_s64();
|
||||
statement_info = response["statement_info"].to_string();
|
||||
DValue* result = response.key("result");
|
||||
return(result ? *result : DValue());
|
||||
}
|
||||
|
||||
DValue SQLite::query(String q) { return(query(q, StringMap())); }
|
||||
bool SQLite::apply_default_pragmas() { return(true); }
|
||||
bool SQLite::bind_params(void* statement, const StringMap& params) { (void)statement; (void)params; return(true); }
|
||||
DValue SQLite::collect_rows(void* statement) { (void)statement; return(DValue()); }
|
||||
|
||||
SQLite* sqlite_connect(String path)
|
||||
|
||||
@@ -6,3 +6,7 @@ uce_host_random
|
||||
uce_host_component_resolve
|
||||
uce_host_file_exists
|
||||
uce_host_file_read
|
||||
uce_host_file_write
|
||||
uce_host_file_unlink
|
||||
uce_host_regex
|
||||
uce_host_sqlite
|
||||
|
||||
+214
-5
@@ -35,6 +35,7 @@
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/time.h>
|
||||
#include <vector>
|
||||
|
||||
@@ -69,6 +70,7 @@ struct WasmWorkerConfig
|
||||
String core_wasm_path = "bin/wasm/core.wasm";
|
||||
String site_root; // absolute
|
||||
String cache_root = "/tmp/uce/work";
|
||||
std::vector<String> write_roots; // absolute prefixes the write membrane allows
|
||||
int64_t memory_limit = 512ll * 1024 * 1024;
|
||||
u32 table_headroom = 4096;
|
||||
u64 epoch_deadline_ticks = 200; // ticker period × ticks = CPU budget
|
||||
@@ -308,6 +310,15 @@ private:
|
||||
{
|
||||
wasmtime::Config config;
|
||||
config.epoch_interruption(true);
|
||||
// CRITICAL: the host (linux_fastcgi.cpp) installs its own SIGSEGV/SIGILL/
|
||||
// SIGBUS handlers per request (install_request_fault_handlers) with plain
|
||||
// signal(), which clobbers Wasmtime's trap-handling handlers. With
|
||||
// signals_based_traps on, a guest `unreachable`/OOB surfaces as a host
|
||||
// signal that the native on_segfault catches and abort()s the worker —
|
||||
// a 502 instead of a clean wasm trap. Disabling it makes Cranelift emit
|
||||
// explicit trap checks: guest traps stay pure wasm traps returned as
|
||||
// Result errors, never a host signal, so the two never collide.
|
||||
config.signals_based_traps(false);
|
||||
return(wasmtime::Engine(std::move(config)));
|
||||
}
|
||||
|
||||
@@ -329,6 +340,18 @@ public:
|
||||
{
|
||||
}
|
||||
|
||||
#ifdef UCE_WASM_HOST_CONNECTORS
|
||||
// Host-owned resource handle table (§3.1): connections opened by the guest
|
||||
// live here and are closed when the workspace drops at request end.
|
||||
std::vector<SQLite*> sqlite_handles;
|
||||
~WasmWorkspace()
|
||||
{
|
||||
for(auto* db : sqlite_handles)
|
||||
if(db)
|
||||
delete db; // ~SQLite disconnects
|
||||
}
|
||||
#endif
|
||||
|
||||
// resolve-kind values shared with the guest core (src/wasm/core.cpp)
|
||||
// must match WasmResolveKind in src/wasm/core.cpp
|
||||
enum ResolveKind { RESOLVE_COMPONENT = 0, RESOLVE_RENDER = 1, RESOLVE_EXISTS = 2, RESOLVE_ONCE = 3 };
|
||||
@@ -794,6 +817,13 @@ private:
|
||||
if(worker.cfg.verbose)
|
||||
fprintf(stderr, "[wasm] loaded %s (mem_base=%u table_base=%u)\n",
|
||||
source_path.c_str(), memory_base, table_base);
|
||||
// The epoch budget is a guest-CPU watchdog, but the ticker is wall-clock
|
||||
// and host-side module compilation here (lazy, mid-render, possibly many
|
||||
// units) burns it without the guest running. Reset the deadline after a
|
||||
// load so the budget measures guest execution between membrane crossings,
|
||||
// not our compile time. A genuine runaway loop makes no loads, so it
|
||||
// still trips the deadline.
|
||||
ctx().set_epoch_deadline(worker.cfg.epoch_deadline_ticks);
|
||||
return("");
|
||||
}
|
||||
|
||||
@@ -864,17 +894,33 @@ private:
|
||||
candidates.push_back(entry_dir + "/" + raw);
|
||||
candidates.push_back(worker.cfg.site_root + "/" + raw);
|
||||
}
|
||||
char site_real[4096];
|
||||
if(!realpath(worker.cfg.site_root.c_str(), site_real))
|
||||
return("");
|
||||
String site_prefix = String(site_real) + "/";
|
||||
// readable roots = the site tree plus the writable scratch dirs (a page
|
||||
// can read back what it is allowed to write, e.g. /tmp), canonicalized.
|
||||
std::vector<String> read_roots;
|
||||
read_roots.push_back(worker.cfg.site_root);
|
||||
for(auto& root : worker.cfg.write_roots)
|
||||
read_roots.push_back(root);
|
||||
std::vector<String> root_prefixes;
|
||||
for(auto& root : read_roots)
|
||||
{
|
||||
char root_real[4096];
|
||||
if(root != "" && realpath(root.c_str(), root_real))
|
||||
root_prefixes.push_back(String(root_real) + "/");
|
||||
}
|
||||
for(auto& candidate : candidates)
|
||||
{
|
||||
char resolved[4096];
|
||||
if(!realpath(candidate.c_str(), resolved))
|
||||
continue;
|
||||
String path(resolved);
|
||||
if(path.rfind(site_prefix, 0) != 0)
|
||||
bool allowed = false;
|
||||
for(auto& prefix : root_prefixes)
|
||||
if(path.rfind(prefix, 0) == 0)
|
||||
{
|
||||
allowed = true;
|
||||
break;
|
||||
}
|
||||
if(!allowed)
|
||||
continue;
|
||||
if(file_exists_host(path))
|
||||
return(path);
|
||||
@@ -882,6 +928,42 @@ private:
|
||||
return("");
|
||||
}
|
||||
|
||||
// write membrane policy: resolve the target (absolute, or relative to the
|
||||
// current unit / site root) and allow it only if its parent directory
|
||||
// canonicalizes under one of the configured write roots (site tree + the
|
||||
// runtime scratch dirs). The file itself need not exist yet.
|
||||
String resolve_guest_write(const String& raw, const String& current_unit)
|
||||
{
|
||||
if(raw == "" || raw.find('\0') != String::npos)
|
||||
return("");
|
||||
String target;
|
||||
if(raw.rfind("/", 0) == 0)
|
||||
target = raw;
|
||||
else
|
||||
{
|
||||
String current_dir = current_unit != "" ? dir_of(current_unit) : String("");
|
||||
target = (current_dir != "" ? current_dir : worker.cfg.site_root) + "/" + raw;
|
||||
}
|
||||
String parent = dir_of(target);
|
||||
String base = parent.size() < target.size() ? target.substr(parent.size() + 1) : String("");
|
||||
if(base == "" || base == "." || base == "..")
|
||||
return("");
|
||||
char parent_real[4096];
|
||||
if(!realpath(parent.c_str(), parent_real))
|
||||
return("");
|
||||
String resolved_parent(parent_real);
|
||||
for(auto& root : worker.cfg.write_roots)
|
||||
{
|
||||
char root_real[4096];
|
||||
if(root == "" || !realpath(root.c_str(), root_real))
|
||||
continue;
|
||||
String root_prefix(root_real);
|
||||
if(resolved_parent == root_prefix || resolved_parent.rfind(root_prefix + "/", 0) == 0)
|
||||
return(resolved_parent + "/" + base);
|
||||
}
|
||||
return("");
|
||||
}
|
||||
|
||||
static String sanitize_symbol_suffix(const String& raw)
|
||||
{
|
||||
// mirrors ascii_safe_name in functionlib.cpp
|
||||
@@ -1062,6 +1144,133 @@ private:
|
||||
results[0] = Val((int32_t)bytes.size());
|
||||
return(std::monostate());
|
||||
}));
|
||||
if(mod == "env" && name == "uce_host_file_write")
|
||||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||||
String path, current, content;
|
||||
self->hostcall_read(args[0].i32(), args[1].i32(), path);
|
||||
self->hostcall_read(args[2].i32(), args[3].i32(), current);
|
||||
self->hostcall_read(args[4].i32(), args[5].i32(), content);
|
||||
bool append = args[6].i32() != 0;
|
||||
String resolved = self->resolve_guest_write(path, current);
|
||||
bool ok = false;
|
||||
if(resolved != "")
|
||||
ok = append ? file_append_contents(resolved, content) : file_put_contents(resolved, content);
|
||||
else if(self->worker.cfg.verbose)
|
||||
fprintf(stderr, "[wasm] file_write denied: %s\n", path.c_str());
|
||||
results[0] = Val(ok ? (int32_t)1 : (int32_t)0);
|
||||
return(std::monostate());
|
||||
}));
|
||||
#ifdef UCE_WASM_HOST_CONNECTORS
|
||||
if(mod == "env" && name == "uce_host_sqlite")
|
||||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||||
// {op,handle,path,query,params} in → result out. The real native
|
||||
// connector runs host-side; connections live in the workspace
|
||||
// handle table (handle = 1-based index).
|
||||
String encoded;
|
||||
self->hostcall_read(args[0].i32(), args[1].i32(), encoded);
|
||||
DValue request, response, decode_err_unused;
|
||||
String decode_error;
|
||||
if(ucb_decode(encoded, request, &decode_error))
|
||||
{
|
||||
String op = request["op"].to_string();
|
||||
if(op == "connect")
|
||||
{
|
||||
SQLite* db = new SQLite();
|
||||
db->connect(request["path"].to_string());
|
||||
u64 handle = 0;
|
||||
if(db->connection)
|
||||
{
|
||||
self->sqlite_handles.push_back(db);
|
||||
handle = self->sqlite_handles.size();
|
||||
}
|
||||
response["handle"] = (f64)handle;
|
||||
response["error_code"] = (f64)db->error_code;
|
||||
response["statement_info"] = db->error();
|
||||
if(handle == 0)
|
||||
delete db;
|
||||
}
|
||||
else
|
||||
{
|
||||
u64 handle = request["handle"].to_u64();
|
||||
SQLite* db = (handle >= 1 && handle <= self->sqlite_handles.size())
|
||||
? self->sqlite_handles[(size_t)handle - 1] : 0;
|
||||
if(op == "query" && db)
|
||||
{
|
||||
StringMap params;
|
||||
DValue* p = request.key("params");
|
||||
if(p)
|
||||
p->each([&](const DValue& value, String key) { params[key] = value.to_string(); });
|
||||
response["result"] = db->query(request["query"].to_string(), params);
|
||||
response["insert_id"] = (f64)db->insert_id;
|
||||
response["affected"] = (f64)db->affected_rows;
|
||||
response["error_code"] = (f64)db->error_code;
|
||||
response["statement_info"] = db->error();
|
||||
}
|
||||
else if(op == "disconnect" && db)
|
||||
{
|
||||
delete db;
|
||||
self->sqlite_handles[(size_t)handle - 1] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
String out = ucb_encode(response);
|
||||
u32 cap = (u32)args[3].i32();
|
||||
int32_t buf = args[2].i32();
|
||||
if(buf != 0 && cap >= out.size())
|
||||
self->hostcall_write(buf, out);
|
||||
results[0] = Val((int32_t)out.size());
|
||||
return(std::monostate());
|
||||
}));
|
||||
#endif
|
||||
if(mod == "env" && name == "uce_host_file_unlink")
|
||||
return(add([self](Caller, Span<const Val> args, Span<Val>) -> Result<std::monostate, Trap> {
|
||||
String path, current;
|
||||
self->hostcall_read(args[0].i32(), args[1].i32(), path);
|
||||
self->hostcall_read(args[2].i32(), args[3].i32(), current);
|
||||
String resolved = self->resolve_guest_write(path, current);
|
||||
if(resolved != "")
|
||||
::unlink(resolved.c_str());
|
||||
return(std::monostate());
|
||||
}));
|
||||
if(mod == "env" && name == "uce_host_regex")
|
||||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||||
// {op,pattern,subject,flags,replacement} in (UCEB1) → result out.
|
||||
// PCRE2 lives host-side; this runs the real native regex_*.
|
||||
String encoded;
|
||||
self->hostcall_read(args[0].i32(), args[1].i32(), encoded);
|
||||
DValue request;
|
||||
String decode_error;
|
||||
DValue response;
|
||||
if(ucb_decode(encoded, request, &decode_error))
|
||||
{
|
||||
String op = request["op"].to_string();
|
||||
String pattern = request["pattern"].to_string();
|
||||
String subject = request["subject"].to_string();
|
||||
String flags = request["flags"].to_string();
|
||||
if(op == "match")
|
||||
response["bool"].set_bool(regex_match(pattern, subject, flags));
|
||||
else if(op == "search")
|
||||
response["tree"] = regex_search(pattern, subject, flags);
|
||||
else if(op == "search_all")
|
||||
response["tree"] = regex_search_all(pattern, subject, flags);
|
||||
else if(op == "replace")
|
||||
response["text"] = regex_replace(pattern, request["replacement"].to_string(), subject, flags);
|
||||
else if(op == "split")
|
||||
for(auto& part : regex_split(pattern, subject, flags))
|
||||
{
|
||||
DValue value;
|
||||
value = part;
|
||||
response["list"].push(value);
|
||||
}
|
||||
}
|
||||
String out = ucb_encode(response);
|
||||
u32 cap = (u32)args[3].i32();
|
||||
int32_t buf = args[2].i32();
|
||||
if(buf != 0 && cap >= out.size())
|
||||
self->hostcall_write(buf, out);
|
||||
results[0] = Val((int32_t)out.size());
|
||||
return(std::monostate());
|
||||
}));
|
||||
if(mod == "env" && name == "uce_host_component_resolve")
|
||||
return(add([self](Caller, Span<const Val> args, Span<Val> results) -> Result<std::monostate, Trap> {
|
||||
String target, current, resolved;
|
||||
|
||||
Reference in New Issue
Block a user