fix: stabilize runtime follow-up regressions
This commit is contained in:
parent
7f757654b6
commit
20db669589
@ -490,3 +490,212 @@ stale. One exported source-file → artifact-path helper in `compiler.cpp`,
|
||||
used by both the compile-failure formatter and the runtime failure page.
|
||||
|
||||
**Status:** fixed, but fix not verified yet.
|
||||
|
||||
---
|
||||
|
||||
## Part 7 — Verification of the fixes (review pass, 2026-06-11)
|
||||
|
||||
All fixes above were re-reviewed against commit `7f75765` and, where possible,
|
||||
verified live against the dev server (network suite: 21/21 public tests pass).
|
||||
|
||||
> **Status update (later the same day):** 7.1–7.8 are fixed, built, deployed,
|
||||
> and verified — full suite including internal tests passes 76/76, once-init
|
||||
> renders again, dashboard/workspace ship their ONCE assets (now asserted by
|
||||
> the smoke suite), and a new `uce_demo_smoke.py` plugin covers every
|
||||
> `site/demo/*.uce` page. Unit ABI bumped to 3 (the @fragment prelude now
|
||||
> instantiates the shared `UceFragmentCapture` from `functionlib.h`). Two notes:
|
||||
>
|
||||
> - Fixing 7.3 exposed a latent bug: on a failed `mysql_real_connect`, the old
|
||||
> code left `connection` pointing at the freed handle, and `error()` read the
|
||||
> error message out of freed memory — that's the only reason the services
|
||||
> page's "skip when no MySQL" branch ever worked. `connect()` now nulls the
|
||||
> handle and signals failure via `_preload_next_error_code`, and `query()` /
|
||||
> `error()` guard against a null connection (clean error message instead of
|
||||
> the SIGSEGV this otherwise caused).
|
||||
> - The 7.8 nit about the `assets.uce` lambda taking `DTree` by value is
|
||||
> withdrawn: `DTree::to_string()` is non-const, so a `const DTree&` parameter
|
||||
> cannot call it. Making the DTree read accessors const-correct would be the
|
||||
> real fix and is a separate, larger change (worth doing before the WASM
|
||||
> DTree C ABI freezes the surface).
|
||||
|
||||
### Verified clean
|
||||
|
||||
**1.1** (double-quoted attr + `html_escape` escapes `'`, regression test),
|
||||
**1.4** (multi-statement rejection is comment/whitespace-tolerant, test),
|
||||
**1.9/5.5** (`compiler_generated_cpp_path` agrees with `setup_unit_paths`;
|
||||
confirmed live — the error page now prints the real `/tmp/uce/work/...` path),
|
||||
**1.10** (`each`/`push` take `const DTree&`, `is_list()` hoisted),
|
||||
**1.11** (both the initial scan and the loop body are wrapped),
|
||||
**2.1, 2.2, 2.3, 2.5/3.4, 3.1, 3.2, 3.5, 4.1, 4.2, 4.4, 4.5** (SQLITE_STATIC
|
||||
is safe: the params map outlives the statement), **5.3, 5.4**.
|
||||
|
||||
### 7.1 REGRESSION (live): ONCE output captured into a slot nobody prints
|
||||
|
||||
**Files:** `src/lib/compiler-parser.cpp` (default slot `once`),
|
||||
`components/theme/head.uce:23`
|
||||
|
||||
`ONCE` now defaults to `@fragment once`, but no theme prints
|
||||
`context.call["fragments"]["once"]`. Verified live: the dashboard page serves
|
||||
without `views/dashboard.css`, the widgets page without any ag-grid assets —
|
||||
the ONCE blocks in `views/dashboard.uce`, `components/data/widgets.uce`, and
|
||||
`components/workspace/primitives.uce` are silently swallowed. The suite passed
|
||||
anyway because it only checks body markers, not asset tags.
|
||||
|
||||
**Fix:** print `fragments["once"]` in `head.uce` (next to `fragments["head"]`),
|
||||
or convert those three ONCE blocks to `@fragment head`. Also worth adding an
|
||||
asset-tag assertion to the dashboard smoke test so this can't regress silently.
|
||||
|
||||
### 7.2 REGRESSION (live): fragment rewriter fires on literal text and duplicates lines
|
||||
|
||||
**File:** `src/lib/compiler-parser.cpp` (`compiler_rewrite_fragment_attributes`)
|
||||
|
||||
The rewriter is a line-based pre-pass with no literal awareness, so any literal
|
||||
HTML line starting with `ONCE(` / `RENDER(` / `COMPONENT(` is treated as an
|
||||
entry point. Verified live: `/demo/once-init.uce` is currently a compile error
|
||||
("extraneous closing brace") because the heading text `ONCE() and INIT()`
|
||||
matches. Two compounding defects:
|
||||
|
||||
- When the entry line has no `{`, the code scans forward across arbitrary
|
||||
lines to inject the prelude into whatever `{` it finds next.
|
||||
- If no `{` is ever found, the inner loop emits the remaining lines but never
|
||||
advances `i`, so the outer loop emits them all a second time.
|
||||
|
||||
**Fix (minimal):** only treat a match as an entry point when the `{` is on the
|
||||
entry line or the immediately following non-`@`-attribute line, and advance
|
||||
`i = j` when the forward scan exhausts. **Fix (right altitude):** literal
|
||||
tracking lives in the char-wise pass — fold fragment rewriting into it
|
||||
eventually; `compiler_rewrite_named_render_syntax` shares the same blind spot.
|
||||
|
||||
### 7.3 Dangling pointers: stack-allocated connectors register `this` but never unregister
|
||||
|
||||
**Files:** `src/lib/mysql-connector.cpp:40`, `src/lib/sqlite-connector.cpp`
|
||||
(connect paths), exposed by `site/tests/services.uce:100`
|
||||
|
||||
`connect()` now registers `this` in `context->resources.*_connections`, but
|
||||
neither class has a destructor, so a stack-allocated connector (`MySQL mysql;
|
||||
mysql.connect(...)` — exactly what `services.uce` does) leaves a dangling
|
||||
pointer behind when it goes out of scope. End-of-request cleanup then calls
|
||||
`disconnect()` on dead stack memory. Currently latent only because the dev
|
||||
host has no reachable MySQL server (the test skips).
|
||||
|
||||
**Fix:** add `~MySQL() { disconnect(); }` and `~SQLite() { disconnect(); }` —
|
||||
`disconnect()` already unregisters (and forgets the worker cache entry), so
|
||||
destructors make stack usage safe and cleanup only ever sees live heap objects.
|
||||
|
||||
### 7.4 Leaks on the failed-connect path; `worker_cache` set before the cache insert
|
||||
|
||||
**File:** `src/lib/sqlite-connector.cpp:267` (`sqlite_connect`),
|
||||
`mysql-connector.h:38` (`mysql_connect`)
|
||||
|
||||
Registration happens only when `connect()` succeeds, so on failure the factory
|
||||
returns a wrapper that is in no registry: `request_cleanup_delete = true` never
|
||||
fires and the wrapper leaks per failed connect. Worse for sqlite: if
|
||||
`sqlite3_open_v2` succeeds but `apply_default_pragmas` fails, the wrapper has
|
||||
`worker_cache = true` but is *not* in the cache map — cleanup skips both the
|
||||
close and the delete, leaking an open connection per occurrence.
|
||||
|
||||
**Fix:** set `worker_cache = true` only at the point of cache insertion, and
|
||||
register the wrapper with the request on the failure path too (the caller still
|
||||
needs it alive to read `sqlite_error`).
|
||||
|
||||
### 7.5 Worker-cached sqlite connections can carry an open transaction across requests
|
||||
|
||||
**File:** `src/lib/sqlite-connector.cpp` (`cleanup_sqlite_connections`)
|
||||
|
||||
The end-of-request reset clears stats/error state but not transaction state. A
|
||||
page that runs `BEGIN` and then faults (or simply forgets `COMMIT`) leaves the
|
||||
cached connection mid-transaction; the next request on this worker inherits it,
|
||||
holding the WAL write lock indefinitely.
|
||||
|
||||
**Fix:** in the cleanup reset branch, `if(!sqlite3_get_autocommit(handle))
|
||||
sqlite3_exec(handle, "ROLLBACK", ...)`.
|
||||
|
||||
### 7.6 split_http_headers: request lines containing `:` are misclassified as headers
|
||||
|
||||
**File:** `src/lib/functionlib.cpp:745`
|
||||
|
||||
The 1.3 fix keys request-line detection on "first non-blank line contains no
|
||||
colon". A legal request line like `GET /page.uce?t=12:30 HTTP/1.1` (colon in
|
||||
the query string, absolute-form URIs, IPv6 hosts) now parses as a header and
|
||||
`REQUEST_METHOD` stays empty — this regresses the direct-HTTP server path for
|
||||
real-world URLs.
|
||||
|
||||
**Fix:** classify as header only when the colon appears before the first
|
||||
space/tab (`colon != npos && colon < first_whitespace`); otherwise it is the
|
||||
request line. Add `GET /x.uce?t=12:30 HTTP/1.1` to the core.uce checks.
|
||||
|
||||
### 7.7 Backtrace capture now mallocs inside the signal handler
|
||||
|
||||
**File:** `src/linux_fastcgi.cpp:108`
|
||||
|
||||
The 1.5 fix captures on the correct (faulting) stack, but
|
||||
`capture_backtrace_string` calls `backtrace_symbols` (malloc) and builds
|
||||
`String`s inside the SIGSEGV handler. If the fault itself is heap corruption or
|
||||
happens inside malloc, the handler deadlocks on the heap lock or double-faults
|
||||
— the worker dies with no graceful 500 at all, which is worse than a shallow
|
||||
trace. Acceptable tradeoff for a diagnostics path, but the standard shape is
|
||||
cheap:
|
||||
|
||||
**Fix:** in the handler, only `backtrace()` into a `static void* frames[32]`
|
||||
(no allocation) and stash the depth; call `backtrace_symbols` + string-building
|
||||
in `handle_complete` after the `siglongjmp`. Call `backtrace()` once at worker
|
||||
startup so libgcc's lazy init doesn't allocate on first use either.
|
||||
|
||||
### 7.8 Minor / cleanup
|
||||
|
||||
- `request_populate_context_params` (`src/lib/uri.cpp:367`) has zero callers
|
||||
now — delete it or make it a two-line wrapper over the `_from_route` variant
|
||||
(its body is a verbatim duplicate).
|
||||
- MySQL positional-`?` check runs twice on the params path (params overload +
|
||||
the re-check inside `query(String)` after substitution; substituted values
|
||||
are always quoted by `escape()`, so the inner check alone suffices).
|
||||
- Both unquoted-`?` scanners treat `?` inside SQL comments (`-- ?`, `/* ? */`)
|
||||
as positional and reject the query — worth a code comment as a known
|
||||
limitation.
|
||||
- `MySQL::error()` consumes/clears state in the new `statement_info` branch but
|
||||
not in the errno-switch branch, and `SQLite::error()` never consumes — pick
|
||||
one semantic.
|
||||
- `theme/assets.uce` lambda still takes `DTree item` by value; make it
|
||||
`const DTree&` to match the new `each()` signature.
|
||||
- `__UceFragmentCapture` is regenerated inline in every captured entry point;
|
||||
define the struct once in the runtime headers and have the prelude emit only
|
||||
the instantiation line (less generated-code surface — also smaller wasm unit
|
||||
modules later, where the capture logic belongs in the core module).
|
||||
- `uce_site_suite.py` does `from run_network_tests import TestFailure` while
|
||||
the runner executes as `__main__` — the import creates a second module
|
||||
instance, so the runner's `except TestFailure` never matches the plugin's
|
||||
class and missing-manifest cases report as "unexpected error" instead of a
|
||||
clean failure (still red, just noisier). Compare by name or pass a fail
|
||||
helper through the context.
|
||||
- Default `run_network_tests.py` executes only the public set (21 cases);
|
||||
sqlite/services/io/zip/tasks are internal-tagged and need the internal run to
|
||||
count as a gate. Worth wiring both into whatever becomes the WASM Phase 5
|
||||
parity gate, plus a demo-pages smoke plugin — both live regressions above
|
||||
(7.1, 7.2) sit exactly in the coverage gaps (asset tags, `demo/`).
|
||||
|
||||
### 7.9 NEW: lists with ten or more entries iterate in lexicographic, not numeric, order
|
||||
|
||||
**File:** `src/lib/dtree.cpp` (`DTree::each`), found while documenting the accessors (2026-06-11)
|
||||
|
||||
`DTree` stores list entries as string keys in a `std::map`, so iteration order
|
||||
is lexicographic: `"0", "1", "10", "11", "2", ...`. Every consumer of `each()`
|
||||
inherits this — `dtree_map`/`dtree_filter`/`dtree_values` re-push in that
|
||||
order and therefore *scramble* any `push()`-built list with ≥ 10 entries, and
|
||||
a `sqlite_query()` result with ≥ 10 rows renders rows 10+ before row 2 when
|
||||
iterated. `is_list()` still reports true because the key *set* is sequential.
|
||||
|
||||
**Fix options:** iterate numerically in `each()` when `is_list()` is true
|
||||
(cheapest, fixes all consumers at once); or use a numeric-aware comparator in
|
||||
the map type. Documented honestly on the `each` doc page in the meantime.
|
||||
This also needs deciding before the WASM DTree C ABI freezes iteration-order
|
||||
semantics.
|
||||
|
||||
**Status:** fixed — and the regression test exposed the deeper layer:
|
||||
`is_list()` itself validated keys against *map iteration order*, so it
|
||||
returned false for any list with ≥ 10 entries. That silently flipped `push()`
|
||||
out of list mode on the 12th element and made the json/yaml encoders
|
||||
serialize big lists as objects instead of arrays. `is_list()` now does an
|
||||
order-independent check (n unique canonical index keys with max n-1), index
|
||||
keys require canonical form (`"1"`, not `"01"`), and `each()` iterates lists
|
||||
in numeric index order, matching the encoders. Regression test with a
|
||||
12-entry list in core.uce covers `each`, `dtree_values`, and `dtree_map`.
|
||||
|
||||
@ -10,11 +10,19 @@ dtree_map
|
||||
dtree_omit
|
||||
dtree_pick
|
||||
dtree_values
|
||||
each
|
||||
get_by_path
|
||||
has
|
||||
is_array
|
||||
is_list
|
||||
set_status
|
||||
String
|
||||
StringList
|
||||
StringMap
|
||||
to_bool
|
||||
to_f64
|
||||
to_json
|
||||
to_s64
|
||||
to_string
|
||||
to_stringmap
|
||||
to_u64
|
||||
|
||||
@ -42,11 +42,18 @@ You will encounter `DTree` throughout the runtime, especially in:
|
||||
- `.key("key")` returns a child pointer when it already exists.
|
||||
- `.get_or_create("key")` returns a child pointer and creates it when missing.
|
||||
- `.get_by_path("a/b/c")` traverses nested children without creating missing keys.
|
||||
- `.to_string()` reads scalar content as text.
|
||||
- `.to_s64()`, `.to_u64()`, and `.to_f64()` perform best-effort numeric conversion.
|
||||
- `.to_bool()` performs best-effort boolean conversion.
|
||||
- `.to_string(default)` reads scalar content as text.
|
||||
- `.to_s64(default)`, `.to_u64(default)`, and `.to_f64(default)` perform best-effort numeric conversion.
|
||||
- `.to_bool(default)` performs best-effort boolean conversion.
|
||||
- `.to_stringmap()` converts a map-shaped tree into `StringMap`.
|
||||
|
||||
All read accessors are `const` and never modify the tree; they work directly on `const DTree&` values such as `each()` callback parameters. Every `to_*` conversion takes an optional default that is returned when the value is missing or cannot be converted — see the individual pages (`to_string`, `to_s64`, `to_u64`, `to_f64`, `to_bool`) for the exact rules:
|
||||
|
||||
```cpp
|
||||
String title = context.props["title"].to_string("Untitled");
|
||||
s64 page_size = context.cfg.get_by_path("app/page_size").to_s64(25);
|
||||
```
|
||||
|
||||
`operator[]` creates missing entries, just like `std::map`. `.has()` and `.key()` are the non-mutating lookup helpers, and `.get_by_path()` is the non-creating traversal helper.
|
||||
|
||||
`json_decode()` currently stores JSON numbers as string-valued `DTree` nodes, so typed numeric conversion is the normal way to consume those values.
|
||||
@ -60,7 +67,7 @@ References are dereferenced automatically in most normal reads.
|
||||
- `bool` values convert numerically to `1` and `0`.
|
||||
- Pointer values convert numerically when read as numbers.
|
||||
- Single-value maps can act as scalar wrappers for numeric and boolean conversion.
|
||||
- Invalid numeric input falls back to `0`.
|
||||
- Missing values and invalid numeric input fall back to the accessor's `default_value` argument (`0`, `""`, or `false` when not given).
|
||||
- `.to_stringmap()` converts map children key-by-key using each child's `to_string()`.
|
||||
|
||||
## Writing Values
|
||||
@ -93,11 +100,11 @@ Useful inspection helpers include:
|
||||
|
||||
## each()
|
||||
|
||||
`each(std::function<void (DTree t, String key)> f)` iterates over the current tree value.
|
||||
`each(std::function<void (const DTree& t, String key)> f)` iterates over the current tree value (see the `each` page for details).
|
||||
|
||||
For map-shaped `DTree` values, the callback runs once per child entry and receives:
|
||||
|
||||
- `t` as the child value
|
||||
- `t` as the child value, by const reference (no copy)
|
||||
- `key` as the child key
|
||||
|
||||
For non-map values, `each()` still invokes the callback once:
|
||||
@ -106,7 +113,7 @@ For non-map values, `each()` still invokes the callback once:
|
||||
- `key` is an empty string
|
||||
|
||||
```cpp
|
||||
context.connection["items"].each([&](DTree item, String key) {
|
||||
context.connection["items"].each([&](const DTree& item, String key) {
|
||||
print(key, ": ", item.to_string(), "\n");
|
||||
});
|
||||
```
|
||||
|
||||
36
site/doc/pages/each.txt
Normal file
36
site/doc/pages/each.txt
Normal file
@ -0,0 +1,36 @@
|
||||
:sig
|
||||
void DTree::each(function<void (const DTree& item, String key)> f) const
|
||||
|
||||
:params
|
||||
f : callback invoked per child with the child node and its key
|
||||
return value : none
|
||||
|
||||
:see
|
||||
>types
|
||||
0_DTree
|
||||
dtree_map
|
||||
dtree_filter
|
||||
dtree_keys
|
||||
is_list
|
||||
|
||||
:content
|
||||
Iterates a `DTree`. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically.
|
||||
|
||||
For map-shaped nodes the callback runs once per child, in key order, receiving the child and its key. For scalar nodes it runs exactly once with the node itself and an empty key.
|
||||
|
||||
The callback receives the child as `const DTree&` — no copy is made, and all read accessors (`to_string()`, `to_s64()`, `get_by_path()`, ...) work on it directly:
|
||||
|
||||
```cpp
|
||||
rows.each([&](const DTree& row, String key) {
|
||||
out("<li>", html_escape(row.get_by_path("title").to_string("Untitled")), "</li>");
|
||||
});
|
||||
```
|
||||
|
||||
Declaring the callback parameter as plain `DTree` also works but deep-copies every child; prefer `const DTree&`. To transform or filter into a new tree, use `dtree_map()` / `dtree_filter()` instead of mutating inside the callback.
|
||||
|
||||
List-shaped trees (see `is_list`) iterate in numeric index order, matching `json_encode()` and the other serializers. Keyed maps iterate in string key order.
|
||||
|
||||
## Related Concepts
|
||||
|
||||
- PHP: `foreach ($tree as $key => $item)`
|
||||
- JavaScript: `Object.entries(obj).forEach(([key, item]) => ...)`
|
||||
@ -1,5 +1,5 @@
|
||||
:sig
|
||||
DTree DTree::get_by_path(String path, String delim = "/")
|
||||
DTree DTree::get_by_path(String path, String delim = "/") const
|
||||
|
||||
:params
|
||||
path : slash-delimited path to traverse
|
||||
@ -11,16 +11,21 @@ return value : the resolved child node, or an empty `DTree` when the path cannot
|
||||
0_Request
|
||||
>types
|
||||
json_decode
|
||||
has
|
||||
to_string
|
||||
|
||||
:content
|
||||
Traverses a nested `DTree` without creating missing keys.
|
||||
Traverses a nested `DTree` without creating missing keys. This is a read accessor: it is `const` and never modifies the tree, unlike `operator[]`, which creates missing entries.
|
||||
|
||||
Empty path segments are ignored, so leading and trailing `/` characters are harmless. If any intermediate node is not a map or a segment is missing, `get_by_path()` returns an empty `DTree`.
|
||||
|
||||
A missing path therefore reads like an empty value — combine it with the `to_*` default arguments to express a fallback in one call:
|
||||
|
||||
## Example
|
||||
|
||||
```cpp
|
||||
context.cfg.get_by_path("theme/options/portal-dark/label").to_string()
|
||||
String label = context.cfg.get_by_path("theme/options/portal-dark/label").to_string("Portal Dark");
|
||||
s64 page_size = context.cfg.get_by_path("app/page_size").to_s64(25);
|
||||
```
|
||||
|
||||
## Related Concepts
|
||||
|
||||
36
site/doc/pages/has.txt
Normal file
36
site/doc/pages/has.txt
Normal file
@ -0,0 +1,36 @@
|
||||
:sig
|
||||
bool DTree::has(String s) const
|
||||
|
||||
:params
|
||||
s : child key to test
|
||||
return value : true when the node is map-shaped and contains the key
|
||||
|
||||
:see
|
||||
>types
|
||||
0_DTree
|
||||
get_by_path
|
||||
each
|
||||
is_array
|
||||
|
||||
:content
|
||||
Tests whether a map-shaped `DTree` contains a child key, without creating it. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically.
|
||||
|
||||
Returns `false` for scalar nodes and for missing keys.
|
||||
|
||||
This matters because `operator[]` creates missing entries, exactly like `std::map`. Use `has()` (or `get_by_path()`) when you only want to look:
|
||||
|
||||
```cpp
|
||||
if(context.props.has("avatar"))
|
||||
print(component("components/basic/avatar", context.props["avatar"], context));
|
||||
```
|
||||
|
||||
`has()` only checks one level. For nested checks, combine with `get_by_path()`:
|
||||
|
||||
```cpp
|
||||
bool configured = context.cfg.get_by_path("mail/smtp").is_array();
|
||||
```
|
||||
|
||||
## Related Concepts
|
||||
|
||||
- PHP: `array_key_exists()` / `isset()`
|
||||
- JavaScript: `Object.hasOwn(obj, key)` / `key in obj`
|
||||
34
site/doc/pages/is_array.txt
Normal file
34
site/doc/pages/is_array.txt
Normal file
@ -0,0 +1,34 @@
|
||||
:sig
|
||||
bool DTree::is_array() const
|
||||
|
||||
:params
|
||||
return value : true when the node is map-shaped
|
||||
|
||||
:see
|
||||
>types
|
||||
0_DTree
|
||||
is_list
|
||||
has
|
||||
each
|
||||
|
||||
:content
|
||||
Tests whether a `DTree` node is map-shaped, i.e. holds nested child values rather than a scalar. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically.
|
||||
|
||||
`is_array()` is true for both list-like and keyed containers; use `is_list()` to distinguish the two:
|
||||
|
||||
```cpp
|
||||
DTree rows = sqlite_query(db, "select * from notes");
|
||||
if(rows.is_array())
|
||||
{
|
||||
rows.each([&](const DTree& row, String key) {
|
||||
// ...
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
Scalar values (strings, numbers, booleans, pointers) return `false`, as do unresolvable references.
|
||||
|
||||
## Related Concepts
|
||||
|
||||
- PHP: `is_array()`
|
||||
- JavaScript: `typeof value === "object"`
|
||||
35
site/doc/pages/is_list.txt
Normal file
35
site/doc/pages/is_list.txt
Normal file
@ -0,0 +1,35 @@
|
||||
:sig
|
||||
bool DTree::is_list() const
|
||||
|
||||
:params
|
||||
return value : true when the node is a sequential, numerically indexed container
|
||||
|
||||
:see
|
||||
>types
|
||||
0_DTree
|
||||
is_array
|
||||
push
|
||||
each
|
||||
dtree_values
|
||||
|
||||
:content
|
||||
Tests whether a map-shaped `DTree` represents a list: its keys are the numeric strings `"0"`, `"1"`, `"2"`, ... in unbroken sequence. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically.
|
||||
|
||||
Containers built with `push()` are lists. An empty container counts as a list when it was created with `set_array()` or `push()`; a keyed map (or a map with gaps in its numeric keys) is `is_array()` but not `is_list()`.
|
||||
|
||||
```cpp
|
||||
DTree items;
|
||||
items.push(first_item);
|
||||
items.push(second_item);
|
||||
// items.is_list() == true
|
||||
|
||||
items["custom"] = "x";
|
||||
// items.is_list() == false, items.is_array() == true
|
||||
```
|
||||
|
||||
`dtree_map()` and `dtree_filter()` use this distinction to decide whether results re-index from zero or keep their original keys.
|
||||
|
||||
## Related Concepts
|
||||
|
||||
- PHP: `array_is_list()`
|
||||
- JavaScript: `Array.isArray()`
|
||||
@ -1,5 +1,9 @@
|
||||
:sig
|
||||
bool DTree::to_bool()
|
||||
bool DTree::to_bool(bool default_value = false) const
|
||||
|
||||
:params
|
||||
default_value : returned when the value is missing or empty
|
||||
return value : the value as a boolean, or `default_value`
|
||||
|
||||
:see
|
||||
>types
|
||||
@ -7,12 +11,27 @@ bool DTree::to_bool()
|
||||
json_decode
|
||||
to_f64
|
||||
to_u64
|
||||
to_string
|
||||
|
||||
:content
|
||||
Reads a `DTree` value as a boolean.
|
||||
Reads a `DTree` value as a boolean. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically.
|
||||
|
||||
String values such as `true`, `yes`, `on`, and `1` read as true. Values such as `false`, `no`, `off`, `0`, and `null` read as false.
|
||||
String values such as `true`, `yes`, `on`, and `1` read as true. Values such as `false`, `no`, `off`, `0`, and `null` read as false. Numeric values read as true when non-zero.
|
||||
|
||||
Numeric values read as true when non-zero. Empty strings read as false.
|
||||
`default_value` is returned when the node is unset, holds an empty string, or is an unresolvable reference. Note the truthiness rule: a non-empty string that parses as neither a boolean word nor a number still reads as `true` — only missing/empty values fall back to the default.
|
||||
|
||||
A map-shaped node with exactly one entry unwraps to that entry's value; other maps read as true when non-empty.
|
||||
|
||||
## Example
|
||||
|
||||
```cpp
|
||||
bool log_requests = context.cfg.get_by_path("app/log_requests").to_bool(true);
|
||||
bool wants_compact = context.props["compact"].to_bool();
|
||||
```
|
||||
|
||||
Use this when consuming request data, JSON-decoded values, config trees, or component props where the original input may be string-shaped.
|
||||
|
||||
## Related Concepts
|
||||
|
||||
- PHP: `filter_var($v, FILTER_VALIDATE_BOOLEAN)`
|
||||
- JavaScript: `Boolean(value)` plus string handling
|
||||
|
||||
@ -1,5 +1,9 @@
|
||||
:sig
|
||||
f64 DTree::to_f64()
|
||||
f64 DTree::to_f64(f64 default_value = 0) const
|
||||
|
||||
:params
|
||||
default_value : returned when the value is missing or cannot be parsed as a number
|
||||
return value : the value as a floating-point number, or `default_value`
|
||||
|
||||
:see
|
||||
>types
|
||||
@ -7,11 +11,32 @@ f64 DTree::to_f64()
|
||||
json_decode
|
||||
float_val
|
||||
to_bool
|
||||
to_s64
|
||||
to_u64
|
||||
|
||||
:content
|
||||
Reads a `DTree` value as a floating-point number.
|
||||
Reads a `DTree` value as a floating-point number. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically.
|
||||
|
||||
String values are parsed using the same permissive conversion rules used by the runtime's scalar helpers. Boolean values become `1.0` or `0.0`.
|
||||
String values are trimmed and parsed permissively: numeric forms and the boolean words understood by `to_bool()` both convert. Boolean values become `1.0` or `0.0`.
|
||||
|
||||
`default_value` is returned when:
|
||||
|
||||
- the node is unset or holds an empty string
|
||||
- the string does not parse as a finite number
|
||||
- the node is map-shaped with more than one entry, or an unresolvable reference
|
||||
|
||||
A map-shaped node with exactly one entry unwraps to that entry's value before converting.
|
||||
|
||||
## Example
|
||||
|
||||
```cpp
|
||||
f64 ratio = context.props["ratio"].to_f64(1.0);
|
||||
f64 threshold = context.cfg.get_by_path("alerts/threshold").to_f64(0.75);
|
||||
```
|
||||
|
||||
Use this for numeric config values, JSON-decoded fields, component props, and request data that should be treated as a number.
|
||||
|
||||
## Related Concepts
|
||||
|
||||
- PHP: `floatval()` with a fallback
|
||||
- JavaScript: `parseFloat(x) || fallback`
|
||||
|
||||
34
site/doc/pages/to_json.txt
Normal file
34
site/doc/pages/to_json.txt
Normal file
@ -0,0 +1,34 @@
|
||||
:sig
|
||||
String DTree::to_json(char quote_char = '"') const
|
||||
|
||||
:params
|
||||
quote_char : quote character used around string output
|
||||
return value : the scalar as a single JSON token
|
||||
|
||||
:see
|
||||
>types
|
||||
0_DTree
|
||||
json_encode
|
||||
json_decode
|
||||
to_string
|
||||
|
||||
:content
|
||||
Renders a single scalar `DTree` value as a JSON token. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically.
|
||||
|
||||
Strings are escaped and quoted, numbers render as numeric literals, and booleans render as `true` / `false`.
|
||||
|
||||
This is **not** a serializer for nested data: map-shaped nodes render as the placeholder string `"(array)"`. Use `json_encode()` to serialize a whole tree.
|
||||
|
||||
## Example
|
||||
|
||||
```cpp
|
||||
String token = context.props["label"].to_json(); // "\"Don't panic\""
|
||||
String body = json_encode(context.props); // full tree serialization
|
||||
```
|
||||
|
||||
The `quote_char` parameter exists for embedding output inside single-quoted contexts; for HTML attributes prefer double quotes plus `html_escape()`.
|
||||
|
||||
## Related Concepts
|
||||
|
||||
- JavaScript: `JSON.stringify(value)` for a single scalar
|
||||
- PHP: `json_encode($scalar)`
|
||||
41
site/doc/pages/to_s64.txt
Normal file
41
site/doc/pages/to_s64.txt
Normal file
@ -0,0 +1,41 @@
|
||||
:sig
|
||||
s64 DTree::to_s64(s64 default_value = 0) const
|
||||
|
||||
:params
|
||||
default_value : returned when the value is missing or cannot be parsed as a number
|
||||
return value : the value as a signed 64-bit integer, or `default_value`
|
||||
|
||||
:see
|
||||
>types
|
||||
0_DTree
|
||||
to_u64
|
||||
to_f64
|
||||
to_bool
|
||||
to_string
|
||||
|
||||
:content
|
||||
Reads a `DTree` value as a signed integer. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically.
|
||||
|
||||
String values are trimmed and parsed permissively: plain integers, floating-point forms (truncated toward zero), and the boolean words understood by `to_bool()` (`yes` reads as `1`) all convert. Results outside the `s64` range clamp to the range boundaries. Boolean values become `1` or `0`.
|
||||
|
||||
`default_value` is returned when:
|
||||
|
||||
- the node is unset or holds an empty string
|
||||
- the string does not parse as a number (`"not-a-number"`)
|
||||
- the node is map-shaped with more than one entry, or an unresolvable reference
|
||||
|
||||
A map-shaped node with exactly one entry unwraps to that entry's value before converting — this matches how single-value rows from query results read.
|
||||
|
||||
## Example
|
||||
|
||||
```cpp
|
||||
s64 page = context.get["page"].to_s64(1);
|
||||
s64 limit = context.cfg.get_by_path("app/page_size").to_s64(25);
|
||||
```
|
||||
|
||||
`json_decode()` stores JSON numbers as string-valued nodes, so this is the normal way to consume decoded numeric fields.
|
||||
|
||||
## Related Concepts
|
||||
|
||||
- PHP: `intval()` with a fallback
|
||||
- JavaScript: `parseInt(x) || fallback`
|
||||
40
site/doc/pages/to_string.txt
Normal file
40
site/doc/pages/to_string.txt
Normal file
@ -0,0 +1,40 @@
|
||||
:sig
|
||||
String DTree::to_string(String default_value = "") const
|
||||
|
||||
:params
|
||||
default_value : returned when the node holds no usable text
|
||||
return value : the scalar content as text, or `default_value`
|
||||
|
||||
:see
|
||||
>types
|
||||
0_DTree
|
||||
to_s64
|
||||
to_f64
|
||||
to_bool
|
||||
to_json
|
||||
get_by_path
|
||||
|
||||
:content
|
||||
Reads a `DTree` value as text. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically.
|
||||
|
||||
`default_value` is returned when the value is missing or not text-convertible:
|
||||
|
||||
- the node is unset or holds an empty string (a missing key read via `get_by_path()` looks exactly like this)
|
||||
- the node is map-shaped (use `json_encode()` or `to_stringmap()` for those)
|
||||
- the node is an unresolvable reference
|
||||
|
||||
Other scalar kinds convert instead of falling back: `f64` values format with six decimal places (`std::to_string`), `bool` values become `(true)` / `(false)`, and pointers format as their numeric address.
|
||||
|
||||
## Example
|
||||
|
||||
```cpp
|
||||
String title = context.props["title"].to_string("Untitled");
|
||||
String theme = context.cfg.get_by_path("theme/key").to_string("light");
|
||||
```
|
||||
|
||||
The default argument replaces the older `first(x.to_string(), "fallback")` idiom.
|
||||
|
||||
## Related Concepts
|
||||
|
||||
- PHP: `$value ?? 'fallback'` / `strval()`
|
||||
- JavaScript: `String(value || "fallback")`
|
||||
36
site/doc/pages/to_stringmap.txt
Normal file
36
site/doc/pages/to_stringmap.txt
Normal file
@ -0,0 +1,36 @@
|
||||
:sig
|
||||
StringMap DTree::to_stringmap() const
|
||||
|
||||
:params
|
||||
return value : a flat `StringMap` projection of the node
|
||||
|
||||
:see
|
||||
>types
|
||||
0_DTree
|
||||
StringMap
|
||||
to_string
|
||||
dtree_keys
|
||||
dtree_values
|
||||
|
||||
:content
|
||||
Converts a `DTree` into a flat `StringMap`. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically.
|
||||
|
||||
- Map-shaped nodes produce one entry per child, with each child read via `to_string()`. Nested maps flatten to empty strings — this is a one-level projection, not a serializer.
|
||||
- A non-empty scalar produces a single `"value"` entry holding the scalar.
|
||||
- Empty values and unresolvable references produce an empty map.
|
||||
|
||||
Use this when handing request- or config-shaped data to APIs that take `StringMap`, such as `sqlite_query()` / `mysql_query()` parameter maps or `encode_query()`.
|
||||
|
||||
## Example
|
||||
|
||||
```cpp
|
||||
StringMap params = context.props["filters"].to_stringmap();
|
||||
DTree rows = sqlite_query(db, "select * from notes where author = :author", params);
|
||||
```
|
||||
|
||||
For a faithful representation of nested data, use `json_encode()` instead.
|
||||
|
||||
## Related Concepts
|
||||
|
||||
- PHP: casting a one-level array with `array_map('strval', $a)`
|
||||
- JavaScript: `Object.fromEntries(Object.entries(o).map(([k, v]) => [k, String(v)]))`
|
||||
@ -1,5 +1,9 @@
|
||||
:sig
|
||||
u64 DTree::to_u64()
|
||||
u64 DTree::to_u64(u64 default_value = 0) const
|
||||
|
||||
:params
|
||||
default_value : returned when the value is missing or cannot be parsed as a number
|
||||
return value : the value as an unsigned 64-bit integer, or `default_value`
|
||||
|
||||
:see
|
||||
>types
|
||||
@ -7,11 +11,32 @@ u64 DTree::to_u64()
|
||||
json_decode
|
||||
int_val
|
||||
to_bool
|
||||
to_s64
|
||||
to_f64
|
||||
|
||||
:content
|
||||
Reads a `DTree` value as an unsigned integer.
|
||||
Reads a `DTree` value as an unsigned integer. This is a read accessor: it is `const`, never creates or modifies nodes, and dereferences internal references automatically.
|
||||
|
||||
String values are parsed numerically. Boolean values become `1` or `0`. Negative values clamp to `0`.
|
||||
String values are trimmed and parsed permissively, like `to_s64()`. Boolean values become `1` or `0`. Negative values clamp to `0`, and results above the `u64` range clamp to the maximum.
|
||||
|
||||
`default_value` is returned when:
|
||||
|
||||
- the node is unset or holds an empty string
|
||||
- the string does not parse as a number
|
||||
- the node is map-shaped with more than one entry, or an unresolvable reference
|
||||
|
||||
A map-shaped node with exactly one entry unwraps to that entry's value before converting. Note that a *present* negative value clamps to `0` rather than returning the default — the default signals "missing or unparseable", not "out of range".
|
||||
|
||||
## Example
|
||||
|
||||
```cpp
|
||||
u64 limit = context.get["limit"].to_u64(50);
|
||||
u64 owner_id = row.get_by_path("owner/id").to_u64();
|
||||
```
|
||||
|
||||
Use this for counts, identifiers, limits, timestamps, and other non-negative numeric values stored inside a `DTree`.
|
||||
|
||||
## Related Concepts
|
||||
|
||||
- PHP: `intval()` with a fallback
|
||||
- JavaScript: `parseInt(x) || fallback` (within unsigned range)
|
||||
|
||||
@ -26,7 +26,7 @@ void app_asset_tags(Request& context, String kind, DTree assets)
|
||||
app_asset_tag_once(context, kind, scalar);
|
||||
return;
|
||||
}
|
||||
assets.each([&](DTree item, String key) {
|
||||
assets.each([&](const DTree& item, String key) {
|
||||
app_asset_tag_once(context, kind, item.to_string());
|
||||
});
|
||||
}
|
||||
|
||||
@ -21,5 +21,6 @@ COMPONENT(Request& context)
|
||||
<script src="<?= app_asset_url("js/morphdom.js", context) ?>"></script>
|
||||
<script src="<?= app_asset_url("js/site.js", context) ?>"></script>
|
||||
<?: context.call["fragments"]["head"].to_string() ?>
|
||||
<?: context.call["fragments"]["once"].to_string() ?>
|
||||
</>
|
||||
}
|
||||
|
||||
@ -28,6 +28,8 @@ RENDER(Request& context)
|
||||
StringMap http_headers = split_http_headers("GET /demo.uce?x=1 HTTP/1.1\r\nHost: example.test\r\nX-Empty:\r\n");
|
||||
StringMap leading_crlf_headers = split_http_headers("\r\nGET /lead.uce HTTP/1.1\r\nHost: lead.example\r\n");
|
||||
StringMap header_only = split_http_headers("Host: example.test\nX-Token: abc\n");
|
||||
StringMap colon_uri_headers = split_http_headers("GET /clock.uce?t=12:30 HTTP/1.1\r\nHost: colon.example\r\n");
|
||||
check("split_http_headers() request line with colon in URI", colon_uri_headers["REQUEST_METHOD"] == "GET" && colon_uri_headers["DOCUMENT_URI"] == "/clock.uce" && colon_uri_headers["QUERY_STRING"] == "t=12:30" && colon_uri_headers["HTTP_HOST"] == "colon.example", var_dump(colon_uri_headers));
|
||||
check("trim() / split_kv() / split_http_headers()", trim(" padded value ") == "padded value" && kv["alpha"] == "one" && kv["empty"] == "" && http_headers["REQUEST_METHOD"] == "GET" && http_headers["DOCUMENT_URI"] == "/demo.uce" && http_headers["QUERY_STRING"] == "x=1" && http_headers["HTTP_X_EMPTY"] == "" && leading_crlf_headers["REQUEST_METHOD"] == "GET" && leading_crlf_headers["DOCUMENT_URI"] == "/lead.uce" && leading_crlf_headers["HTTP_HOST"] == "lead.example" && header_only["REQUEST_METHOD"] == "" && header_only["HTTP_HOST"] == "example.test" && header_only["HTTP_X_TOKEN"] == "abc", trim(" padded value ") + " / " + var_dump(kv) + " / " + var_dump(http_headers) + " / " + var_dump(leading_crlf_headers) + " / " + var_dump(header_only));
|
||||
check("replace()", replace("alpha-beta-beta", "beta", "done") == "alpha-done-done", replace("alpha-beta-beta", "beta", "done"));
|
||||
check("html_escape() attribute-safe quotes", html_escape("<&>\"Don't") == "<&>"Don't", html_escape("<&>\"Don't"));
|
||||
@ -84,6 +86,29 @@ RENDER(Request& context)
|
||||
DTree nav_dash_public = dtree_omit(nav_dash, {"section"});
|
||||
check("DTree collection helpers", empty_pop.to_string() == "" && app_nav.is_list() && nav_titles["0"].to_string() == "Dashboard" && grouped_nav["app"]["1"]["title"].to_string() == "Themes" && join(dtree_keys(nav_dash_summary), ",") == "title" && dtree_values(nav_dash_public)["0"].to_string() == "Dashboard", json_encode(grouped_nav));
|
||||
|
||||
DTree conv;
|
||||
conv["name"] = "ada";
|
||||
conv["count"] = "12";
|
||||
conv["junk"] = "not-a-number";
|
||||
conv["flag"] = "off";
|
||||
check("DTree to_* defaults", conv.get_by_path("missing/key").to_string("fallback") == "fallback" && conv["name"].to_string("fallback") == "ada" && conv["junk"].to_s64(-7) == -7 && conv["count"].to_s64(-7) == 12 && conv["junk"].to_f64(2.5) == 2.5 && conv["junk"].to_u64(9) == 9 && conv.get_by_path("nope").to_bool(true) && !conv["flag"].to_bool(true), json_encode(conv));
|
||||
|
||||
const DTree& conv_read = conv;
|
||||
String const_each_keys = "";
|
||||
conv_read.each([&](const DTree& item, String key) { const_each_keys += key + ":" + item.to_string("-") + " "; });
|
||||
check("DTree const read accessors", conv_read.has("name") && conv_read.get_by_path("name").to_string() == "ada" && conv_read.is_array() && !conv_read.is_list() && conv_read.to_stringmap()["count"] == "12" && conv_read.get_type_name() == "array" && contains(const_each_keys, "count:12"), const_each_keys);
|
||||
|
||||
DTree ordered;
|
||||
for(u32 i = 0; i < 12; i++)
|
||||
{
|
||||
DTree ordered_entry;
|
||||
ordered_entry = "v" + std::to_string(i);
|
||||
ordered.push(ordered_entry);
|
||||
}
|
||||
String ordered_keys = "";
|
||||
ordered.each([&](const DTree& item, String key) { ordered_keys += key + ","; });
|
||||
check("DTree list iteration is numeric", ordered_keys == "0,1,2,3,4,5,6,7,8,9,10,11," && dtree_values(ordered)["10"].to_string() == "v10" && dtree_map(ordered, [](const DTree& item, String key) { return(item); })["11"].to_string() == "v11", ordered_keys);
|
||||
|
||||
String binary_payload = "core";
|
||||
binary_payload.push_back((char)0x00);
|
||||
binary_payload.push_back((char)0xff);
|
||||
|
||||
@ -345,7 +345,7 @@ bool compiler_line_starts_fragmentable_entrypoint(String trimmed, String& kind)
|
||||
|
||||
String compiler_fragment_capture_prelude(String slot)
|
||||
{
|
||||
return("\nstruct __UceFragmentCapture { Request& context; String slot; __UceFragmentCapture(Request& c, String s) : context(c), slot(s) { ob_start(); } ~__UceFragmentCapture() { String html = ob_get_close(); if(html != \"\") context.call[\"fragments\"][slot] = context.call[\"fragments\"][slot].to_string() + html; } }; __UceFragmentCapture __uce_fragment_capture(context, " + compiler_cpp_string_literal(slot) + ");\n");
|
||||
return("\nUceFragmentCapture __uce_fragment_capture(context, " + compiler_cpp_string_literal(slot) + ");\n");
|
||||
}
|
||||
|
||||
String compiler_rewrite_fragment_attributes(String content)
|
||||
@ -365,7 +365,6 @@ String compiler_rewrite_fragment_attributes(String content)
|
||||
}
|
||||
|
||||
String slot = (kind == "ONCE" ? "once" : "");
|
||||
StringList pending;
|
||||
bool has_fragment_attr = false;
|
||||
u32 j = i + 1;
|
||||
while(j < lines.size())
|
||||
@ -402,28 +401,31 @@ String compiler_rewrite_fragment_attributes(String content)
|
||||
continue;
|
||||
}
|
||||
|
||||
result += line;
|
||||
if(j < lines.size())
|
||||
result += "\n";
|
||||
|
||||
while(j < lines.size())
|
||||
// The body must open on the next non-blank line after the attribute
|
||||
// lines; anything else (e.g. literal text that merely starts with
|
||||
// "ONCE(") is not an entry point and passes through untouched.
|
||||
u32 body_index = j;
|
||||
while(body_index < lines.size() && trim(lines[body_index]) == "")
|
||||
body_index++;
|
||||
bool body_opens = body_index < lines.size() && trim(lines[body_index]).rfind("{", 0) == 0;
|
||||
if(!body_opens)
|
||||
{
|
||||
String body_line = lines[j];
|
||||
auto brace_pos = body_line.find("{");
|
||||
if(brace_pos == String::npos)
|
||||
{
|
||||
result += body_line;
|
||||
if(j + 1 < lines.size())
|
||||
result += "\n";
|
||||
j++;
|
||||
continue;
|
||||
}
|
||||
result += body_line.substr(0, brace_pos + 1) + compiler_fragment_capture_prelude(slot) + body_line.substr(brace_pos + 1);
|
||||
if(j + 1 < lines.size())
|
||||
result += line;
|
||||
if(i + 1 < lines.size())
|
||||
result += "\n";
|
||||
i = j;
|
||||
break;
|
||||
continue;
|
||||
}
|
||||
|
||||
result += line;
|
||||
result += "\n";
|
||||
for(u32 k = j; k < body_index; k++)
|
||||
result += lines[k] + "\n";
|
||||
String body_line = lines[body_index];
|
||||
auto brace_pos = body_line.find("{");
|
||||
result += body_line.substr(0, brace_pos + 1) + compiler_fragment_capture_prelude(slot) + body_line.substr(brace_pos + 1);
|
||||
if(body_index + 1 < lines.size())
|
||||
result += "\n";
|
||||
i = body_index;
|
||||
}
|
||||
return(result);
|
||||
}
|
||||
|
||||
@ -18,7 +18,7 @@ const char* UCE_CLI_SYMBOL = "__uce_cli";
|
||||
const char* UCE_SERVE_HTTP_SYMBOL = "__uce_serve_http";
|
||||
const char* UCE_ONCE_SYMBOL = "__uce_once";
|
||||
const char* UCE_INIT_SYMBOL = "__uce_init";
|
||||
const u64 UCE_UNIT_ABI_VERSION = 2;
|
||||
const u64 UCE_UNIT_ABI_VERSION = 4;
|
||||
|
||||
struct SharedUnitFilesystemState
|
||||
{
|
||||
|
||||
@ -20,7 +20,7 @@ TreePtr dtree_resolve_reference(TreePtr tree)
|
||||
return(tree);
|
||||
}
|
||||
|
||||
bool dtree_key_is_index(String key, s64 expected_index = -1)
|
||||
bool dtree_key_is_index(String key)
|
||||
{
|
||||
if(key == "")
|
||||
return(false);
|
||||
@ -29,8 +29,10 @@ bool dtree_key_is_index(String key, s64 expected_index = -1)
|
||||
if(!isdigit(c))
|
||||
return(false);
|
||||
}
|
||||
if(expected_index >= 0)
|
||||
return(key == std::to_string(expected_index));
|
||||
// Only canonical index strings count ("1", not "01"), so each numeric
|
||||
// value has exactly one representation.
|
||||
if(key.length() > 1 && key[0] == '0')
|
||||
return(false);
|
||||
return(true);
|
||||
}
|
||||
|
||||
@ -140,12 +142,25 @@ u64 dtree_clamp_to_u64_range(long double value)
|
||||
|
||||
}
|
||||
|
||||
void DTree::each(std::function <void (const DTree& t, String key)> f)
|
||||
void DTree::each(std::function <void (const DTree& t, String key)> f) const
|
||||
{
|
||||
const DTree& target = deref();
|
||||
switch(target.type)
|
||||
{
|
||||
case('M'):
|
||||
// Lists iterate in numeric index order (string keys sort "10"
|
||||
// before "2"), matching the json/xml/yaml encoders.
|
||||
if(target.is_list())
|
||||
{
|
||||
for(u64 i = 0; i < target._map.size(); i++)
|
||||
{
|
||||
auto it = target._map.find(std::to_string(i));
|
||||
if(it == target._map.end())
|
||||
break;
|
||||
f(it->second, it->first);
|
||||
}
|
||||
break;
|
||||
}
|
||||
for (auto it = target._map.begin(); it != target._map.end(); ++it)
|
||||
{
|
||||
f(it->second, it->first);
|
||||
@ -157,7 +172,7 @@ void DTree::each(std::function <void (const DTree& t, String key)> f)
|
||||
}
|
||||
}
|
||||
|
||||
bool DTree::is_array()
|
||||
bool DTree::is_array() const
|
||||
{
|
||||
return(deref().type == 'M');
|
||||
}
|
||||
@ -169,44 +184,45 @@ bool DTree::is_list() const
|
||||
return(false);
|
||||
if(target._map.size() == 0)
|
||||
return(target._list_mode);
|
||||
s64 expected_index = 0;
|
||||
// The map iterates in string order ("10" before "2"), so the check must
|
||||
// be order-independent: n unique canonical index keys with maximum n-1
|
||||
// are exactly 0..n-1.
|
||||
s64 max_index = -1;
|
||||
for(const auto& entry : target._map)
|
||||
{
|
||||
if(!dtree_key_is_index(entry.first, expected_index))
|
||||
if(!dtree_key_is_index(entry.first))
|
||||
return(false);
|
||||
expected_index += 1;
|
||||
s64 index = strtoll(entry.first.c_str(), 0, 10);
|
||||
if(index > max_index)
|
||||
max_index = index;
|
||||
}
|
||||
return(true);
|
||||
return(max_index == (s64)target._map.size() - 1);
|
||||
}
|
||||
|
||||
String DTree::to_string()
|
||||
String DTree::to_string(String default_value) const
|
||||
{
|
||||
const DTree& target = deref();
|
||||
switch(target.type)
|
||||
{
|
||||
case('S'):
|
||||
if(target._String == "")
|
||||
return(default_value);
|
||||
return(target._String);
|
||||
break;
|
||||
case('F'):
|
||||
return(std::to_string(target._float));
|
||||
break;
|
||||
case('B'):
|
||||
return(target._bool ? "(true)" : "(false)");
|
||||
break;
|
||||
case('M'):
|
||||
return("");
|
||||
break;
|
||||
return(default_value);
|
||||
case('P'):
|
||||
return(std::to_string((u64)target._ptr));
|
||||
break;
|
||||
case('R'):
|
||||
return("");
|
||||
break;
|
||||
return(default_value);
|
||||
}
|
||||
return("");
|
||||
return(default_value);
|
||||
}
|
||||
|
||||
s64 DTree::to_s64()
|
||||
s64 DTree::to_s64(s64 default_value) const
|
||||
{
|
||||
const DTree& target = deref();
|
||||
switch(target.type)
|
||||
@ -215,7 +231,7 @@ s64 DTree::to_s64()
|
||||
{
|
||||
f64 value = 0;
|
||||
if(!dtree_string_to_f64_value(target._String, value))
|
||||
return(0);
|
||||
return(default_value);
|
||||
return(dtree_clamp_to_s64_range((long double)value));
|
||||
}
|
||||
case('F'):
|
||||
@ -226,18 +242,18 @@ s64 DTree::to_s64()
|
||||
{
|
||||
const DTree* item = dtree_scalar_map_value(target);
|
||||
if(item)
|
||||
return(const_cast<DTree*>(item)->to_s64());
|
||||
return(0);
|
||||
return(item->to_s64(default_value));
|
||||
return(default_value);
|
||||
}
|
||||
case('P'):
|
||||
return(dtree_clamp_to_s64_range((long double)(u64)target._ptr));
|
||||
case('R'):
|
||||
return(0);
|
||||
return(default_value);
|
||||
}
|
||||
return(0);
|
||||
return(default_value);
|
||||
}
|
||||
|
||||
u64 DTree::to_u64()
|
||||
u64 DTree::to_u64(u64 default_value) const
|
||||
{
|
||||
const DTree& target = deref();
|
||||
switch(target.type)
|
||||
@ -246,7 +262,7 @@ u64 DTree::to_u64()
|
||||
{
|
||||
f64 value = 0;
|
||||
if(!dtree_string_to_f64_value(target._String, value))
|
||||
return(0);
|
||||
return(default_value);
|
||||
return(dtree_clamp_to_u64_range((long double)value));
|
||||
}
|
||||
case('F'):
|
||||
@ -257,18 +273,18 @@ u64 DTree::to_u64()
|
||||
{
|
||||
const DTree* item = dtree_scalar_map_value(target);
|
||||
if(item)
|
||||
return(const_cast<DTree*>(item)->to_u64());
|
||||
return(0);
|
||||
return(item->to_u64(default_value));
|
||||
return(default_value);
|
||||
}
|
||||
case('P'):
|
||||
return((u64)target._ptr);
|
||||
case('R'):
|
||||
return(0);
|
||||
return(default_value);
|
||||
}
|
||||
return(0);
|
||||
return(default_value);
|
||||
}
|
||||
|
||||
f64 DTree::to_f64()
|
||||
f64 DTree::to_f64(f64 default_value) const
|
||||
{
|
||||
const DTree& target = deref();
|
||||
switch(target.type)
|
||||
@ -277,7 +293,7 @@ f64 DTree::to_f64()
|
||||
{
|
||||
f64 value = 0;
|
||||
if(!dtree_string_to_f64_value(target._String, value))
|
||||
return(0);
|
||||
return(default_value);
|
||||
return(value);
|
||||
}
|
||||
case('F'):
|
||||
@ -288,18 +304,18 @@ f64 DTree::to_f64()
|
||||
{
|
||||
const DTree* item = dtree_scalar_map_value(target);
|
||||
if(item)
|
||||
return(const_cast<DTree*>(item)->to_f64());
|
||||
return(0);
|
||||
return(item->to_f64(default_value));
|
||||
return(default_value);
|
||||
}
|
||||
case('P'):
|
||||
return(dtree_clamp_to_f64_range((long double)(u64)target._ptr));
|
||||
case('R'):
|
||||
return(0);
|
||||
return(default_value);
|
||||
}
|
||||
return(0);
|
||||
return(default_value);
|
||||
}
|
||||
|
||||
bool DTree::to_bool()
|
||||
bool DTree::to_bool(bool default_value) const
|
||||
{
|
||||
const DTree& target = deref();
|
||||
switch(target.type)
|
||||
@ -312,7 +328,11 @@ bool DTree::to_bool()
|
||||
f64 numeric_value = 0;
|
||||
if(dtree_string_to_f64_value(target._String, numeric_value))
|
||||
return(numeric_value != 0);
|
||||
return(dtree_trim(target._String) != "");
|
||||
// Non-empty unparseable strings stay truthy; only a missing/empty
|
||||
// value falls back to the default.
|
||||
if(dtree_trim(target._String) != "")
|
||||
return(true);
|
||||
return(default_value);
|
||||
}
|
||||
case('F'):
|
||||
return(target._float != 0);
|
||||
@ -322,18 +342,18 @@ bool DTree::to_bool()
|
||||
{
|
||||
const DTree* item = dtree_scalar_map_value(target);
|
||||
if(item)
|
||||
return(const_cast<DTree*>(item)->to_bool());
|
||||
return(item->to_bool(default_value));
|
||||
return(target._map.size() > 0);
|
||||
}
|
||||
case('P'):
|
||||
return(target._ptr != 0);
|
||||
case('R'):
|
||||
return(false);
|
||||
return(default_value);
|
||||
}
|
||||
return(false);
|
||||
return(default_value);
|
||||
}
|
||||
|
||||
StringMap DTree::to_stringmap()
|
||||
StringMap DTree::to_stringmap() const
|
||||
{
|
||||
const DTree& target = deref();
|
||||
StringMap result;
|
||||
@ -341,7 +361,7 @@ StringMap DTree::to_stringmap()
|
||||
{
|
||||
case('M'):
|
||||
for(const auto& entry : target._map)
|
||||
result[entry.first] = const_cast<DTree&>(entry.second.deref()).to_string();
|
||||
result[entry.first] = entry.second.deref().to_string();
|
||||
break;
|
||||
case('S'):
|
||||
if(dtree_trim(target._String) != "")
|
||||
@ -350,7 +370,7 @@ StringMap DTree::to_stringmap()
|
||||
case('F'):
|
||||
case('B'):
|
||||
case('P'):
|
||||
result["value"] = const_cast<DTree&>(target).to_string();
|
||||
result["value"] = target.to_string();
|
||||
break;
|
||||
case('R'):
|
||||
break;
|
||||
@ -358,7 +378,7 @@ StringMap DTree::to_stringmap()
|
||||
return(result);
|
||||
}
|
||||
|
||||
String DTree::to_json(char quote_char)
|
||||
String DTree::to_json(char quote_char) const
|
||||
{
|
||||
const DTree& target = deref();
|
||||
switch(target.type)
|
||||
@ -385,7 +405,7 @@ String DTree::to_json(char quote_char)
|
||||
return("\"(unknown)\"");
|
||||
}
|
||||
|
||||
String DTree::get_type_name()
|
||||
String DTree::get_type_name() const
|
||||
{
|
||||
const DTree& target = deref();
|
||||
switch(target.type)
|
||||
@ -412,7 +432,7 @@ String DTree::get_type_name()
|
||||
return("unknown");
|
||||
}
|
||||
|
||||
DTree DTree::get_by_path(String path, String delim)
|
||||
DTree DTree::get_by_path(String path, String delim) const
|
||||
{
|
||||
const DTree* current = &deref();
|
||||
if(path == "")
|
||||
@ -447,7 +467,7 @@ DTree DTree::get_by_path(String path, String delim)
|
||||
return(current->deref());
|
||||
}
|
||||
|
||||
bool DTree::is_reference()
|
||||
bool DTree::is_reference() const
|
||||
{
|
||||
return(type == 'R');
|
||||
}
|
||||
@ -778,12 +798,12 @@ String to_String(DTree t)
|
||||
return(t.to_string());
|
||||
}
|
||||
|
||||
String var_dump(DTree map, String prefix, String postfix)
|
||||
String var_dump(const DTree& map, String prefix, String postfix)
|
||||
{
|
||||
String result = "";
|
||||
if(!map.is_array())
|
||||
return(map.to_string());
|
||||
map.each([&] (DTree item, String key) {
|
||||
map.each([&] (const DTree& item, String key) {
|
||||
result += prefix + key + ": " + item.to_string() + postfix;
|
||||
if(item.is_array())
|
||||
result += var_dump(item, prefix + "\t");
|
||||
|
||||
@ -19,19 +19,22 @@ struct DTree {
|
||||
void* _ptr;
|
||||
std::map<String, DTree> _map;
|
||||
|
||||
void each(std::function <void (const DTree& t, String key)> f);
|
||||
bool is_array();
|
||||
// Read accessors are const and never create or modify nodes. The to_*
|
||||
// conversions take an optional default that is returned when the value is
|
||||
// missing (empty) or cannot be converted to the requested type.
|
||||
void each(std::function <void (const DTree& t, String key)> f) const;
|
||||
bool is_array() const;
|
||||
bool is_list() const;
|
||||
String to_string();
|
||||
s64 to_s64();
|
||||
u64 to_u64();
|
||||
f64 to_f64();
|
||||
bool to_bool();
|
||||
StringMap to_stringmap();
|
||||
String to_json(char quote_char = '"');
|
||||
String get_type_name();
|
||||
DTree get_by_path(String path, String delim = "/");
|
||||
bool is_reference();
|
||||
String to_string(String default_value = "") const;
|
||||
s64 to_s64(s64 default_value = 0) const;
|
||||
u64 to_u64(u64 default_value = 0) const;
|
||||
f64 to_f64(f64 default_value = 0) const;
|
||||
bool to_bool(bool default_value = false) const;
|
||||
StringMap to_stringmap() const;
|
||||
String to_json(char quote_char = '"') const;
|
||||
String get_type_name() const;
|
||||
DTree get_by_path(String path, String delim = "/") const;
|
||||
bool is_reference() const;
|
||||
DTree* reference_target();
|
||||
const DTree* reference_target() const;
|
||||
DTree& deref();
|
||||
@ -63,4 +66,4 @@ struct DTree {
|
||||
};
|
||||
|
||||
String to_String(DTree t);
|
||||
String var_dump(DTree map, String prefix = "", String postfix = "\n");
|
||||
String var_dump(const DTree& map, String prefix = "", String postfix = "\n");
|
||||
|
||||
@ -745,7 +745,19 @@ StringMap split_http_headers(String s)
|
||||
while(header_start < lines.size() && trim(lines[header_start]) == "")
|
||||
header_start++;
|
||||
|
||||
if(header_start < lines.size() && lines[header_start].find(':') == String::npos)
|
||||
// A header line has its colon before any whitespace ("Host: x"); a request
|
||||
// line has a space first even when the URI contains colons
|
||||
// ("GET /x.uce?t=12:30 HTTP/1.1").
|
||||
bool first_line_is_header = false;
|
||||
if(header_start < lines.size())
|
||||
{
|
||||
String first_line = trim(lines[header_start]);
|
||||
size_t colon = first_line.find(':');
|
||||
size_t space = first_line.find_first_of(" \t");
|
||||
first_line_is_header = colon != String::npos && (space == String::npos || colon < space);
|
||||
}
|
||||
|
||||
if(header_start < lines.size() && !first_line_is_header)
|
||||
{
|
||||
String request_line = trim(lines[header_start]);
|
||||
result["REQUEST_METHOD"] = nibble(request_line, " ");
|
||||
|
||||
@ -132,6 +132,26 @@ void ob_close();
|
||||
String ob_get();
|
||||
String ob_get_close();
|
||||
|
||||
// RAII capture used by the preprocessor's @fragment rewrite: buffers the
|
||||
// handler's output and appends it to context.call["fragments"][slot].
|
||||
struct UceFragmentCapture
|
||||
{
|
||||
Request& context;
|
||||
String slot;
|
||||
|
||||
UceFragmentCapture(Request& c, String s) : context(c), slot(s)
|
||||
{
|
||||
ob_start();
|
||||
}
|
||||
|
||||
~UceFragmentCapture()
|
||||
{
|
||||
String html = ob_get_close();
|
||||
if(html != "")
|
||||
context.call["fragments"][slot] = context.call["fragments"][slot].to_string() + html;
|
||||
}
|
||||
};
|
||||
|
||||
String safe_name(String raw);
|
||||
String ascii_safe_name(String raw);
|
||||
|
||||
|
||||
@ -3,16 +3,28 @@
|
||||
#include <stdlib.h>
|
||||
#include "mysql-connector.h"
|
||||
|
||||
static void mysql_register_request_connection(MySQL* db)
|
||||
{
|
||||
if(!context || !db)
|
||||
return;
|
||||
auto& connections = context->resources.mysql_connections;
|
||||
if(std::find(connections.begin(), connections.end(), (void*)db) == connections.end())
|
||||
connections.push_back(db);
|
||||
}
|
||||
|
||||
bool MySQL::connect(String host, String username, String password)
|
||||
{
|
||||
// Register regardless of outcome: tracking is about the wrapper's
|
||||
// lifetime, not the connection's. disconnect()/~MySQL() unregister.
|
||||
mysql_register_request_connection(this);
|
||||
//switch_to_system_alloc();
|
||||
connection = mysql_init(NULL);
|
||||
if (connection == NULL)
|
||||
{
|
||||
auto e = mysql_error((MYSQL*)connection);
|
||||
fprintf(stderr, "%s\n", e);
|
||||
fprintf(stderr, "mysql_init failed\n");
|
||||
//switch_to_arena(context->mem);
|
||||
statement_info.assign(e);
|
||||
_preload_next_error_code = CR_OUT_OF_MEMORY;
|
||||
statement_info = "mysql_init failed";
|
||||
return(false);
|
||||
}
|
||||
|
||||
@ -21,9 +33,11 @@ bool MySQL::connect(String host, String username, String password)
|
||||
{
|
||||
auto e = mysql_error((MYSQL*)connection);
|
||||
fprintf(stderr, "%s\n", e);
|
||||
mysql_close((MYSQL*)connection);
|
||||
//switch_to_arena(context->mem);
|
||||
_preload_next_error_code = CR_UNKNOWN_ERROR;
|
||||
statement_info.assign(e);
|
||||
mysql_close((MYSQL*)connection);
|
||||
connection = NULL;
|
||||
//switch_to_arena(context->mem);
|
||||
return(false);
|
||||
}
|
||||
|
||||
@ -37,8 +51,6 @@ bool MySQL::connect(String host, String username, String password)
|
||||
*/
|
||||
//switch_to_arena(context->mem);
|
||||
statement_info = String("connected");
|
||||
if(context)
|
||||
context->resources.mysql_connections.push_back(this);
|
||||
return(true);
|
||||
}
|
||||
|
||||
@ -192,6 +204,12 @@ DTree MySQL::query(String q)
|
||||
statement_info = "mysql positional ? placeholders are not supported; use named :name placeholders";
|
||||
return(DTree());
|
||||
}
|
||||
if(!connection)
|
||||
{
|
||||
_preload_next_error_code = CR_UNKNOWN_ERROR;
|
||||
statement_info = "mysql connection is not open";
|
||||
return(DTree());
|
||||
}
|
||||
_preload_next_error_code = mysql_query((MYSQL*)connection, q.c_str());
|
||||
DTree result;
|
||||
if(_preload_next_error_code == 0)
|
||||
@ -237,12 +255,8 @@ static bool mysql_has_unquoted_positional_placeholder(String query)
|
||||
|
||||
DTree MySQL::query(String q, StringMap params)
|
||||
{
|
||||
if(mysql_has_unquoted_positional_placeholder(q))
|
||||
{
|
||||
_preload_next_error_code = CR_UNKNOWN_ERROR;
|
||||
statement_info = "mysql positional ? placeholders are not supported; use named :name placeholders";
|
||||
return(DTree());
|
||||
}
|
||||
// Positional ? placeholders survive named substitution (values are always
|
||||
// quoted by escape()), so the check in query(String) covers this path too.
|
||||
return(query(
|
||||
parse_query_parameters(q, params).c_str()
|
||||
));
|
||||
@ -347,6 +361,8 @@ String MySQL::error()
|
||||
_preload_next_error_code = 0;
|
||||
return(p);
|
||||
}
|
||||
if(!connection)
|
||||
return("");
|
||||
const char* res = mysql_error((MYSQL*)connection);
|
||||
if(res)
|
||||
{
|
||||
|
||||
@ -33,6 +33,10 @@ struct MySQL {
|
||||
DTree query(String q, StringMap params);
|
||||
DTree get_pending_result();
|
||||
|
||||
// Unregisters from the request's connection tracking, so stack-allocated
|
||||
// instances cannot leave dangling pointers behind for request cleanup.
|
||||
~MySQL() { disconnect(); }
|
||||
|
||||
};
|
||||
|
||||
MySQL* mysql_connect(String host = "localhost", String username = "root", String password = "")
|
||||
|
||||
@ -70,15 +70,21 @@ bool SQLite::connect(String path)
|
||||
{
|
||||
disconnect();
|
||||
this->path = path;
|
||||
// Register regardless of outcome: tracking is about the wrapper's
|
||||
// lifetime, not the connection's. disconnect()/~SQLite() unregister.
|
||||
sqlite_register_request_connection(this);
|
||||
s32 rc = sqlite3_open_v2(path.c_str(), (sqlite3**)&connection, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, 0);
|
||||
if(rc != SQLITE_OK)
|
||||
{
|
||||
set_error(rc, "sqlite open failed for " + path);
|
||||
disconnect();
|
||||
if(connection)
|
||||
{
|
||||
sqlite3_close((sqlite3*)connection);
|
||||
connection = 0;
|
||||
}
|
||||
return(false);
|
||||
}
|
||||
sqlite3_busy_timeout((sqlite3*)connection, 5000);
|
||||
sqlite_register_request_connection(this);
|
||||
if(!apply_default_pragmas())
|
||||
return(false);
|
||||
statement_info = "connected";
|
||||
@ -278,9 +284,11 @@ SQLite* sqlite_connect(String path)
|
||||
}
|
||||
|
||||
SQLite* db = new SQLite();
|
||||
db->worker_cache = true;
|
||||
if(db->connect(path) && db->connection)
|
||||
{
|
||||
db->worker_cache = true;
|
||||
sqlite_worker_connection_cache[path] = db;
|
||||
}
|
||||
else
|
||||
db->request_cleanup_delete = true;
|
||||
return(db);
|
||||
@ -339,6 +347,10 @@ void cleanup_sqlite_connections()
|
||||
context->resources.sqlite_connections.pop_back();
|
||||
if(db->worker_cache)
|
||||
{
|
||||
// A page that ran BEGIN and faulted must not leak its transaction
|
||||
// (and the WAL write lock) into the next request on this worker.
|
||||
if(db->connection && !sqlite3_get_autocommit((sqlite3*)db->connection))
|
||||
sqlite3_exec((sqlite3*)db->connection, "ROLLBACK", 0, 0, 0);
|
||||
db->affected_rows = 0;
|
||||
db->insert_id = 0;
|
||||
db->error_code = SQLITE_OK;
|
||||
|
||||
@ -17,6 +17,10 @@ struct SQLite {
|
||||
DTree query(String q);
|
||||
DTree query(String q, const StringMap& params);
|
||||
|
||||
// Unregisters from the request's connection tracking, so stack-allocated
|
||||
// instances cannot leave dangling pointers behind for request cleanup.
|
||||
~SQLite() { disconnect(); }
|
||||
|
||||
private:
|
||||
void set_error(s32 code, String info = "");
|
||||
bool apply_default_pragmas();
|
||||
|
||||
@ -17,17 +17,12 @@ constexpr f64 FILE_LOCK_WAIT_TIMEOUT_SECONDS = 3.0;
|
||||
|
||||
}
|
||||
|
||||
String capture_backtrace_string(u32 max_frames, u32 skip_frames)
|
||||
String backtrace_frames_string(void* const* frames, size_t size, u32 skip_frames)
|
||||
{
|
||||
if(max_frames == 0)
|
||||
return("");
|
||||
|
||||
std::vector<void*> frames(max_frames);
|
||||
size_t size = backtrace(frames.data(), max_frames);
|
||||
if(size == 0)
|
||||
return("");
|
||||
|
||||
char** symbols = backtrace_symbols(frames.data(), size);
|
||||
char** symbols = backtrace_symbols(frames, size);
|
||||
if(!symbols)
|
||||
return("");
|
||||
|
||||
@ -38,7 +33,17 @@ String capture_backtrace_string(u32 max_frames, u32 skip_frames)
|
||||
trace += "\n";
|
||||
}
|
||||
free(symbols);
|
||||
return(trace);
|
||||
return(trace);
|
||||
}
|
||||
|
||||
String capture_backtrace_string(u32 max_frames, u32 skip_frames)
|
||||
{
|
||||
if(max_frames == 0)
|
||||
return("");
|
||||
|
||||
std::vector<void*> frames(max_frames);
|
||||
size_t size = backtrace(frames.data(), max_frames);
|
||||
return(backtrace_frames_string(frames.data(), size, skip_frames));
|
||||
}
|
||||
|
||||
String signal_name(int sig)
|
||||
|
||||
@ -65,6 +65,7 @@ bool ws_send(String message, bool binary = false, String scope = "");
|
||||
bool ws_send_to(String connection_id, String message, bool binary = false);
|
||||
bool ws_close(String connection_id = "");
|
||||
|
||||
String backtrace_frames_string(void* const* frames, size_t size, u32 skip_frames = 0);
|
||||
String capture_backtrace_string(u32 max_frames = 32, u32 skip_frames = 0);
|
||||
String signal_name(int sig);
|
||||
|
||||
|
||||
@ -366,14 +366,9 @@ DTree request_route_from_raw_path(String raw_path, String default_path)
|
||||
|
||||
void request_populate_context_params(Request& context, String default_path)
|
||||
{
|
||||
DTree route = request_query_route(context, default_path);
|
||||
String script_url = request_script_url(context);
|
||||
context.params["SCRIPT_URL"] = script_url;
|
||||
context.params["BASE_URL"] = request_base_url_from_script_url(script_url);
|
||||
context.params["ROUTE_PATH"] = route["l_path"].to_string();
|
||||
context.params["ROUTE_PAGE"] = route["page"].to_string();
|
||||
context.params["ROUTE_PATH_RAW"] = route["raw_path"].to_string();
|
||||
context.params["ROUTE_VALID"] = route["valid"].to_bool() ? "1" : "0";
|
||||
String raw_path;
|
||||
parse_query(context.params["QUERY_STRING"], &raw_path);
|
||||
request_populate_context_params_from_route(context, raw_path, default_path);
|
||||
}
|
||||
|
||||
void request_populate_context_params_from_route(Request& context, String raw_path, String default_path)
|
||||
|
||||
@ -18,7 +18,10 @@ static sigjmp_buf request_fault_jmp;
|
||||
static volatile sig_atomic_t request_fault_active = 0;
|
||||
static volatile sig_atomic_t request_fault_signal = 0;
|
||||
static Request* request_fault_request = 0;
|
||||
static String request_fault_trace = "";
|
||||
// Raw frame pointers only: the signal handler must not allocate, so frames are
|
||||
// captured here and symbolized after the siglongjmp.
|
||||
static void* request_fault_frames[64];
|
||||
static volatile sig_atomic_t request_fault_frame_count = 0;
|
||||
static int websocket_exec_fd = -1;
|
||||
static String websocket_exec_read_buffer = "";
|
||||
static std::deque<DTree> websocket_exec_pending_jobs;
|
||||
@ -105,7 +108,7 @@ void on_request_fault_signal(int sig)
|
||||
request_fault_signal = sig;
|
||||
if(request_fault_active && request_fault_request)
|
||||
{
|
||||
request_fault_trace = capture_backtrace_string(32, 1);
|
||||
request_fault_frame_count = backtrace(request_fault_frames, 64);
|
||||
siglongjmp(request_fault_jmp, 1);
|
||||
}
|
||||
on_segfault(sig);
|
||||
@ -887,7 +890,7 @@ int handle_complete(FastCGIRequest& request) {
|
||||
request_fault_request = &request;
|
||||
request_fault_active = 1;
|
||||
request_fault_signal = 0;
|
||||
request_fault_trace = "";
|
||||
request_fault_frame_count = 0;
|
||||
install_request_fault_handlers();
|
||||
|
||||
String failure_title = "";
|
||||
@ -898,7 +901,7 @@ int handle_complete(FastCGIRequest& request) {
|
||||
{
|
||||
failure_title = "fatal signal during request";
|
||||
failure_details = "worker recovered before closing the upstream connection";
|
||||
failure_trace = request_fault_trace;
|
||||
failure_trace = backtrace_frames_string(request_fault_frames, request_fault_frame_count, 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -1502,6 +1505,10 @@ void init_base_process()
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
// Warm up libgcc's backtrace state so the first in-handler backtrace()
|
||||
// after a fault does not allocate.
|
||||
backtrace(request_fault_frames, 4);
|
||||
|
||||
init_base_process();
|
||||
ensure_proactive_compiler();
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
37
tests/plugins/uce_demo_smoke.py
Normal file
37
tests/plugins/uce_demo_smoke.py
Normal file
@ -0,0 +1,37 @@
|
||||
from pathlib import Path
|
||||
|
||||
# The demo pages are not regression suites, but every one of them must at
|
||||
# least compile and render: a preprocessor or runtime change that breaks a
|
||||
# demo page (e.g. literal text that looks like an entry point) should fail CI.
|
||||
|
||||
ERROR_MARKERS = [
|
||||
"uce compile error",
|
||||
"fatal signal during request",
|
||||
"uncaught exception during request",
|
||||
]
|
||||
|
||||
|
||||
def _repo_root():
|
||||
return Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def _make_demo_case(page_path):
|
||||
def run(context):
|
||||
response = context.expect_status(page_path, 200)
|
||||
body_lower = response.text.lower()
|
||||
for marker in ERROR_MARKERS:
|
||||
if marker in body_lower:
|
||||
raise Exception("response body contained error marker %r for %s" % (marker, page_path))
|
||||
return "HTTP 200 without error markers for %s" % page_path
|
||||
|
||||
return run
|
||||
|
||||
|
||||
def register(registry):
|
||||
demo_dir = _repo_root() / "site" / "demo"
|
||||
for path in sorted(demo_dir.glob("*.uce")):
|
||||
registry.case(
|
||||
"demo page " + path.name,
|
||||
_make_demo_case("/demo/" + path.name),
|
||||
tags=["http", "smoke", "uce", "demo", "public"],
|
||||
)
|
||||
@ -12,7 +12,9 @@ def register(registry):
|
||||
starter_pages = [
|
||||
("starter home", "/examples/uce-starter/", 200, "Stunning Apps"),
|
||||
("starter dashboard", "/examples/uce-starter/?dashboard", 200, "Dashboard"),
|
||||
("starter dashboard ONCE assets reach head", "/examples/uce-starter/?dashboard", 200, "views/dashboard.css"),
|
||||
("starter workspace nested route", "/examples/uce-starter/?workspace/projects", 200, "Workspace"),
|
||||
("starter workspace ONCE assets reach head", "/examples/uce-starter/?workspace/projects", 200, "css/workspace.css"),
|
||||
("starter ajax section", "/examples/uce-starter/?page2-section1", 200, "UCE starter AJAX fragment response"),
|
||||
("starter route traversal blocked", "/examples/uce-starter/?../../../demo/index", 404, "The requested page does not exist."),
|
||||
]
|
||||
|
||||
@ -14,6 +14,12 @@ from dataclasses import asdict, dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Callable, Dict, Iterable, List, Optional
|
||||
|
||||
# When executed as a script this module is "__main__"; register it under its
|
||||
# real name too, so plugins doing `from run_network_tests import TestFailure`
|
||||
# get this module instance (and the same TestFailure class) instead of a
|
||||
# re-imported copy whose exceptions the runner's except clause cannot catch.
|
||||
sys.modules.setdefault("run_network_tests", sys.modules[__name__])
|
||||
|
||||
|
||||
@dataclass
|
||||
class Target:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user