Skip to content

Commit fdea135

Browse files
committed
TASK-192: Implementation complete
Add owned v3 cookie parsing and atomic typed Set-Cookie rendering with preserved byte semantics, canonical attributes, bounded GMT dates and injection guards. Bind named parity evidence to actual executable and log identities; ship public, installed shared/static and managed/external raw-wire coverage with explicit migration documentation. Verified four normal/ASan+UBSan TLS-off/on lanes, complete fresh normal and sanitized parity (60 capabilities, 360 cells, 168 observations each), eight strict installed consumer runs, header/linkage/distribution checks, genuine Doxygen, cpplint, Python contracts and diff hygiene. Task remains In Progress pending caller validation/finalization; nonlocal platforms remain CI/v3-PR owned.
1 parent 94195e7 commit fdea135

23 files changed

Lines changed: 966 additions & 35 deletions

‎docs/task-179-behavior-parity.md‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,12 @@ also verify snapshot stability across validated rebinding.
8181

8282
v3 retains raw escaped target/query, ordered fields and separate trailers; it
8383
has no merged v2 query/form/path map. Applications opt into bounded decoders;
84-
path captures use exchange::path_args(). Cookies use the standalone parser/render
85-
API and ordered Cookie/Set-Cookie fields. Request bodies are pulled and footers
84+
path captures use exchange::path_args(). Cookies use the owned `http::cookie` value,
85+
`http::parse_cookie_header` and typed `http::render_set_cookie` operations with
86+
ordered Cookie/Set-Cookie fields. TASK-192 supplies the installed helper and the
87+
`http_cookie` suite with all three capability-054 identities. Receipts bind named
88+
checks to the actual executable/hash, command, complete log and executed counts;
89+
semantic supporting evidence does not claim per-protocol wire observations. Request bodies are pulled and footers
8690
are accessed after end-of-body. Malformed encodings and limits return typed errors.
8791
Multipart disk sinks have explicit abort cleanup, keep-once callbacks and sanitized
8892
names, verified by named decoder and disk lifecycle tests.

‎docs/v2-to-v3-migration.md‎

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,31 @@ application work still consumes a worker.
6262
| `get_content` | `admit_body(body_policy{cap})`, then `co_await body().collect(cap)` or incremental `read_some()` | Admission before reads; typed limits, cancellation and protocol errors |
6363
| `get_footers` | `body().trailers()` after end-of-body | Trailers remain separate from initial fields |
6464
| Multipart uploaded-file map and cleanup callbacks | `forms::read_multipart`, `part_sink`, `temp_file_part_sink`, `temp_file_options::should_keep` | Limits are snapshotted at invocation; exchange and part sink remain borrowed through task completion. Partial parts abort exactly once; disk files clean up on abort/destruction; keep callback runs once per completed file, throws mean remove; client names sanitized |
65-
| `get_cookie` / response `with_cookie` | Ordered Cookie/Set-Cookie fields; parse and render cookie values in application code | Preserve repeated fields; no native request/response convenience aliases |
65+
| `get_cookie` / response `with_cookie` | `http::parse_cookie_header`, `http::render_set_cookie`, ordered Cookie/Set-Cookie fields | Preserve repeated fields; no native request/response convenience aliases |
6666

6767
Form/query decoding is optional, bounded, and reports malformed encodings.
6868
A query is the substring after `?` in `raw_target`; decoding it does not modify
6969
the raw target. Body trailers are meaningful only after a completed body read.
7070

71-
The legacy cookie class is removed. Native headers retain repeated Cookie and
72-
Set-Cookie field values; applications own value parsing and rendering.
71+
Include `<httpserver/http/cookie.hpp>` directly or `<httpserver/http.hpp>`.
72+
`http::cookie` owns raw strings and attributes; `parse_cookie_header` returns
73+
owned pairs in token order. Iterate every `head_fields.all("cookie")` occurrence
74+
in received order. Parsing trims SP/HT at token/name edges, splits at the first
75+
`=`, strips one outer quote pair and skips malformed/empty-name tokens. It does
76+
not URL-decode, trim leading value whitespace or remove interior controls.
77+
78+
`render_set_cookie(value, output)` validates before replacing output and returns
79+
`http::outcome_code::invalid_argument` on invalid input. Append output to an
80+
ordinary `Set-Cookie` field only on success, once per cookie. This replaces v2
81+
fluent setters and throwing rendering; direct mutation is raw until rendering.
82+
The renderer rejects CR/LF/NUL/semicolon, controls/whitespace/equals in names,
83+
and commas in Domain/Path. Empty rendered names are invalid. Attribute order is
84+
Expires, Max-Age, Domain, Path, Secure, HttpOnly, SameSite; None implies Secure.
85+
Expires uses locale/timezone-independent GMT Unix seconds in years 0000..9999;
86+
out-of-range times now fail explicitly instead of platform-dependent conversion.
87+
Signed Max-Age, empty values, case and percent bytes remain unchanged. Existing
88+
field budgets bound input; there is no cookie jar or cached request convenience.
89+
The `http_cookie` suite preserves the three named capability-054 witnesses.
7390

7491
## Response ownership and producers
7592

‎docs/v3-api.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,17 @@ provide owned/reopened/leased body sources and per-send producers.
3535
\include v3_routes.cpp
3636
\include v3_route_handlers.hpp
3737

38+
\section native_v3_cookies Owned cookie values
39+
40+
Include <httpserver/http/cookie.hpp> directly or through <httpserver/http.hpp>.
41+
\ref httpserver::http::parse_cookie_header returns owned raw cookie pairs;
42+
iterate every ordered Cookie field. Construct or modify
43+
\ref httpserver::http::cookie attributes, then call
44+
\ref httpserver::http::render_set_cookie and append the output as a separate
45+
Set-Cookie field only on success. Invalid input yields invalid_argument and
46+
leaves output unchanged. The migration guide documents exact parsing,
47+
canonical attribute ordering, SameSite=None and bounded GMT Expires behavior.
48+
3849
\section native_v3_websockets WebSocket sessions
3950

4051
\ref httpserver::exchange::upgrade returns a typed rejection or owning

‎scripts/run-v3-behavior-parity.py‎

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -273,8 +273,10 @@ def check_receipt(root, inventory, receipt):
273273
errors.append('missing live policy refusal witness')
274274
if item.get('mode') in ('h2-tls', 'h3-quic') and type(witness.get('stream')) is not int:
275275
errors.append('missing refused stream identity')
276-
elif item['name'] not in executed_tests((root / command['log']).read_text()):
277-
errors.append('named supporting test did not execute successfully')
276+
else:
277+
log = (root / command['log']).read_text()
278+
if item['name'] not in executed_tests(log) or not matching_execution(command, item['suite'], log):
279+
errors.append('named supporting test lacks matching suite execution provenance')
278280
outcomes = {}
279281
for outcome in receipt['outcomes']:
280282
key = (outcome['capability'], outcome['mode'])
@@ -340,15 +342,51 @@ def check_receipt(root, inventory, receipt):
340342
return errors
341343

342344

345+
def named_execution(binary, suite, log):
346+
"""Bind named checks to the invoked executable, including libtool wrappers."""
347+
actual = binary.parent / '.libs' / binary.name
348+
actual = actual if actual.is_file() else binary
349+
return {'suite': suite, 'executable': str(actual.resolve()),
350+
'sha256': hashlib.sha256(actual.read_bytes()).hexdigest(),
351+
'log_sha256': hashlib.sha256(log.encode()).hexdigest(),
352+
'tests': len(executed_tests(log)),
353+
'checks': int(re.search(r'^(\d+) checks$', log, re.MULTILINE)[1])}
354+
355+
356+
def matching_execution(command, suite, log):
357+
proof = command.get('named_execution', {})
358+
argv = command['argv']
359+
cwd = Path(command.get('cwd', ''))
360+
if not isinstance(proof, dict) or not cwd.is_absolute() or not argv or proof.get('suite') != suite:
361+
return False
362+
binary = (cwd / argv[-1]).resolve()
363+
if binary.name != suite or not binary.is_file():
364+
return False
365+
if len(argv) != 1:
366+
# The sole supported launcher is the explicitly recorded peer_policy affinity.
367+
if suite != 'peer_policy' or len(argv) != 4 or Path(argv[0]).name != 'taskset' \
368+
or argv[1] != '-c' or not argv[2].isdigit():
369+
return False
370+
if not executed_tests(log):
371+
return False
372+
expected = named_execution(binary, suite, log)
373+
return proof == expected and type(proof.get('tests')) is int and type(proof.get('checks')) is int
374+
375+
343376
def executed_tests(log):
344377
"""Require completed named Littletest checks, never merely a zero exit."""
345-
names = re.findall(r'^Running test \(\d+\): ([\w]+)$', log, re.MULTILINE)
378+
running = re.findall(r'^Running test \((\d+)\): ([\w]+)$', log, re.MULTILINE)
379+
names = [name for _, name in running]
346380
completed = re.findall(r'^- Time spent during "([\w]+)":', log, re.MULTILINE)
347-
count = re.search(r'^(\d+) tests executed$', log, re.MULTILINE)
348-
checks = re.search(r'^(\d+) checks$', log, re.MULTILINE)
349-
if not count or not checks or int(checks[1]) < 1 or int(count[1]) != len(names) or names != completed \
350-
or len(set(names)) != len(names) or not re.search(r'^-> 0 failures$', log, re.MULTILINE) \
351-
or not re.search(r'^-> 0 skipped$', log, re.MULTILINE):
381+
counts = [re.findall(pattern, log, re.MULTILINE) for pattern in
382+
(r'^(\d+) tests executed$', r'^(\d+) checks$', r'^-> (\d+) successes$',
383+
r'^-> (\d+) failures$', r'^-> (\d+) skipped$')]
384+
if any(len(values) != 1 for values in counts):
385+
return set()
386+
tests, checks, successes, failures, skipped = (int(values[0]) for values in counts)
387+
if checks < 1 or tests != len(names) or checks != successes or names != completed \
388+
or [int(number) for number, _ in running] != list(range(1, tests + 1)) \
389+
or len(set(names)) != len(names) or failures or skipped:
352390
return set()
353391
return set(names)
354392

@@ -419,6 +457,7 @@ def execute(identity, argv, cwd=root, tool=None, timeout=300):
419457
named = executed_tests(log)
420458
if not named:
421459
raise RuntimeError(lane + '-' + suite + ' has failed, skipped, empty or incomplete named checks')
460+
receipt['commands'][-1]['named_execution'] = named_execution(binary, suite, log)
422461
if lane == 'on':
423462
for name in sorted(named):
424463
kinds = {test['kind'] for cap in inventory['capabilities'] for test in cap.get('tests', [])
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# TASK-192 interface decision and doubts
2+
3+
Constraints: owned byte-transparent cookie pairs, no v2 classes, ordinary ordered fields, libhttpserver outcomes, one mandatory render validation boundary. Inspection finds no reusable public `result<T>`; existing HTTP helpers use optional or named result records.
4+
5+
| Alternative | Depth | Locality | Seam |
6+
|---|---|---|---|
7+
| Validated class with fluent setters and static parsing | Hides validation but many entry points | Every setter plus raw parse bypass | Raw input needs a second state representation |
8+
| Owned aggregate plus explicit parse/render | Two operations hide tokenization, validation and date formatting | All emission checks in one renderer | Matches callers composing ordered fields |
9+
| Field adapter parsing/adding Cookie/Set-Cookie automatically | Convenient repeated fields, wider interface | Couples cookie and field policies | Hides allocation and application failure decisions |
10+
11+
Choose owned `http::cookie`, `parse_cookie_header(string_view) -> vector<cookie>` and `render_set_cookie(cookie, string&) -> outcome`. The output reference follows atomic state replacement: errors leave output unchanged. Raw construction/mutation is explicit; validation is deferred until rendering, including parsed values. Applications append a successful value once as an ordinary Set-Cookie field. This deliberately migrates v2 fluent setters/throwing exceptions, not preserved wire semantics. No generic result framework or adapter.
12+
13+
Decision to refute: a single renderer can safely validate every raw construction path without losing lenient parser behavior.
14+
15+
| Doubt | Discriminator/clearing evidence required |
16+
|---|---|
17+
| Borrowed input outlives parsing | Destroy/overwrite original input then render owned copies |
18+
| Parsed CR or mutable attributes bypass safety | Explicit CR/name preconditions; typed failure and unchanged sentinel for CR/LF/NUL/semicolon and comma attributes |
19+
| Whitespace/quotes change legacy parsing | Legacy code inspection: trim only SP/HT at token/name ends; retain value-leading whitespace, first equals, strip exactly one matching quote pair |
20+
| Date overflows or depends on locale/timezone | C++20 civil date conversion with bounded years 0000..9999; epoch, negative second, leap day, extrema and int64 overflow tests; fixed English spelling |
21+
| Unrelated executable proves named parity | Command path/suite match and real binary SHA-256, native positive execution, adversarial absent/mismatched/wrong-case/count/skip/completion fixtures |
22+
| Allocation grows beyond input | Linear scan, one owned pair per nonempty input token; no cache/global state; input remains governed by existing field budgets |
23+
| Reflection creates another wire message | Independent raw TCP managed/external consumer checks exact repeated response fields, rejected attempted injection and one response |
24+
25+
Rollback: revert the additive helper/registration before v3 API freeze. No persistent state changes. All interface doubts are cleared by the named native cases, real positive/provenance runner contracts, four lane receipts and installed managed/external raw-wire consumers in [implementation evidence](TASK-192-implementation-evidence.md). Ownership, injection, formatting and storage discriminators have no unresolved result.

0 commit comments

Comments
 (0)