Add archive helpers and harden task runtime

This commit is contained in:
udo
2026-05-21 00:00:23 +00:00
parent 9f7625c7fd
commit 02e153a6a7
71 changed files with 12817 additions and 305 deletions
+3
View File
@@ -37,6 +37,8 @@ RENDER(Request& context)
<div class="grid-heading">Data Types &amp; Parsing</div>
<? render_card("dtree.uce", "DTree", "Dynamic hierarchical data tree"); ?>
<? render_card("json.uce", "JSON", "Parse and encode JSON data"); ?>
<? render_card("xml.uce", "XML", "Structural XML encode/decode with DTree"); ?>
<? render_card("yaml.uce", "YAML", "Concise config files with DTree"); ?>
<? render_card("preprocessor-comments.uce", "Preprocessor Comments", "Regression coverage for comment parsing in templates"); ?>
<? render_card("regex.uce", "Regular Expressions", "PCRE2 matching, captures, replacement, and splitting"); ?>
<? render_card("string.uce", "String", "String operations"); ?>
@@ -54,6 +56,7 @@ RENDER(Request& context)
<div class="grid-heading">Storage &amp; I/O</div>
<? render_card("fileio.uce", "File I/O", "Read and write files"); ?>
<? if(allow_server_demos) { render_card("zip.uce", "ZIP", "Create, list, read, and extract ZIP archives"); } ?>
<? if(allow_server_demos) { render_card("file_append.uce", "File Append", "Append data to files"); } ?>
<? if(allow_server_demos) { render_card("shell.uce", "Shell", "Execute shell commands"); } ?>
<? if(allow_server_demos) { render_card("memcached.uce", "Memcached", "Memcached key-value store"); } ?>
+1 -1
View File
@@ -68,7 +68,7 @@ RENDER(Request& context)
<pre style="white-space: pre-wrap"><?
print("New Task ID: ", task("example-task", []() {
print("New Task ID: ", task(task_name, []() {
sleep(10);
+1 -1
View File
@@ -68,7 +68,7 @@ RENDER(Request& context)
<pre style="white-space: pre-wrap"><?
print("New Task ID: ", task_repeat("example-task", 5, []() {
print("New Task ID: ", task_repeat(task_name, 5, []() {
sleep(1);
+61
View File
@@ -0,0 +1,61 @@
#include "demo_guard.h"
RENDER(Request& context)
{
DTree book;
book["name"] = "book";
book["attrs"]["id"] = "b1";
book["attrs"]["type"] = "reference";
DTree title;
title["name"] = "title";
title["text"] = "UCE & XML";
book["children"].push(title);
DTree chapter;
chapter["name"] = "chapter";
chapter["attrs"]["number"] = "1";
chapter["text"] = "Structural conversion without schema validation.";
book["children"].push(chapter);
String encoded = xml_encode(book);
DTree decoded = xml_decode(encoded);
DTree simple;
simple["title"] = "Hello";
simple["count"] = "3";
String incoming = "<note priority=\"high\"><to>UCE</to><body><![CDATA[5 < 6]]></body><symbol>&#x41;&#66;</symbol></note>";
DTree incoming_tree = xml_decode(incoming);
<>
<link rel="stylesheet" href='style.css'></link>
<h1>
<a href="index.uce">UCE Test</a>:
XML
</h1>
<p><code>xml_encode()</code> and <code>xml_decode()</code> convert between XML strings and element-shaped <code>DTree</code> values without schema validation.</p>
<h2>Encoded Element Tree</h2>
<pre><?= encoded ?></pre>
<h2>Decoded DTree</h2>
<pre><?= json_encode(decoded) ?></pre>
<h2>Simple Map Encoding</h2>
<pre><?= xml_encode(simple, "payload") ?></pre>
<h2>Decode Existing XML</h2>
<p>Input XML:</p>
<pre><?= incoming ?></pre>
<p>Decoded DTree:</p>
<pre><?= json_encode(incoming_tree) ?></pre>
<p>
<a href="../doc/index.uce?p=xml_encode">xml_encode() docs</a>
|
<a href="../doc/index.uce?p=xml_decode">xml_decode() docs</a>
</p>
</>
}
+68
View File
@@ -0,0 +1,68 @@
#include "demo_guard.h"
RENDER(Request& context)
{
String source = "# UCE app config\n"
"app:\n"
" name: UCE Starter\n"
" debug: true\n"
" port: 8080\n"
" paths:\n"
" - site\n"
" - cache\n"
"message: |\n"
" Keep config files readable.\n"
" Load them as DTree values.\n";
DTree cfg = yaml_decode(source);
String encoded = yaml_encode(cfg);
DTree roundtrip = yaml_decode(encoded);
DTree generated;
generated["database"]["host"] = "localhost";
generated["database"]["port"] = (f64)3306;
generated["features"]["components"].set_bool(true);
generated["features"]["markdown"].set_bool(true);
DTree theme;
theme = "clean";
generated["themes"].push(theme);
theme = "compact";
generated["themes"].push(theme);
<>
<link rel="stylesheet" href='style.css'></link>
<h1>
<a href="index.uce">UCE Test</a>:
YAML
</h1>
<p><code>yaml_encode()</code> and <code>yaml_decode()</code> convert concise config-style YAML to and from <code>DTree</code> values.</p>
<h2>Config Source</h2>
<pre><?= source ?></pre>
<h2>Decoded DTree</h2>
<pre><?= json_encode(cfg) ?></pre>
<h2>Encoded Again</h2>
<pre><?= encoded ?></pre>
<h2>Generated Config</h2>
<pre><?= yaml_encode(generated) ?></pre>
<h2>Round Trip Reads</h2>
<pre><?
print("app.name = ", roundtrip["app"]["name"].to_string(), "\n");
print("app.debug = ", roundtrip["app"]["debug"].to_bool() ? "true" : "false", "\n");
print("app.port = ", std::to_string(roundtrip["app"]["port"].to_s64()), "\n");
print("app.paths[1] = ", roundtrip["app"]["paths"]["1"].to_string(), "\n");
?></pre>
<p>
<a href="../doc/index.uce?p=yaml_encode">yaml_encode() docs</a>
|
<a href="../doc/index.uce?p=yaml_decode">yaml_decode() docs</a>
</p>
</>
}
+49
View File
@@ -0,0 +1,49 @@
#include "demo_guard.h"
RENDER(Request& context)
{
if(!test_demo_request_allowed(context))
{
test_demo_render_restricted_html(context, "ZIP Demo", "create and extract temporary server-side archive files");
return;
}
String base = "/tmp/uce-demo-zip";
String archive = path_join(base, "demo.zip");
String extract_dir = path_join(base, "extract");
mkdir(base);
mkdir(extract_dir);
DTree entries;
entries["hello.txt"] = "Hello from a generated ZIP archive.\n";
entries["notes/readme.txt"] = "zip_create(), zip_list(), zip_read(), and zip_extract() are available to UCE pages.\n";
zip_create(archive, entries);
DTree listing = zip_list(archive);
String hello = zip_read(archive, "hello.txt");
zip_extract(archive, extract_dir);
String extracted = file_get_contents(path_join(extract_dir, "notes/readme.txt"));
String gz_source = "This string was compressed with gz_compress() and restored with gz_uncompress().";
String gz_body = gz_compress(gz_source);
String gz_roundtrip = gz_uncompress(gz_body);
?><html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1"></meta>
<link rel="stylesheet" href="style.css?v=<?= time() ?>"></link>
</head>
<body>
<h1><a href="index.uce">UCE Test Suite</a> / ZIP</h1>
<p>This demo creates a temporary archive at <code><?= archive ?></code>, lists it, reads one member, and extracts it under <code><?= extract_dir ?></code>.</p>
<h2>zip_list()</h2>
<pre><?= json_encode(listing) ?></pre>
<h2>zip_read()</h2>
<pre><?= hello ?></pre>
<h2>Extracted File</h2>
<pre><?= extracted ?></pre>
<h2>gzip Helpers</h2>
<p>Source bytes: <?= std::to_string((u64)gz_source.size()) ?>; compressed bytes: <?= std::to_string((u64)gz_body.size()) ?></p>
<pre><?= gz_roundtrip ?></pre>
</body>
</html><?
}
+4
View File
@@ -1,3 +1,7 @@
Markup Functions
markdown_to_ast
markdown_to_html
xml_decode
xml_encode
yaml_decode
yaml_encode
+3
View File
@@ -1,6 +1,9 @@
Output / Invocation Functions
1_RENDER
1_CLI
cli_input
cli_arg
unit_call
component
component_exists
+6
View File
@@ -16,3 +16,9 @@ cwd_set
shell_escape
shell_exec
file_unlink
zip_create
zip_list
zip_read
zip_extract
gz_compress
gz_uncompress
+67
View File
@@ -0,0 +1,67 @@
:title
CLI
:sig
CLI(Request& context)
:see
>1_RENDER
>1_COMPONENT
>1_WS
>1_INIT
>1_ONCE
>unit_call
>cli_input
>cli_arg
:content
Defines a local command-line entrypoint for a UCE unit.
`CLI(Request& context)` is invoked only through the local UCE CLI Unix socket, not through ordinary public HTTP requests. This lets web apps keep test runners, migrations, maintenance tasks, and admin tooling beside the rest of their UCE units while still separating those commands from browser-facing `RENDER()` routes.
The default CLI socket path is `/run/uce/cli.sock` and is configured with `CLI_SOCKET_PATH`.
Example convenience script usage:
```sh
scripts/uce-cli /tests/cli.uce
scripts/uce-cli /tests/cli.uce action=echo message=hello
scripts/uce-cli --json '{"action":"echo","message":"hello"}' /tests/cli.uce
```
Equivalent curl probe:
```sh
curl --unix-socket /run/uce/cli.sock http://localhost/tests/cli.uce
```
For structured commands, prefer JSON POST bodies. The `scripts/uce-cli` helper sends `key=value` parameters as JSON POST by default, while still allowing `--get` for simple query-string probes.
Inside the handler, the usual `Request& context` fields are available:
- `context.get` for command query parameters
- `context.post` and `context.in` for POST bodies
- `context.params["UCE_CLI"] == "1"` for CLI socket dispatch
- `context.params["SCRIPT_FILENAME"]` for the invoked unit file
CLI responses default to `text/plain; charset=utf-8`, but the handler may set headers and status explicitly.
Use `cli_input(context)` to merge query parameters, form parameters, and JSON POST fields into one `DTree`.
```cpp
CLI(Request& context)
{
DTree input = cli_input(context);
String action = first(input["action"].to_string(), "ping");
if(action == "ping")
{
print("ok\n");
return;
}
context.set_status(400, "Bad Command");
print("unknown action: ", action, "\n");
}
```
`ONCE(Request& context)` runs before `CLI()` in the same way it runs before render and component entrypoints. `INIT(Request& context)` runs when the unit is loaded into a worker.
+2 -2
View File
@@ -25,7 +25,7 @@ The template rewriting implementation lives in `src/lib/compiler-parser.cpp`, wi
- Inside a literal block, `<?= expression ?>` emits `print(html_escape(expression));`.
- Inside a literal block, `<?: expression ?>` emits `print(expression);` without HTML escaping.
- `#load "other.uce"` injects another UCE unit at compile time.
- `RENDER(Request& context)`, `COMPONENT(Request& context)`, `ONCE(Request& context)`, `INIT(Request& context)`, and `WS(Request& context)` are normal C++ macros from `src/lib/compiler.h`.
- `RENDER(Request& context)`, `COMPONENT(Request& context)`, `CLI(Request& context)`, `ONCE(Request& context)`, `INIT(Request& context)`, and `WS(Request& context)` are normal C++ macros from `src/lib/compiler.h`.
- `COMPONENT:NAME(Request& context)` is rewritten by the custom pass into an exported named component handler.
- `EXPORT` is also a normal C++ macro, but the custom pass additionally records exported declarations for metadata.
@@ -47,7 +47,7 @@ The template rewriting implementation lives in `src/lib/compiler-parser.cpp`, wi
- The final generated source is written to `BIN_DIRECTORY + src_path + "/" + source_file + ".cpp"`.
- `scripts/compile` then compiles that generated `.cpp` into `source_file + ".so"` with `clang++ -shared -std=c++20 ...`.
- When a worker loads the compiled unit into memory, the runtime checks for `INIT(Request& context)` and calls it once for that worker-side load.
- On each request, the first time a given unit is entered through `RENDER()` or any `COMPONENT...` handler, the runtime checks for `ONCE(Request& context)` and calls it before the render/component handler.
- On each request, the first time a given unit is entered through `RENDER()`, `CLI()`, or any `COMPONENT...` handler, the runtime checks for `ONCE(Request& context)` and calls it before the selected handler.
## Generated Files
+24
View File
@@ -0,0 +1,24 @@
:title
cli_arg
:sig
String cli_arg(Request& context, String key, String default_value = "")
:see
>1_CLI
>cli_input
:content
Reads one value from the merged `cli_input(context)` parameter tree.
If the key is missing, or the resolved value is empty, `default_value` is returned.
```cpp
CLI(Request& context)
{
String action = cli_arg(context, "action", "help");
print("action=", action, "\n");
}
```
For commands that need multiple values or typed reads, prefer calling `cli_input(context)` once and reading the returned `DTree` directly.
+41
View File
@@ -0,0 +1,41 @@
:title
cli_input
:sig
DTree cli_input(Request& context)
:see
>1_CLI
>json_decode
>DTree
:content
Returns a structured parameter tree for a `CLI(Request& context)` invocation.
`cli_input()` merges simple command inputs into one `DTree`:
1. query parameters from `context.get`
2. form parameters from `context.post`
3. JSON object fields from an `application/json` or `*+json` request body
Later sources override earlier ones, so a JSON body can override a query or form key with the same name.
If the JSON body is a scalar or array instead of an object, the decoded value is stored under `input["_"]`.
```cpp
CLI(Request& context)
{
DTree input = cli_input(context);
String action = first(input["action"].to_string(), "help");
if(action == "echo")
print(input["message"].to_string(), "\n");
}
```
Convenience script usage:
```sh
scripts/uce-cli /tests/cli.uce action=echo message=hello
scripts/uce-cli --json '{"action":"echo","message":"hello"}' /tests/cli.uce
```
+24
View File
@@ -0,0 +1,24 @@
:sig
String gz_compress(String src)
:params
src : uncompressed input bytes
return value : gzip-format compressed bytes
:see
>sys
gz_uncompress
zip_create
zip_read
:content
Compresses `src` and returns a gzip-format byte string.
The result is binary data. Store it in a file, send it with an appropriate content type/encoding, or pass it directly to `gz_uncompress()`.
```uce
String compressed = gz_compress("hello\n");
file_put_contents("/tmp/hello.txt.gz", compressed);
```
UCE writes a standard gzip wrapper around a deflate stream, including CRC32 and uncompressed-size footer fields.
+22
View File
@@ -0,0 +1,22 @@
:sig
String gz_uncompress(String compressed)
:params
compressed : gzip-format compressed bytes
return value : uncompressed bytes
:see
>sys
gz_compress
zip_list
zip_extract
:content
Uncompresses a gzip-format byte string and returns the original content.
```uce
String compressed = file_get_contents("/tmp/hello.txt.gz");
String plain = gz_uncompress(compressed);
```
`gz_uncompress()` validates the gzip header, CRC32 footer, and uncompressed-size footer. It throws a runtime error when the input is not a supported gzip stream or fails validation.
+11 -5
View File
@@ -1,20 +1,26 @@
:sig
pid_t task(String key, std::function<void()> exec_func)
pid_t task(String key, std::function<void()> exec_func, u64 timeout = 60*10)
:params
key : string uniquely identifying the task
key : string uniquely identifying the task across the whole runtime instance
exec_func : function to execute
return value : the process ID of the started (or still running) task
timeout : maximum run time in seconds; `0` disables the timeout
return value : the process ID of the started (or still running) task, or `0` when the task could not be started
:see
>task
task_pid
task_kill
task_repeat
:content
Starts `exec_func` in a new process and returns that process ID.
If a process with the same `key` is already running, `task()` does not start a second copy. Instead it returns the PID of the already-running task.
If a process with the same `key` is already running anywhere in the runtime instance, `task()` does not start a second copy. Instead it returns the PID of the already-running task. Coordination is through a shared task status file under `BIN_DIRECTORY`, so the key applies across workers, not just the current worker process.
Use the `key` to make a background job idempotent across repeated calls.
Task keys may contain ordinary user-facing text. UCE hashes the key before using it as an internal lock/status filename so slashes and other path-like characters cannot escape the task state directory.
`timeout` is enforced in the child process with an alarm. The default is ten minutes. Pass `0` only for tasks that are intentionally unbounded and have their own shutdown path.
Related:
+3 -1
View File
@@ -13,7 +13,9 @@ task_pid
task_repeat
:content
Wraps the standard POSIX `kill()` function.
Wraps the standard POSIX `kill()` function for positive process IDs.
`task_kill()` rejects `pid <= 0` and returns `-1`, so callers cannot accidentally use POSIX process-group or broadcast semantics through this helper.
`sig` may be any supported POSIX signal, including values such as `SIGTERM`, `SIGKILL`, `SIGINT`, `SIGUSR1`, `SIGUSR2`, `SIGCHLD`, `SIGCONT`, and related process-control signals.
+4 -2
View File
@@ -9,9 +9,11 @@ return value : the process ID of the task
>task
:content
Checks whether a process with the given `key` is running and returns its PID if it is.
Checks whether a process with the given `key` is running anywhere in the runtime instance and returns its PID if it is.
Returns `0` when no matching task is active.
Returns `0` when no matching task is active or task state cannot be read safely. Stale task status files are removed when the recorded PID is no longer alive.
New task status records include the Linux process start tick from `/proc/<pid>/stat`, so `task_pid()` can reject a stale status file if the PID has exited and the numeric PID has since been reused by another process.
Related:
+10 -5
View File
@@ -2,21 +2,26 @@
pid_t task_repeat(String key, f64 interval, std::function<void()> exec_func, u64 timeout = 60*10)
:params
key : string uniquely identifying the task
key : string uniquely identifying the task across the whole runtime instance
interval : repeat interval in seconds
exec_func : function to execute repeatedly
timeout : optional task timeout value
return value : the process ID of the started (or still running) task
timeout : maximum run time in seconds; `0` disables the timeout
return value : the process ID of the started (or still running) task, or `0` when the task could not be started
:see
>task
task
task_pid
task_kill
:content
Starts a repeating background worker process.
`exec_func` runs in a loop, and the worker sleeps for `interval` seconds between executions.
`exec_func` runs in a loop, and the worker sleeps for `interval` seconds between executions. `interval` must be greater than zero.
If a process with the same `key` is already running, `task_repeat()` does not start a second worker and instead returns the PID of the existing one.
If a process with the same `key` is already running anywhere in the runtime instance, `task_repeat()` does not start a second worker and instead returns the PID of the existing one. Coordination is through the same shared task state used by `task()`.
`timeout` bounds the lifetime of the repeating worker. The default is ten minutes. Pass `0` only for intentionally unbounded workers with another shutdown path.
Related:
+63
View File
@@ -0,0 +1,63 @@
:sig
DTree xml_decode(String s)
:params
s : XML source string
return value : element-shaped DTree
:see
>markup
xml_encode
json_decode
0_DTree
String
:content
Parses a simple XML document into a structured `DTree`.
`xml_decode()` is intentionally small. It does not validate schemas, DTDs, namespaces, or document types. It parses the first root element and returns the same structural element shape accepted by `xml_encode()`.
Try the live example in the [XML demo](../demo/xml.uce).
Return shape:
```text
node["name"] = element name
node["attrs"] = map of attributes
node["text"] = text content when non-empty
node["children"] = list of child element nodes
```
Example:
```uce
DTree book = xml_decode("<book id=\"b1\"><title>UCE &amp; XML</title></book>");
book["name"].to_string(); // book
book["attrs"]["id"].to_string(); // b1
book["children"]["0"]["name"].to_string(); // title
book["children"]["0"]["text"].to_string(); // UCE & XML
```
CDATA and numeric entities are folded into text:
```uce
DTree note = xml_decode("<note><![CDATA[5 < 6]]><symbol>&#x41;&#66;</symbol></note>");
note["text"].to_string(); // 5 < 6
note["children"]["0"]["text"].to_string(); // AB
```
Supported parser features:
- elements
- attributes with quoted values
- self-closing tags
- text nodes
- XML entities such as `&amp;`, `&lt;`, `&gt;`, `&quot;`, and `&apos;`
- decimal and hexadecimal numeric entities
- comments, processing instructions, and CDATA sections
Whitespace-only text between child elements is ignored. Mixed non-empty text is concatenated into `node["text"]`.
Malformed XML raises a request-visible runtime error.
+76
View File
@@ -0,0 +1,76 @@
:sig
String xml_encode(DTree t)
String xml_encode(DTree t, String root_name)
:params
t : tree to serialize as XML
root_name : optional element name to use when `t` is not already an element-shaped tree, defaults to `root`
return value : XML string
:see
>markup
xml_decode
json_encode
0_DTree
html_escape
:content
Serializes a `DTree` into a simple XML string.
`xml_encode()` does not validate against a schema, DTD, or namespace rules. It is a structural converter for application data, similar in spirit to `json_encode()`.
Try the live example in the [XML demo](../demo/xml.uce).
The native element shape is:
```text
node["name"] = "book"
node["attrs"]["id"] = "b1"
node["text"] = "optional text"
node["children"] = list of child element nodes
```
Example:
```uce
DTree book;
book["name"] = "book";
book["attrs"]["id"] = "b1";
DTree title;
title["name"] = "title";
title["text"] = "UCE & XML";
book["children"].push(title);
String xml = xml_encode(book);
```
The result is:
```xml
<book id="b1"><title>UCE &amp; XML</title></book>
```
For simple map/list/scalar trees, `xml_encode()` creates ordinary child elements:
```uce
DTree payload;
payload["title"] = "Hello";
payload["count"] = "3";
String xml = xml_encode(payload, "payload");
```
Result:
```xml
<payload><count>3</count><title>Hello</title></payload>
```
Notes:
- Element and attribute names are normalized to XML-name-safe strings when needed.
- Text and attribute values are escaped.
- List values use repeated `<item>` children.
- Empty elements serialize as self-closing tags.
- Map child order follows `DTree` map iteration order.
+57
View File
@@ -0,0 +1,57 @@
:sig
DTree yaml_decode(String s)
:params
s : YAML source string
return value : decoded DTree
:see
>markup
yaml_encode
json_decode
xml_decode
0_DTree
:content
Parses a practical YAML subset into a `DTree`.
`yaml_decode()` is designed for concise UCE config files. It intentionally avoids full YAML schema behavior and does not support anchors, aliases, tags, directives, or complex inline collection syntax.
Try the live example in the [YAML demo](../demo/yaml.uce).
Example:
```uce
String source = "app:\n"
" name: UCE Starter\n"
" debug: true\n"
" port: 8080\n"
" paths:\n"
" - site\n"
" - cache\n";
DTree cfg = yaml_decode(source);
cfg["app"]["name"].to_string(); // UCE Starter
cfg["app"]["debug"].to_bool(); // true
cfg["app"]["port"].to_s64(); // 8080
cfg["app"]["paths"]["1"].to_string(); // cache
```
Supported syntax:
- indentation-based maps
- indentation-based lists
- `key: value` map entries
- list entries with `- value`
- quoted strings with single or double quotes
- booleans `true` and `false`
- empty/null-ish values as empty strings
- comments beginning with `#` outside quoted strings
- literal block strings with `|`
- folded block strings with `>`
- optional `---` and `...` document markers
Numeric-looking values are stored as strings, matching `json_decode()`'s current behavior. Use `to_s64()`, `to_u64()`, or `to_f64()` when reading numeric config values.
Malformed input raises a request-visible `yaml_decode(): ...` runtime error.
+57
View File
@@ -0,0 +1,57 @@
:sig
String yaml_encode(DTree t)
:params
t : tree to serialize as YAML
return value : YAML string
:see
>markup
yaml_decode
json_encode
xml_encode
0_DTree
:content
Serializes a `DTree` into a compact YAML string for configuration files.
`yaml_encode()` focuses on the ordinary data shapes UCE apps use for config: nested maps, lists, strings, booleans, and numeric `f64` values. It does not emit YAML tags, anchors, aliases, or custom schema features.
Try the live example in the [YAML demo](../demo/yaml.uce).
Example:
```uce
DTree cfg;
cfg["app"]["name"] = "UCE Starter";
cfg["app"]["debug"].set_bool(true);
cfg["app"]["port"] = (f64)8080;
DTree path;
path = "site";
cfg["app"]["paths"].push(path);
path = "cache";
cfg["app"]["paths"].push(path);
String yaml = yaml_encode(cfg);
```
Typical result:
```yaml
app:
debug: true
name: UCE Starter
paths:
- site
- cache
port: 8080
```
Notes:
- Map keys are emitted in `DTree` map iteration order.
- Strings are left plain when safe and quoted when needed.
- Multiline strings are emitted as literal block scalars with `|`.
- Empty strings are emitted as `""` so they are not confused with YAML null.
- Decoded numeric-looking config values are strings unless the original `DTree` value was explicitly numeric.
+39
View File
@@ -0,0 +1,39 @@
:sig
bool zip_create(String zip_file_name, DTree entries)
:params
zip_file_name : path to the archive to create
entries : map or list of archive entries
return value : `true` when the archive is written
:see
>sys
zip_list
zip_read
zip_extract
DTree
:content
Creates a ZIP archive at `zip_file_name`.
`entries` can be a simple map where each key is the archive member name and each value is the file content:
```uce
DTree entries;
entries["hello.txt"] = "Hello ZIP";
entries["nested/readme.txt"] = "Nested file";
zip_create("/tmp/example.zip", entries);
```
For list-shaped input or when the map key should not be the member name, each child can provide explicit fields:
```uce
DTree item;
item["name"] = "data/value.txt";
item["content"] = "42";
entries["ignored-key"] = item;
```
An entry may also provide `file` instead of `content`; UCE reads that source file and stores its contents under `name`.
Entry names are normalized to forward slashes and rejected when they are absolute paths, drive-qualified paths, empty names, or contain `..` path segments.
+24
View File
@@ -0,0 +1,24 @@
:sig
bool zip_extract(String zip_file_name, String destination_directory)
:params
zip_file_name : path to the ZIP archive
destination_directory : directory that should receive extracted files
return value : `true` when extraction completes
:see
>sys
zip_create
zip_list
zip_read
:content
Extracts every member in a ZIP archive into `destination_directory`.
```uce
zip_extract("/tmp/example.zip", "/tmp/example-extract");
```
UCE creates the destination directory and any needed child directories when they do not already exist.
For safety, every member name is normalized and checked before extraction. Absolute paths, drive-qualified paths, empty names, and `..` path segments are rejected so archives cannot write outside the destination directory.
+38
View File
@@ -0,0 +1,38 @@
:sig
DTree zip_list(String zip_file_name)
:params
zip_file_name : path to the ZIP archive to inspect
return value : DTree containing archive metadata and an `entries` array
:see
>sys
zip_create
zip_read
zip_extract
DTree
:content
Reads the central directory from `zip_file_name` and returns structured metadata.
The returned tree contains:
- `file` : archive path
- `count` : number of entries
- `entries` : list of entry metadata
Each entry contains:
- `name`
- `index`
- `size`
- `compressed_size`
- `is_directory`
- `method`
Example:
```uce
DTree info = zip_list("/tmp/example.zip");
print(json_encode(info));
```
+24
View File
@@ -0,0 +1,24 @@
:sig
String zip_read(String zip_file_name, String entry_name)
:params
zip_file_name : path to the ZIP archive
entry_name : member name to read from the archive
return value : uncompressed entry contents
:see
>sys
zip_create
zip_list
zip_extract
:content
Reads one file member from a ZIP archive and returns its uncompressed bytes as a `String`.
```uce
String body = zip_read("/tmp/example.zip", "hello.txt");
```
`entry_name` is normalized to forward slashes and rejected when it is empty, absolute, drive-qualified, or contains a `..` path segment.
Use `zip_list()` when you need to discover entry names before reading them.
+1 -4
View File
@@ -1,7 +1,4 @@
#load "../../lib/app.uce"
COMPONENT(Request& context)
{
starter_boot(context);
print(starter_page_main_html(context));
print(component("page_helpers.uce:MAIN_HTML", context.props, context));
}
+3 -4
View File
@@ -1,12 +1,11 @@
#load "../../lib/app.uce"
COMPONENT(Request& context)
{
starter_boot(context);
context.header["Content-Type"] = "application/json";
context.header["Cache-Control"] = "no-cache, no-store, must-revalidate";
if(context.props["json"].get_type_name() == "array")
print(json_encode(context.props["json"]));
else if(context.call["info"]["json"].get_type_name() == "array")
print(json_encode(context.call["info"]["json"]));
else
print(starter_page_main_html(context));
print(component("page_helpers.uce:MAIN_HTML", context.props, context));
}
+9
View File
@@ -0,0 +1,9 @@
COMPONENT:MAIN_HTML(Request& context)
{
String main_html = context.props["main_html"].to_string();
if(main_html == "")
main_html = context.props["main"].to_string();
if(main_html == "")
main_html = context.call["info"]["fragments"]["main"].to_string();
print(main_html);
}
+26
View File
@@ -0,0 +1,26 @@
CLI(Request& context)
{
context.header["Content-Type"] = "text/plain; charset=utf-8";
DTree input = cli_input(context);
String action = first(input["action"].to_string(), "ping");
if(action == "ping")
{
print("uce-unit-cli: ok\n");
print("script=", context.params["SCRIPT_FILENAME"], "\n");
return;
}
if(action == "echo")
{
print(input["message"].to_string(), "\n");
return;
}
context.set_status(400, "CLI Error");
print("unknown cli action: ", action, "\n");
}
RENDER(Request& context)
{
context.set_status(404, "Not Found");
context.header["Content-Type"] = "text/plain; charset=utf-8";
print("This unit is intended for local CLI socket invocation.\n");
}
+25
View File
@@ -55,6 +55,31 @@ RENDER(Request& context)
DTree decoded = json_decode(payload_json);
check("json_encode() / json_decode()", decoded["name"].to_string() == "uce" && int_val(decoded["count"].to_string()) == 3, payload_json);
DTree xml_doc;
xml_doc["name"] = "book";
xml_doc["attrs"]["id"] = "b1";
DTree xml_title;
xml_title["name"] = "title";
xml_title["text"] = "UCE & XML";
xml_doc["children"].push(xml_title);
String encoded_xml = xml_encode(xml_doc);
DTree decoded_xml = xml_decode(encoded_xml);
check("xml_encode() / xml_decode()", contains(encoded_xml, "id=\"b1\"") && contains(encoded_xml, "UCE &amp; XML") && decoded_xml["children"]["0"]["text"].to_string() == "UCE & XML", encoded_xml);
DTree xml_payload;
xml_payload["title"] = "Hello";
xml_payload["count"] = "3";
check("xml_encode() simple map", xml_encode(xml_payload, "payload") == "<payload><count>3</count><title>Hello</title></payload>", xml_encode(xml_payload, "payload"));
check("xml_decode() numeric entities", xml_decode("<x>&#x41;&#66;</x>")["text"].to_string() == "AB", xml_decode("<x>&#x41;&#66;</x>")["text"].to_string());
String yaml_config = "app:\n name: UCE Starter\n debug: true\n port: 8080\n paths:\n - site\n - cache\nmessage: |\n hello\n world\n";
DTree decoded_yaml = yaml_decode(yaml_config);
check("yaml_decode() config", decoded_yaml["app"]["name"].to_string() == "UCE Starter" && decoded_yaml["app"]["debug"].to_bool() && decoded_yaml["app"]["port"].to_s64() == 8080 && decoded_yaml["app"]["paths"]["1"].to_string() == "cache" && decoded_yaml["message"].to_string() == "hello\nworld", json_encode(decoded_yaml));
String encoded_yaml = yaml_encode(decoded_yaml);
DTree yaml_roundtrip = yaml_decode(encoded_yaml);
check("yaml_encode() / yaml_decode()", contains(encoded_yaml, "app:") && contains(encoded_yaml, "paths:") && yaml_roundtrip["app"]["debug"].to_bool() && yaml_roundtrip["message"].to_string() == "hello\nworld", encoded_yaml);
DTree tree;
tree["suite"] = "core";
tree["nested"]["api"] = "dtree";
+1
View File
@@ -12,6 +12,7 @@ RENDER(Request& context)
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");
?></div><?
+19 -1
View File
@@ -31,7 +31,7 @@ RENDER(Request& context)
if(mode != "stop")
{
short_pid = task("site-tests-short", []() {
sleep(2);
sleep(5);
});
repeat_pid = task_repeat("site-tests-repeat", 1.0, []() {
@@ -39,8 +39,23 @@ RENDER(Request& context)
}, 4);
}
pid_t timeout_pid = 0;
pid_t unsafe_key_pid = 0;
if(mode != "stop")
{
timeout_pid = task("site-tests-timeout", []() {
sleep(5);
}, 1);
unsafe_key_pid = task("site-tests/../unsafe key", []() {
sleep(5);
});
sleep(2);
}
pid_t seen_short_pid = task_pid("site-tests-short");
pid_t seen_repeat_pid = task_pid("site-tests-repeat");
pid_t seen_timeout_pid = task_pid("site-tests-timeout");
pid_t seen_unsafe_key_pid = task_pid("site-tests/../unsafe key");
int short_alive = seen_short_pid == 0 ? -1 : task_kill(seen_short_pid, 0);
int repeat_alive = seen_repeat_pid == 0 ? -1 : task_kill(seen_repeat_pid, 0);
@@ -57,6 +72,9 @@ RENDER(Request& context)
check("task_pid() + task_kill(pid, 0)", short_alive == 0, "kill(0) result=" + std::to_string(short_alive));
check("task_repeat()", repeat_pid != 0 && seen_repeat_pid != 0, "started=" + std::to_string(repeat_pid) + " seen=" + std::to_string(seen_repeat_pid));
check("repeat worker liveness", repeat_alive == 0, "kill(0) result=" + std::to_string(repeat_alive));
check("task() timeout", timeout_pid != 0 && seen_timeout_pid == 0, "started=" + std::to_string(timeout_pid) + " seen_after_timeout=" + std::to_string(seen_timeout_pid));
check("task key normalization", unsafe_key_pid != 0 && seen_unsafe_key_pid != 0, "started=" + std::to_string(unsafe_key_pid) + " seen=" + std::to_string(seen_unsafe_key_pid));
check("task_kill() rejects negative pid", task_kill(-1, 0) == -1, "kill(-1, 0) rejected");
}
?><div class="tests-section">
+84
View File
@@ -0,0 +1,84 @@
#include "testlib.h"
RENDER(Request& context)
{
if(!test_demo_request_allowed(context))
{
site_tests_restricted(context, "ZIP", "create and extract temporary server-side archive files");
return;
}
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("ZIP", "Local-only coverage for zip_create(), zip_list(), zip_read(), and zip_extract().");
String base = "/tmp/uce-site-tests-zip";
String archive = path_join(base, "sample.zip");
String extract_dir = path_join(base, "extract");
mkdir(base);
mkdir(extract_dir);
DTree entries;
entries["hello.txt"] = "Hello ZIP";
entries["nested/readme.txt"] = "Nested file";
DTree explicit_entry;
explicit_entry["name"] = "data/value.txt";
explicit_entry["content"] = "42";
entries["ignored-map-key"] = explicit_entry;
bool created = zip_create(archive, entries);
DTree listed = zip_list(archive);
String hello = zip_read(archive, "hello.txt");
bool extracted = zip_extract(archive, extract_dir);
String nested = file_get_contents(path_join(extract_dir, "nested/readme.txt"));
String value = file_get_contents(path_join(extract_dir, "data/value.txt"));
check("zip_create()", created && file_exists(archive), archive);
check("zip_list()", listed["count"].to_u64() == 3 && listed["entries"]["0"]["name"].to_string() != "", json_encode(listed));
check("zip_read()", hello == "Hello ZIP", hello);
check("zip_extract()", extracted && nested == "Nested file" && value == "42", nested + " / " + value);
bool unsafe_rejected = false;
try
{
DTree unsafe_entries;
unsafe_entries["/absolute.txt"] = "bad";
zip_create(path_join(base, "unsafe.zip"), unsafe_entries);
}
catch(std::exception& e)
{
unsafe_rejected = contains(e.what(), "unsafe");
}
check("zip_create() rejects unsafe names", unsafe_rejected, "absolute member name rejected");
String gz_source = "UCE gzip payload\nline two\n";
String gz_body = gz_compress(gz_source);
String gz_roundtrip = gz_uncompress(gz_body);
check("gz_compress()", gz_body.size() > gz_source.size() && (u8)gz_body[0] == 0x1f && (u8)gz_body[1] == 0x8b, "bytes=" + std::to_string((u64)gz_body.size()));
check("gz_uncompress()", gz_roundtrip == gz_source, gz_roundtrip);
bool bad_gz_rejected = false;
try
{
gz_uncompress("not gzip data");
}
catch(std::exception& e)
{
bad_gz_rejected = contains(e.what(), "gz_uncompress");
}
check("gz_uncompress() rejects invalid data", bad_gz_rejected, "invalid stream rejected");
site_tests_summary(passed, failed, skipped, "ZIP tests write only under /tmp/uce-site-tests-zip.");
site_tests_page_end();
}