Add archive helpers and harden task runtime
This commit is contained in:
@@ -37,6 +37,8 @@ RENDER(Request& context)
|
||||
<div class="grid-heading">Data Types & 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 & 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
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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>AB</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>
|
||||
</>
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
}
|
||||
@@ -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><?
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
Markup Functions
|
||||
markdown_to_ast
|
||||
markdown_to_html
|
||||
xml_decode
|
||||
xml_encode
|
||||
yaml_decode
|
||||
yaml_encode
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
Output / Invocation Functions
|
||||
|
||||
1_RENDER
|
||||
1_CLI
|
||||
cli_input
|
||||
cli_arg
|
||||
unit_call
|
||||
component
|
||||
component_exists
|
||||
|
||||
@@ -16,3 +16,9 @@ cwd_set
|
||||
shell_escape
|
||||
shell_exec
|
||||
file_unlink
|
||||
zip_create
|
||||
zip_list
|
||||
zip_read
|
||||
zip_extract
|
||||
gz_compress
|
||||
gz_uncompress
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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
@@ -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:
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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 & 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>AB</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 `&`, `<`, `>`, `"`, and `'`
|
||||
- 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.
|
||||
@@ -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 & 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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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));
|
||||
```
|
||||
@@ -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,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));
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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 & 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>AB</x>")["text"].to_string() == "AB", xml_decode("<x>AB</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";
|
||||
|
||||
@@ -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
@@ -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">
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user